# Learn PHP with Claude in 2026: build real skills, not AI dependency

> Source: <https://dev.to/ohugonnot/learn-php-with-claude-in-2026-build-real-skills-not-ai-dependency-1c81>
> Published: 2026-07-01 09:00:02+00:00

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
// ❌ 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.

``` php
// ❌ 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](https://www.web-developpeur.com/en/blog/apprendre-python-avec-ia-2026) — 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](https://www.php.net/manual/en/)) 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 autoloading. 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, autoloading, 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 tests`php -S`

, sees the error, explains the stack trace**Advantage**: 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](https://www.web-developpeur.com/en/blog/apprendre-go-guide-developpeur). For Python, it's [over here](https://www.web-developpeur.com/en/blog/apprendre-python-avec-ia-2026).
