Rediscovering PHP: Modern Practices Beyond Legacy

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

Slide 1

Slide 1

Rediscovering PHP Modern Practices Beyond Legacy Horacio Gonzalez 2025-12-02

Slide 2

Slide 2

Who are we? Introducing myself and introducing Clever Cloud

Slide 3

Slide 3

Horacio Gonzalez @LostInBrittany Spaniard Lost in Brittany Old(ish) Developer

Slide 4

Slide 4

Clever Cloud From Code to Product

Slide 5

Slide 5

You’re going to talk about PHP ?!?! The elephpant IS in the room

Slide 6

Slide 6

So yeah, I know both PHP and its reputation… And in 2004 is was quite accurate, PHP felt like the Wild West

Slide 7

Slide 7

If you think PHP is inconsistent, insecure, and strictly procedural… You were right 15 years ago !

Slide 8

Slide 8

I’ve been a Java developer since ‘97 And I have done tons of frontend in HTML/CSS/JS

Slide 9

Slide 9

But I’ve also written and debugged lots of PHP I even made a living of it!

Slide 10

Slide 10

For years I kept away from PHP I reckon I was a bit snobbish about it…

Slide 11

Slide 11

Until I joined Clever Cloud and rediscovered it And, mate, it was so much better!

Slide 12

Slide 12

PHP is not like PHP 4 anymore PHP powers 75%+ of the web… and 90% of that usage looks nothing like WordPress 3.0

Slide 13

Slide 13

PHP – The Language Syntax for the Java Mind

Slide 14

Slide 14

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

Slide 15

Slide 15

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

Slide 16

Slide 16

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.

Slide 17

Slide 17

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, ) {} }

Slide 18

Slide 18

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

  • @IsGranted(“ROLE_ADMIN”) {

// Native syntax. “Route” is a real class found via “use”

  • This is technically a comment. #[Route(‘/api/users’, methods: [‘GET’])]
  • If I typo “@Rout”, nothing crashes… #[IsGranted(‘ROLE_ADMIN’)]
  • until the app runs and the route is missing. public function list(): Response */ { public function list() // … { } // … } } } // How to make your own Attribute? It’s just a class! #[Attribute] class MyCustomMetadata { public function __construct(public string $info) {} }

Slide 19

Slide 19

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”

Slide 20

Slide 20

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’,

Slide 21

Slide 21

PHP – The Ecosystem Professionalism & Tooling

Slide 22

Slide 22

The PSR Standards (PHP-FIG) PHP-FIG: Agree on Interfaces, not Implementations PSRs (PHP Standard Recommendations).

Slide 23

Slide 23

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

Slide 24

Slide 24

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

Slide 25

Slide 25

PHPStan: The Compiler Substitute $ phpstan PHP class UserManager $ vendor/bin/phpstan analyse src —level=9 { /**

  • @param array<User> $users <— PHPStan reads this “Generic” definition 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% */ public function activateAll(array $users): void { ——— ————————————————————————————————Line foreach ($users as $user) { // Runtime: If $user is NOT a User object, this crashes HERE. $user->activate(); } } } // The Bug: $managers = [new User(), new User(), “oops, a string”]; (new UserManager())->activateAll($managers); src/UserManager.php ——— ————————————————————————————————18 Parameter #1 $users of method UserManager::activateAll() expects array<User>, array{User, User, string} given. Type string is not subtype of User. ——— ————————————————————————————————[ERROR] Found 1 error

Slide 26

Slide 26

PHP – Performance & Architecture

Slide 27

Slide 27

JIT & Performance Sources: Kinsta, Tideways, ICDSoft, PHP manual, various public benchmarks

Slide 28

Slide 28

The Frameworks: Symfony & Laravel Symfony is Spring Boot. Laravel is Rails. We aren’t scripting anymore; we are architecting

Slide 29

Slide 29

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

Slide 30

Slide 30

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()));

Slide 31

Slide 31

New Runtimes: Async & Long-Running Processes PHP used to die after every request. Not anymore.

Slide 32

Slide 32

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

Slide 33

Slide 33

PHP – Myths, Misconceptions, and Reality Checks

Slide 34

Slide 34

“PHP is slow” Myth busted!

Slide 35

Slide 35

“PHP has no types” Myth busted!

Slide 36

Slide 36

“PHP apps don’t scale” Myth busted!

Slide 37

Slide 37

“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!

Slide 38

Slide 38

“PHP is insecure” Myth busted!

Slide 39

Slide 39

“PHP developers are isolated” Myth busted!

Slide 40

Slide 40

WordPress Question: “Why Does It Still Look Old? Chose backward compatibility Chose modernization Legacy Deployment ≠ Language Limitation

Slide 41

Slide 41

PHP – The Polyglot Conclusion

Slide 42

Slide 42

The “Right Tool” Matrix (PHP vs The World) PHP don’t want to be Java. It wants to be the web.

Slide 43

Slide 43

Reality Check: When PHP Isn’t the Answer Honest Assessment: Choose the Right Tool

Slide 44

Slide 44

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

Slide 45

Slide 45

PHP – Live Demo / Code Walkthrough

Slide 46

Slide 46

J’avais une belle démo prêt pour ce soir Mais je vois ça en arrivant chez Zenika…

Slide 47

Slide 47

Alors, nouvelle démo! https://github.com/LostInBrittany/clash-of-pop-culture

Slide 48

Slide 48

Clash of Pop Culture https://clash-of-pop-culture.cleverapps.io/index.html

Slide 49

Slide 49

That’s all, folks! Thank you all!