Chapter 22: Frameworks Overview
PHP has a healthy ecosystem of frameworks that help you deliver projects with predictable structure and batteries included. This chapter gives you a practical overview of Laravel, Symfony, and Slim; it also shows how to choose a framework, how to test inside each ecosystem, and how to migrate between them without rewriting everything at once.
Laravel: Routing, Eloquent, and Blade
Laravel provides batteries that fit together out of the box. You get routing, an expressive ORM named Eloquent, a templating system named Blade, queues, mail, caching, and standard project structure. The defaults encourage sensible conventions for modern applications.
Registering routes and controllers clearly
Routes map verbs and paths to controller actions. Small projects can use closures; larger projects use dedicated controllers so you can test actions in isolation.
// routes/web.php
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
Route::get('/articles', [ArticleController::class, 'index']);
Route::get('/articles/{id}', [ArticleController::class, 'show']);
Route::post('/articles', [ArticleController::class, 'store']);
// app/Http/Controllers/ArticleController.php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index()
{
return view('articles.index', ['articles' => Article::latest()->paginate(10)]);
}
public function show(int $id)
{
$article = Article::findOrFail($id);
return view('articles.show', compact('article'));
}
public function store(Request $request)
{
$data = $request->validate(['title' => 'required|string|max:255']);
$article = Article::create($data);
return redirect('/articles/' . $article->id);
}
}
Modeling data with Eloquent conveniently
Eloquent models wrap tables and relationships. You define fillable fields and relationships; then you compose queries fluently. Under the hood it uses a query builder so you can drop to raw queries when needed.
// app/Models/Article.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = ['title', 'body'];
public function author()
{
return $this->belongsTo(User::class);
}
}
Rendering views with Blade templates
Blade templates compile to cached PHP. You use sections, layouts, and components to keep markup tidy while still writing plain HTML.
<!-- resources/views/articles/index.blade.php -->
@extends('layouts.app')
@section('content')
<h1>Articles</h1>
@foreach ($articles as $article)
<article>
<h2>{{ $article->title }}</h2>
<p>{{ Str::limit($article->body, 120) }}</p>
</article>
@endforeach
{{ $articles->links() }}
@endsection
Symfony: Components and Flex
Symfony is a collection of well tested components and a full stack framework. You can assemble only what you need, or you can start with the standard skeleton that includes HTTP kernel, routing, dependency injection, console, and Twig for views. Flex streamlines installation and configuration through recipes.
Routing and controllers with attributes
Controllers are simple classes; methods receive the request and return a response. Attributes on methods make routes discoverable and local to the action.
// src/Controller/ArticleController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ArticleController extends AbstractController
{
#[Route('/articles', name: 'article_index', methods: ['GET'])]
public function index(): Response
{
return $this->render('article/index.html.twig', ['articles' => []]);
}
}
Composing with components and Flex recipes
You can install components like symfony/http-client, mailer, or messenger as needed. Flex recipes create configuration files and register services so you can focus on your code rather than wiring.
# composer.json snippet …
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
maker commands for consistent scaffolding. Generators save time and ensure new classes are registered in the container correctly.
Slim and Microframework Patterns
Slim focuses on fast routing and middleware with minimal opinions. You bring your preferred template engine, ORM, and container. This works well for APIs and services that need a small surface area with predictable behavior.
Defining routes and middleware succinctly
Middleware wraps your application like layers of an onion. You can add error handlers, authentication, and JSON emission in small reusable functions.
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->get('/articles', function (Request $req, Response $res) {
$payload = json_encode(['data' => [['id' => 1, 'title' => 'Hello']]]);
$res->getBody()->write($payload);
return $res->withHeader('Content-Type', 'application/json');
});
$app->run();
Wiring a container for services
Bring a PSR-11 container to manage services and configuration. This keeps construction logic in one place and makes testing easier.
use DI\Container;
use Slim\Factory\AppFactory;
$container = new Container();
$container->set('db.dsn', 'sqlite::memory:');
AppFactory::setContainer($container);
$app = AppFactory::create();
// resolve services via $this->get('db.dsn') inside route callables when needed
Selecting a Framework
Choose based on team experience, project lifespan, ecosystem features, and operational needs. Laravel favors rapid delivery with strong defaults. Symfony favors explicit composition and long term maintainability. Slim favors small services where you assemble only what you need.
Comparing strengths at a glance
The following table summarizes common considerations for typical web projects. You can adapt these criteria to your own context.
| Criterion | Laravel | Symfony | Slim |
| Learning curve | Gentle with opinionated defaults | Moderate with explicit configuration | Gentle if you already know PSR stack |
| Out of the box features | Rich batteries included | Powerful when you add packs | Minimal core; you add pieces |
| Best fit | Full stack apps and dashboards | Large codebases and long support horizons | APIs and microservices |
| Customization style | Extend conventions | Assemble components | Pick libraries |
Testing Inside Frameworks
Frameworks integrate test runners, HTTP clients, and helpers so you can write fast unit tests and realistic feature tests. Aim for a pyramid with many unit tests and fewer end to end tests that hit a real database or HTTP boundary.
Writing Laravel feature tests with Pest or PHPUnit
Laravel ships with helpers for making requests and asserting responses. You can seed data and use in memory databases for speed where possible.
// tests/Feature/ArticleTest.php
it('lists articles', function () {
\App\Models\Article::factory()->count(2)->create();
$resp = $this->get('/articles');
$resp->assertStatus(200)
->assertSee('Articles');
});
Using Symfony test client for HTTP assertions
Symfony provides a kernel test client that boots the container in a test environment. You can request URLs and assert status, headers, and content.
// tests/Controller/ArticleControllerTest.php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ArticleControllerTest extends WebTestCase
{
public function testIndex(): void
{
$client = static::createClient();
$client->request('GET', '/articles');
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('h1');
}
}
Migration Between Frameworks
Migrations usually happen when requirements change or when teams consolidate stacks. Plan a route that reduces risk by keeping business logic independent from controllers and HTTP wiring. Adopt a strangler pattern that proxies some routes to the new system while the old system continues to serve the rest.
Extracting domain code into plain PHP first
Move services and use cases into framework agnostic classes that depend on interfaces. Adapters in each framework wire those interfaces to HTTP, CLI, or persistence. Once the domain is independent, swapping the web layer is smaller and safer.
// Domain service independent of framework
namespace Domain;
interface ArticleRepository {
public function find(int $id): ?Article;
public function save(Article $a): void;
}
final class PublishArticle {
public function __construct(private ArticleRepository $repo) {}
public function __invoke(array $input): int {
// validate and persist
// return new id
return 1;
}
}
Proxying routes during a staged cutover
Introduce an edge proxy that forwards selected paths to the new app. Keep cookies, sessions, and authentication unified at the edge so users do not notice the change while you move endpoints gradually.