13 CLAUDE.md Rules That Make AI Write Modern PHP (Not PHP 5 Resurrected)

Dev.to / 5/5/2026

💬 OpinionDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

Key Points

  • The article argues that many AI coding assistants generate “PHP 5–era” code, often repeating insecure or outdated practices learned from older Stack Overflow answers.
  • By placing a CLAUDE.md file at the repository root, the author claims the AI can read project-specific rules before tasks, improving consistency and safety.
  • One highlighted rule is to add `declare(strict_types=1);` as the first statement in every PHP file to prevent silent type coercion bugs from surfacing only in production.
  • Another key rule is to avoid raw/interpolated SQL and instead use PDO with prepared statements to reduce the risk of SQL injection vulnerabilities.
  • The full CLAUDE.md approach includes 13 rules, with the first shown being focused on eliminating common “worst patterns” in generated PHP code.

If you've ever asked Claude Code, Cursor, or Copilot to "add a function" to a PHP project, you've seen the output: untyped properties, mysql_query("SELECT * FROM users WHERE email = '$email'"), md5($password . SALT) for hashing, global $db everywhere, extract($_POST) because it's "convenient", and @file_get_contents($url) to silence the error you needed to see.

It's PHP 5 with a <?php header. The training data is half a decade of Stack Overflow answers from before strict types existed, before password_hash() was a function, before PSR-4 was a thing, and before anyone considered eval($_POST['code']) a problem.

Drop a CLAUDE.md at the root of your repo and the AI reads it before every task. Here are the five rules that fix the worst patterns — the full pack has 13.

Rule 1: declare(strict_types=1); at the top of every PHP file

PHP's default coercion is the source of an enormous class of "works in dev, breaks in prod" bugs. Without strict types, this passes:

function age(int $years): int {
    return $years + 1;
}

age("7 dwarves"); // returns 8 — coerced silently

With declare(strict_types=1); as the first statement of the file, the same call throws TypeError and you find the bug at the call site instead of three layers down.

<?php
declare(strict_types=1);

It's per-file, has to be the first statement after <?php, and CI greps for files missing it. AI omits it by default because most snippets in its training data predate PHP 7's strict mode. The rule kills the reflex.

Rule 2: No raw SQL — PDO with prepared statements, always

Every PHP CVE-of-the-year list opens with SQL injection from interpolated strings. AI will write this without flinching:

$pdo->query("SELECT * FROM users WHERE email = '$email'");
$pdo->query("DELETE FROM orders WHERE status = '" . $status . "'");

Both are remote-code-execution by another name. The rule is: no string-interpolated SQL, ever. Use named parameters:

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Configure PDO with ATTR_ERRMODE => ERRMODE_EXCEPTION, ATTR_DEFAULT_FETCH_MODE => FETCH_ASSOC, and crucially ATTR_EMULATE_PREPARES => false so prepared statements actually go to the server instead of being faked client-side. Identifiers (table and column names) cannot be parameterised — allowlist them in code, never accept them from input.

Rule 3: password_hash() for passwords, random_bytes() for tokens — never md5, sha1, or mt_rand

md5($password . SALT) is in every legacy PHP codebase, and AI keeps writing it because the training data is full of it.

// Banned
$hash = md5($password . SALT);
$token = md5(uniqid(mt_rand(), true));

password_hash() uses bcrypt (or Argon2, depending on PASSWORD_DEFAULT) with proper salting and a tunable work factor. random_bytes() is cryptographically secure. The mt_* family is predictable and unsafe for tokens, session IDs, password resets, or anything else security-sensitive.

$hash = password_hash($password, PASSWORD_DEFAULT);

if (!password_verify($input, $hash)) {
    throw new InvalidCredentialsException();
}

if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    // store new hash on next successful login
}

$token = bin2hex(random_bytes(32));

PASSWORD_DEFAULT upgrades automatically as PHP's recommended algorithm changes. password_needs_rehash() lets you migrate old hashes transparently on the next successful login. Never roll your own KDF. Never reach for crypt() directly.

