A presentation at Lava JUG in in Clermont-Ferrand, France by Horacio Gonzalez

Rediscovering PHP Modern Practices Beyond Legacy Horacio Gonzalez 2025-12-02
Who are we? Introducing myself and introducing Clever Cloud
Horacio Gonzalez @LostInBrittany Spaniard Lost in Brittany Old(ish) Developer
Clever Cloud From Code to Product
You’re going to talk about PHP ?!?! The elephpant IS in the room
So yeah, I know both PHP and its reputation… And in 2004 is was quite accurate, PHP felt like the Wild West
If you think PHP is inconsistent, insecure, and strictly procedural… You were right 15 years ago !
I’ve been a Java developer since ‘97 And I have done tons of frontend in HTML/CSS/JS
But I’ve also written and debugged lots of PHP I even made a living of it!
For years I kept away from PHP I reckon I was a bit snobbish about it…
Until I joined Clever Cloud and rediscovered it And, mate, it was so much better!
PHP is not like PHP 4 anymore PHP powers 75%+ of the web… and 90% of that usage looks nothing like WordPress 3.0
PHP – The Language Syntax for the Java Mind
Code Organization: Namespaces & Autoloading The Good Old Days The Modern Way include ‘config.php’; namespace App\Http\Controllers; include ‘database.php’; use App\Services\UserService; include ‘functions.php’; use App\Models\User; include ‘models/user.php’; use App\Mail\WelcomeEmail; include ‘models/admin.php’; include ‘controllers/user_controller.php’; class UserController include ‘lib/email.php’; { include ‘lib/validator.php’; public function __construct( // … 50 more includes …// Everything is global private UserService $userService $user = new User(); ) {} $admin = new Admin(); // Name collision disasters waiting to happen: function send_email() { } // From email.php … } function send_email() { } // Oops, redeclared in another file! PSR-4 Autoloading: clean namespace hierarchy
Typing The Good Old Days The Modern Way function createUser($name, $age, $active) { declare(strict_types=1); // <—- The “Java Switch” // Is $age an int? “20”? 20.5? // Contract-based programming. If you violate the contract, // Is $active a boolean? 0? 1? “yes”? // the application stops immediately. No guessing. final class UserFactory if ($active == 1) { { return “User: ” . $name . ” is ” . $age; // Typed Arguments, Return Type, Visibility } public function create( string $name, int $age, bool $isActive // Returns null implicitly if condition fails ): string { // Inconsistent return types! if ($isActive) { } return sprintf(“User: %s is %d”, $name, $age); } $u = createUser(“Horacio”, “45”, “true”); // Works, but relies on “Magic” casting. throw new InactiveUserException(); } } From “Anything Goes” to Strictness
The Type System Flex (Union & Intersection Types) The Modern Way public function handle(User|Guest $entity): Response { // Type system knows it’s one of these // IDE autocomplete works // Static analysis validates it return match(true) { $entity instanceof User => $this->handleUser($entity), $entity instanceof Guest => $this->handleGuest($entity), }; } // Return type unions: much cleaner than Optional public function findUser(int $id): User|null { return $this->repository->find($id); } // Must satisfy BOTH interfaces public function log(Loggable&Serializable $event): void No need for overloading boilerplate.
Constructor Property Promotion (PHP 8.0+) The Good Old Days The Modern Way class User class UserProfile { { public function __construct( private string $name; // Visibility + Type + Name = Property Created & Assigned private string $email; private string $name, private int $age; private string $email, private int $age, public function __construct(string $name, string $email, int $age) ) {} { // That’s it. Properties are declared, assigned, and accessible. $this->name = $name; // Getters are optional - you decide based on your encapsulation needs. $this->email = $email; $this->age = $age; } public function getName(): string { return $this->name; } public function getEmail(): string { return $this->email; } public function getAge(): int { return $this->age; } } // Want to make it immutable like a Java Record? // Just add ‘readonly’ (PHP 8.2+): class ImmutableUser { public function __construct( } public readonly string $name, public readonly string $email, ) {} }
Attributes: the End of “Code in Comments” The Good Old Days The Modern Way class UserController use Symfony\Component\Routing\Attribute\Route; { use Symfony\Component\Security\Http\Attribute\IsGranted; /** * @Route(“/api/users”, methods={“GET”}) class UserController
// Native syntax. “Route” is a real class found via “use”
Enums (The Real Deal) The Good Old Days The Modern Way class BlogPost / 1. Defined using ‘enum’ keyword { enum Status: string // These are just strings. { // Nothing prevents me from passing “garbage” to a function expecting a case Draft = ‘draft’; status. case Published = ‘published’; const STATUS_DRAFT = ‘draft’; case Archived = ‘archived’; const STATUS_PUBLISHED = ‘published’; // 2. THEY CAN HAVE METHODS! (Just like Java) const STATUS_ARCHIVED = ‘archived’; public function color(): string { public function setStatus(string $status) { return match($this) { // I have to manually validate if $status is one of the allowed self::Draft => ‘gray’, constants self::Published => ‘green’, if (!in_array($status, [self::STATUS_DRAFT, …])) { self::Archived => ‘red’, throw new Exception(); }; } $this->status = $status; } } } } // 3. Type Safety is absolute function updateStatus(Status $newStatus): void { // Impossible to pass “foo” or “draft” string here. } // 4. Accessing values echo Status::Published->value; // “published” echo Status::Published->color(); // “green”
Match Expressions The Good Old Days // The problem: The Modern Way $status = 200; // 1. Loose comparison (‘200’ string matches 200 int) // 2. Requires ‘break’ (easy to forget = fallthrough bugs) // 1. Assign result directly to variable // 3. Verbose assignment logic // 2. Strict comparison (‘200’ string will NOT match 200 int) // 3. Comma-separated values for multiple matches $status = 200; $message = null; $message = match ($status) { 200 => ‘OK’, switch ($status) { 300, 301 => ‘Redirect’, case 200: 404 => ‘Not Found’, $message = ‘OK’; break; default => ‘Unknown’, }; case 300: case 301: // Bonus: Pattern matching in PHP 8 allows logic inside match! $message = ‘Redirect’; /* break; $result = match (true) { case 404: $age >= 18 => ‘Adult’, $message = ‘Not Found’; break; default: $message = ‘Unknown’; } $age < 18 }; */ => ‘Minor’,
PHP – The Ecosystem Professionalism & Tooling
The PSR Standards (PHP-FIG) PHP-FIG: Agree on Interfaces, not Implementations PSRs (PHP Standard Recommendations).
PSR: Dependency Inversion The Good Old Days // Tightly coupled to “Monolog” // Decoupled. Relies on PSR-3 Standard. use Monolog\Logger; use Psr\Log\LoggerInterface; class UserManager class UserManager { { public function __construct( public function __construct( private Logger $logger // <—- HARD DEPENDENCY private LoggerInterface $logger // <—- INTERFACE ONLY ) {} ) {} public function create() { public function create() { // Tied to Monolog’s specific method names // Guaranteed to exist on ANY PSR-3 compliant logger $this->logger->addInfo(‘User created’); $this->logger->info(‘User created’); } } } } True Dependency Inversion at a community scale
Dependency Management: Composer Java - Maven pom.xml <dependency> PHP - Composer composer.json { <groupId>org.slf4j</groupId> “require”: { <artifactId>slf4j-api</artifactId> “monolog/monolog”: “^3.0” <version>2.0.7</version> } </dependency> }
<!— And then refresh your IDE… —>// Terminal command: composer require monolog/monolog Dependency Management: Solved since 2012
PHP – Performance & Architecture
JIT & Performance Sources: Kinsta, Tideways, ICDSoft, PHP manual, various public benchmarks
The Frameworks: Symfony & Laravel Symfony is Spring Boot. Laravel is Rails. We aren’t scripting anymore; we are architecting
Symfony: The Enterprise Standard The Good Old DaysJava - Spring @Service PHP - Symfony use Symfony\Component\DependencyInjection\Attribute\Autowire; public class ReportGenerator { private final Mailer mailer; class ReportGenerator { @Autowired // Constructor Injection is the standard. public ReportGenerator(Mailer mailer) { // The container automatically injects the implementation o this.mailer = mailer; public function __construct( } private MailerInterface $mailer } ) {} } If you know Spring, you know Symfony ■ Dependency Injection Container ■ Decoupling ■ Stability
Laravel: The “Developer Happiness” Framework If you love the speed of Express or Rails, Laravel is that, but typed. It’s not just a framework; it’s a platform. ● ● ● ● ● Eloquent ORM: (ActiveRecord) Incredibly expressive Queue Workers: (Laravel Horizon) Redis-backed queues out of the box. Real-time: (Laravel Reverb) WebSockets without Node.js. Serverless: (Laravel Vapor) AWS Lambda deployment helper. … PHP - Laravel <?php // Find all active users and email them… in 3 lines. User::query() ->where(‘active’, true) ->get() ->each(fn(User $user) => Mail::to($user)->send(new WelcomeEmail()));
New Runtimes: Async & Long-Running Processes PHP used to die after every request. Not anymore.
The Game Changer: FrankenPHP A modern application server written in Go, built on top of Caddy, that embeds the PHP interpreter and most popular extensions ● ● ● ● Worker Mode Early Hints (HTTP 103) Real-Time Mercure hub Static Binary
PHP – Myths, Misconceptions, and Reality Checks
“PHP is slow” Myth busted!
“PHP has no types” Myth busted!
“PHP apps don’t scale” Myth busted!
“PHP code is spaghetti” Modern PHP uses Strict MVC or Clean Architecture If you write 1000 lines of code in a single file in Java, it’s spaghetti. If you do it in PHP, it’s spaghetti. Bad code is language-agnostic Myth busted!
“PHP is insecure” Myth busted!
“PHP developers are isolated” Myth busted!
WordPress Question: “Why Does It Still Look Old? Chose backward compatibility Chose modernization Legacy Deployment ≠ Language Limitation
PHP – The Polyglot Conclusion
The “Right Tool” Matrix (PHP vs The World) PHP don’t want to be Java. It wants to be the web.
Reality Check: When PHP Isn’t the Answer Honest Assessment: Choose the Right Tool
PHP isn’t the language you remember It’s a pragmatic, high-performance tool that respects your time. Don’t hate it, add it to your toolbox
PHP – Live Demo / Code Walkthrough
J’avais une belle démo prêt pour ce soir Mais je vois ça en arrivant chez Zenika…
Alors, nouvelle démo! https://github.com/LostInBrittany/clash-of-pop-culture
Clash of Pop Culture https://clash-of-pop-culture.cleverapps.io/index.html
That’s all, folks! Thank you all!
PHP powers over half of the web, yet many developers still remember it as spaghetti-code of the ’90s. In this session, you’ll see how PHP has evolved into a modern platform featuring JIT compilation, strong typing, attributes, and a thriving ecosystem of packages & frameworks. We’ll dispel common myths – no, PHP does know objects – and explore how today’s best practices bring PHP in line with, and often ahead of, other mainstream languages.
With concise examples and case studies, you’ll learn how modern PHP leverages tools like Composer, PSR standards, static analysis, and Dependency Injection to build maintainable, scalable applications. Whether you’re coming from Java, JavaScript, Go or Rust, you’ll discover how PHP’s strengths – its simplicity, performance gains in PHP 8+, and vibrant community – can complement your existing toolset.
What You’ll Take Away:
Join us to bridge the gap between the PHP community and the wider development world—ignite your curiosity and see why PHP deserves a spot in every polyglot’s toolbox.