Code review last week. A "fullstack" dev shows me his Laravel API. Clean on the surface β well-organized controllers, Eloquent migrations, Form Request validation. I ask him why he's using $request->all()
instead of $request->validated()
in his controller. Blank stare. He didn't know that all()
returns everything in the payload, including unvalidated fields. Claude generated the controller, he copied it, it worked. Six months of Laravel and a dormant mass assignment vulnerability in every endpoint.
PHP has a problem Python doesn't: its reputation. "Dead language," "spaghetti code," "only good for WordPress." In 2026, that's been wrong for a long time β PHP 8.3 is a typed, modern language with enums, fibers, readonly properties, and performance PHP 5 could never have imagined. But this reputation means many beginners skip the basics and jump straight to a framework, guided by AI. The result: Laravel operators, not PHP developers. This article explains how to use Claude to become the latter without falling into the former.
Before talking pedagogy, let's settle this. "Why learn PHP when everyone's moving to Go / Rust / TypeScript?" Three facts, not opinions.
1. PHP powers 75% of the web. WordPress, Shopify (backend), Symfony, Laravel β millions of projects in production. PHP gigs aren't scarce, and they pay well because good PHP devs are rare. Everyone wants to code in Rust; few can debug a Doctrine\ORM\Query\QueryException
at 2am.
2. PHP 8.3 has nothing in common with PHP 5. Type declarations, enums, readonly
, match
, named arguments, fibers, JIT β it's a different language. PHP criticism dates back to 2012. In 2026, the same patterns people applauded in TypeScript exist natively in PHP.
3. AI doesn't debug your business context. Claude generates a UserController
in 15 seconds. But when the client's LDAP auth breaks in staging and sessions no longer propagate between PHP-FPM workers, you're the one who needs to understand the HTTP request lifecycle. If you never understood $_SESSION
, you're stuck.
PHP has its own learning anti-patterns. AI amplifies them if you don't know they exist.
You ask "how to create a contact form in PHP," Claude gives you WordPress code with wp_mail()
and add_action
hooks. You copy, it works in WordPress. You learned nothing about PHP. You don't know that native mail()
exists, that it's terrible in production (no SMTP auth, no TLS), and that PHPMailer solves the real problem. WordPress is not PHP β it's a framework with its own dialect.
Laravel in week 1. It's the most common trap. The framework abstracts everything: routing, requests, responses, sessions, middleware. If you don't understand what $_GET
, $_POST
, $_SERVER['REQUEST_METHOD']
are, you don't understand what the framework does for you. And the day it does something unexpected, you're lost.
AI was trained on millions of lines of old PHP. When you ask "how to hash a password in PHP," there's a non-trivial chance Claude suggests md5()
or sha1()
in certain contexts. The right reflex: password_hash()
with PASSWORD_BCRYPT
or PASSWORD_ARGON2ID
. Always check the PHP version of suggested patterns.
// β PHP 5 β NEVER do this
$hash = md5($password);
$hash = sha1($password . $salt);
// β
PHP 8 β the only right way
$hash = password_hash($password, PASSWORD_ARGON2ID);
$valid = password_verify($input, $hash);
PHP is optionally typed. AI often generates code without types because it's "simpler." Result: you learn a lenient PHP that accepts anything and crashes in production with TypeError
s you don't understand. Enable declare(strict_types=1)
from your very first file. No exceptions.
// β No typing β everything passes, nothing is safe
function calculatePrice($quantity, $unitPrice) {
return $quantity * $unitPrice;
}
calculatePrice("3", "19.99"); // Works... by accident
// β
With strict_types β errors are explicit
declare(strict_types=1);
function calculatePrice(int $quantity, float $unitPrice): float {
return $quantity * $unitPrice;
}
calculatePrice("3", "19.99"); // Immediate TypeError
Claude sometimes suggests mysqli
, sometimes PDO
, depending on context. The real advice: use PDO
, period. It's the standard abstraction layer, it handles prepared statements cleanly, and it works with any database engine. mysqli
is MySQL-specific and brings nothing extra in 99% of cases.
// β mysqli β coupled to MySQL, confusing API
$conn = new mysqli("localhost", "user", "pass", "db");
$stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
// β
PDO β portable, clear, chainable
$pdo = new PDO('mysql:host=localhost;dbname=db', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
The same cycle as for any language β I covered it in the Python guide β but with concrete PHP examples.
Problem: filter an array of users to keep only adults. Your first version, without AI:
// My version β filter adult users
function getAdults(array $users): array {
$adults = [];
foreach ($users as $user) {
if ($user['age'] >= 18) {
$adults[] = $user;
}
}
return $adults;
}
Here's my PHP code to filter an array of users. Don't give me a corrected version. Tell me what could be improved and why, and let me rewrite it myself.
Claude will point out: array_filter
exists for this. The return type is array
but could be more precise with a @return array<int, array{name: string, age: int}>
docblock. And what if $users
is empty? Does your function handle that? (Yes, it does β but did you think about it?)
// My improved version after review
/** @param array<array{name: string, age: int}> $users */
function getAdults(array $users): array {
return array_filter($users, fn(array $user): bool => $user['age'] >= 18);
}
You discovered array_filter
and arrow functions. You understand that fn() =>
is syntactic sugar for single-expression closures. You know why it's more concise, and you know that array_filter
preserves keys (which can be a trap if you then iterate with numeric indices).
Here's my improved version. What am I not seeing? Is there a more robust pattern for this kind of filtering?
Claude will probably mention the match
expression for multi-criteria filtering, typed collections with classes, and the difference between array_filter
and array_values(array_filter(...))
to reindex the array. Three levels of understanding, and you wrote the first two yourself.
The difference between learning and delegating comes down to how you phrase the prompt. Here are patterns adapted to PHP.
β Passive prompt
β Pedagogical prompt
"Write a PHP CRUD for managing products"
"I wrote this POST route that inserts a product with PDO. Do my prepared statements really protect against SQL injection?"
"Fix this error"
"This code raises TypeError: Argument #1 must be of type string, null given
. Explain where the null
comes from without giving me the fix."
"Make me a Laravel auth"
"I implemented session auth with password_verify()
and a CSRF token. What could an attacker exploit?"
"What are namespaces?"
"I understand that use App\Service\UserService
imports a class. But what's the difference between use
in a namespace and use
in a closure?"
"Write a PHP class"
"I have this User
class with 8 constructor properties. Would PHP 8 promoted properties simplify it? Show me the difference."
The common pattern: show what you did or understood, then ask a specific question. Not "explain namespaces" but "I understood this much, what am I missing?". Claude adapts its response to your actual level instead of serving a long-form version of the PHP.net docs.
Not a rigid program β a path that worked for people I've mentored. Adapted to PHP specifics. AI comes in after the effort, never before.
Variables, types, conditions, loops, functions, arrays. Without Claude. Use the official PHP documentation (php.net/manual) and do the exercises by hand. Start every file with declare(strict_types=1)
. The goal: make foreach ($items as $key => $value)
a reflex, not a formula you ask for every time.
Exercise: write a script that reads a CSV file and calculates statistics (average, min, max) on a numeric column. No external library. Just fopen()
, fgetcsv()
, an associative array. When done, ask Claude for a review.
Classes, interfaces, traits, inheritance, composition, namespaces, PSR-4 auto. This is where PHP stands out: OOP is at the core of the modern language, and namespaces are the mechanism that structures any non-trivial project. Claude moves to review mode.
Exercise: a CLI task manager β add, delete, mark as "done," JSON export. Each entity in its own class. Use readonly
, constructor promoted properties, and enums for statuses. Write first, review with Claude after. It'll probably mention interfaces β that's the right moment.
// What you should write yourself in week 3
declare(strict_types=1);
enum TaskStatus: string {
case Todo = 'todo';
case Done = 'done';
case Cancelled = 'cancelled';
}
final class Task {
public function __construct(
public readonly string $id,
public readonly string $title,
public TaskStatus $status = TaskStatus::Todo,
public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
) {}
}
Composer, auto, dependencies. PDO, prepared statements, manual migrations. Writing a minimal REST API β without a framework. Claude becomes a patterns teacher: "why separate routing from business logic?", "why not put SQL in the controller?".
Exercise: a REST API that manages blog posts (CRUD). Manual routing ($_SERVER['REQUEST_URI']
match
), PDO for the database, JSON in/out. HTTP error handling (404, 422, 500). Ask Claude: "is my routing secure?" β the answer will surprise you.
A project that combines everything. Two calibrated options:
Claude switches to pair programming mode: you write a feature, you discuss architecture with it, it challenges your choices. "Why put validation in the controller instead of a dedicated service?" β if you can't answer, you haven't consciously decided yet.
Two tools, two uses. Both cost $20/month (Claude Pro) or $20/month (Claude Code with the Max plan) β but the learning experience is very different.
Web interface, conversation. The right tool for:
abstract class
and interface
in PHP with a concrete case"===
and not ==
in PHP?"Advantage: accessible, no terminal needed, conversation history. Limitation: you copy-paste code, it can't see your composer.json
.
In the terminal, directly in your project. The right tool for:
composer.json
, your PHPUnit testsphp -S
, sees the error, explains the stack traceAdvantage: full project context, real execution. Limitation: more technical to set up, requires a terminal.
Weeks 1-4: Claude Pro for concepts and review. You don't have a complex project yet, the web interface is enough. Weeks 5-8: Claude Code for the project. You need file context, tests, live debugging. Claude Pro remains useful for conceptual questions in parallel.
Claude writes correct PHP. But there's a difference between correct code and professional code. These conventions are learned by reading good open source code (Symfony, Laravel, PHPStan) and getting corrected.
camelCase
for methods, PascalCase
for classes. Use PHP CS Fixer from day one.require
require_once 'lib/User.php'
, you're writing 2008 PHP. PSR-4 + composer dump-autoload
, period.new PDO(...)
in every method. That's a major anti-pattern.declare(strict_types=1)
everywhere"42abc"
to 42
. With it, it throws a TypeError
. You want the second behavior.array
holding structured data should be a DTO (Data Transfer Object) with typed properties. AI loves associative arrays. Resist.How to know if you're actually learning and not just "doing things with Claude"? Three concrete tests.
Test 1 β The whiteboard. Take a problem you solved with Claude last week. Rewrite the class from memory, no IDE, no AI. If you're stuck on promoted property constructor syntax, you didn't learn β you delegated.
Test 2 β The explanation. Explain your code to someone. Every method. If you say "this part with the match
I'm not sure why it's like that," that's a gap in your understanding.
Test 3 β Debugging without a net. Introduce a null
somewhere in your call chain. Close Claude. Find the TypeError
with var_dump()
, error_log()
, or just by reading the stack trace. The time it takes measures your real autonomy.
PHP isn't dead, it just grew faster than its reputation. The 2026 language β typed, with enums, readonly, match, fibers β has little in common with the PHP from Reddit jokes. But this modernity is useless if you discover it through copy-paste from Claude. The discipline stays the same: write first, ask second, rewrite yourself.
The developer who finishes this 8-week path knows how to read a stack trace, choose between an array and a DTO, write PSR-12 code others can read, and use Claude as a multiplier β not a crutch. That's exactly the profile the market is missing: someone who can code PHP and who knows how to get the most out of AI.
The same workflow applies to any language. If you're coming from Go, I wrote the equivalent guide for Go. For Python, it's over here.