Rule 4: No eval(), no extract(), no variable variables, no @-suppression

These four are remote-code-execution waiting to happen, and there is no legitimate use of any of them in modern PHP:

eval($code);                      // RCE
extract($_REQUEST);               // mass-assignment of locals — sets $admin = true if ?admin=1
$$varName = $value;               // variable variables — defeats static analysis
@file_get_contents($url);         // hides errors you needed to see

extract($_REQUEST) is particularly dangerous because it silently creates $admin = true in the local scope if the request includes ?admin=1. AI loves this pattern because it makes templates "convenient". It also makes them exploitable.

The replacements are obvious once the bans are in place:

// Dynamic dispatch — match or a lookup, never eval
$handler = match ($type) {
    'csv'  => new CsvExporter(),
    'json' => new JsonExporter(),
    default => throw new InvalidArgumentException("unknown type: $type"),
};

// Parsing config — json_decode, never eval
$config = json_decode(
    file_get_contents($path),
    associative: true,
    flags: JSON_THROW_ON_ERROR,
);

CI greps for \beval\s*\(, \bextract\s*\(, \$\$[a-zA-Z_], and the @ operator on function calls. Build fails on any hit. No exceptions for "templates", "the admin panel", or "just this once".

Rule 5: Constructor injection, not global $db

global $db; global $logger; global $mailer; at the top of every method makes code untestable, hides collaborators, and turns every function into a leaky abstraction. AI reaches for it because it's shorter.

// Before
class OrderService {
    public function place(array $cart): int {
        global $db, $logger, $mailer;
        $db->insert(...);
        $logger->info(...);
        $mailer->send(...);
        return $db->lastInsertId();
    }
}

The fix is constructor injection with a real DI container (PHP-DI, Symfony DI, Laravel's container). Dependencies are explicit, the constructor is the contract, and tests pass mocks instead of monkey-patching globals.

final class OrderService
{
    public function __construct(
        private readonly OrderRepository $orders,
        private readonly LoggerInterface $logger,
        private readonly MailerInterface $mailer,
    ) {}

    public function place(Cart $cart): OrderId
    {
        $id = $this->orders->save($cart->toOrder());
        $this->logger->info('order.placed', ['id' => (string) $id]);
        $this->mailer->send(new OrderConfirmation($id));
        return $id;
    }
}

readonly on the properties enforces immutability — the compiler catches mutations instead of trusting reviewers. PSR-11's ContainerInterface is fine if you genuinely need the container itself, but inject specific dependencies wherever possible.

The other eight rules

The full Gist also covers:

  • Rule 2: Type everything — properties, parameters, returns, including void and never
  • Rule 5: Escape on output, not on input — htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8')
  • Rule 6: PSR-4 autoloading via Composer — no require chains
  • Rule 7: PSR-12 enforced by php-cs-fixer + phpstan at level 8
  • Rule 8: Exceptions, not return codes — typed, hierarchical, $previous always passed
  • Rule 10: match over switch, named arguments over positional booleans, enums over magic strings
  • Rule 12: PHPUnit (or Pest) with real-DB feature tests, no DB mocks
  • Rule 13: Configuration via env vars, secrets out of git, .env.example checked in

Wrapping up

These rules don't replace the PHP manual or the PSR specs — they encode the failure modes AI repeats most often when generating PHP. Strict types, prepared statements, password_hash over md5, contextual escaping, PSR-4 autoloading, exceptions instead of return codes, constructor injection, match and enums, and a hard ban on eval/extract aren't style preferences. They're the line between a PHP app that ships and one that ends up in a CVE feed.

Drop the file at the root of your repo. The next AI prompt produces PHP your future self won't have to rewrite at 3am.

Free sample (this article + Gist): CLAUDE.md for PHP on GitHub Gist

Get the full CLAUDE.md Rules Pack — 35+ stacks, ready-to-drop files for every project: