原始内容
name: php
slug: php
version: 1.0.2
changelog: "Full coverage pass: deeper guides, situation-named files, and per-user configuration"
description: >-
Writes, debugs, and reviews PHP: type juggling, arrays, OOP, Composer, PDO, sessions, PHP-FPM, OPcache, PHP 8 features.
Use when PHP returns a blank white page or a 500 with no message, when "headers already sent", "Allowed memory size
exhausted", "Maximum execution time exceeded", "Class not found" after a Composer install, or an undefined array key
warning appears; when == compares wrong, strpos or preg_match returns a falsy match, accents turn into mojibake,
DateTime shifts by a day or an hour, json_encode returns false, PDO refuses to bind a parameter or emulates
prepares, sessions serialize parallel AJAX requests, nginx answers 502 or 504 in front of PHP-FPM, OPcache keeps
serving old code, or a version upgrade breaks on deprecations. Also for escaping output, hashing passwords, sizing
FPM workers, writing PHPUnit tests, and clearing PHPStan errors. Not for framework internals — laravel covers
Eloquent, queues, and Laravel auth.
homepage: https://clawic.com/skills/php
metadata:
clawdbot:
emoji: 🐘
requires:
bins:
- php
os:
- linux
- darwin
- win32
displayName: PHP
configPaths:
- ~/Clawic/data/php/
User preferences live in ~/Clawic/data/php/config.yaml (see Configuration); nothing else is stored on the user's machine. If you have data at an old location (~/php/ or ~/clawic/php/), move it to ~/Clawic/data/php/.
When To Use
- Writing or reviewing PHP — read Core Rules and Output Gates before emitting code
- Any error whose cause is not obvious in ten seconds: white page, silent 500, wrong value with no exception, a hang
- Runtime and deployment work: php.ini precedence, FPM pool sizing, OPcache, CLI workers, cron
- Dependency and upgrade work: Composer constraints, autoloading, deprecations, moving to a newer 8.x
- Data-boundary work: PDO queries, JSON payloads, uploads, sessions, dates, encodings
- Hardening: escaping per context, password hashing, deserialization, path containment, upload validation
- Not for framework internals —
laravelowns Eloquent, queues, Blade, and Laravel auth
Quick Reference
| Situation | Play |
|---|---|
== compares wrong, a numeric string misbehaves, floats do not add up |
=== everywhere; "0e12" == "0e34" is true → hash_equals → types.md |
| Keys vanish, order is wrong, a filtered list encodes as a JSON object | array_filter keeps keys; array_values() before encoding → arrays.md |
Accents break, strlen disagrees with what you see, a regex silently fails |
mb_* for text, /u on every pattern, check preg_last_error() → strings.md |
Class design: traits, static::, readonly, enums, magic methods, clone |
self:: binds at compile time, static:: at call time → oop.md |
| Exceptions, error handlers, warnings you cannot see, fatal errors | Catch \Throwable; fatals need register_shutdown_function → errors.md |
Injection, XSS, uploads, unserialize, passwords, tokens |
Bind every value, escape per output context, random_bytes for secrets → security.md |
| Which PHP 8 feature to use, and how it bites | match is strict and exhaustive; named args make parameter names API → modern.md |
| Upgrading PHP, a deprecation wall, "which version can I still target" | Run the OLD version with deprecations as errors first → versions.md |
Class not found, lock conflicts, ^ vs ~, slow or huge installs |
Commit the lock for apps, never for libraries → composer.md |
| White page, silent 500, "it worked yesterday", a hang with no output | Symptom → cause chains, plus the Xdebug 3 setup that actually works → debugging.md |
| Slow requests, high memory, "should we turn on JIT" | OPcache first, profile second; JIT does almost nothing for I/O-bound web → performance.md |
| A setting you changed has no effect; CLI and web disagree | php --ini per SAPI; .user.ini is cached for 300s → php-ini.md |
| 502, 504, "server reached pm.max_children", workers eating RAM | Size from measured RSS; slowlog names the hung function → fpm.md |
| Console scripts, daemons, cron, signals, exit codes | max_execution_time is 0 on CLI; recycle workers on a job count → cli.md |
| PHPUnit setup, flaky or falsely-passing tests, coverage, mocks | assertSame not assertEquals; static state leaks between tests → testing.md |
| PHPStan or Psalm errors, baselines, coding standard, automated refactors | Baseline the legacy, ratchet one level per merge → static-analysis.md |
| PDO connections, binding, transactions, "MySQL server has gone away" | charset=utf8mb4 in the DSN, EMULATE_PREPARES => false → database.md |
| Request/response handling, JSON bodies, uploads, outbound HTTP calls | JSON bodies live in php://input, not $_POST → http.md |
| Parallel requests from one user run one at a time; logins do not stick | session_start() holds an exclusive lock — session_write_close() early → sessions.md |
| Dates off by a day or an hour, DST, parsing, storage | DateTimeImmutable + IANA zones; createFromFormat inherits today → datetime.md |
json_decode returns null, big IDs lose digits, json_encode returns false |
Always pass JSON_THROW_ON_ERROR → json.md |
| Reading or writing files, CSV, streams, temp files, permissions | Stream instead of slurping; write temp then rename() → files.md |
| Parallel work, queues, locks across processes, long-running runtimes | Share-nothing per request; proc_open deadlocks if you read one pipe → concurrency.md |
| Anything else | Core Rules below, then reproduce with php -d error_reporting=E_ALL -d display_errors=1 -r '<five suspect lines>' and add one thing back at a time |
Each file above is one sub-job and is self-contained: read SKILL.md by default, open exactly one guide when the situation matches.
Core Rules
declare(strict_types=1);on line 1 of every file. It is per-file and governs the CALLS made in that file, not the functions declared there — so an untyped legacy caller still passes"7"into yourintparameter. Return values are checked against the mode of the file where the function is DECLARED (types.md).===and!==by default;==only where both operand types are proven. Worked case:"0e12" == "0e34"istruebecause both are numeric strings equal to 0 — a hash compared with==accepts the wrong password. Compare digests withhash_equals($known, $given), which is also constant-time.- Check anything that can return
falsewith!== false, never with truthiness.strpos($haystack, $needle)returns0for a match at offset 0, andif (!strpos(...))reads that correct answer as a miss; the same shape breakspreg_match(0= no match,false= engine error) andfile_get_contents(""is a valid file). - Every SQL value is a bound parameter; every identifier comes from an allowlist you wrote. Placeholders cannot stand in for table or column names, and with the MySQL driver's default emulation the "prepared" statement is interpolated client-side — set
PDO::ATTR_EMULATE_PREPARES => false(database.md). - Escape at output, per context, never at input.
htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')covers HTML body and quoted attributes only; a value going into JavaScript needsjson_encodewith the HEX flags, one going into a URL needsrawurlencode, one going into a shell needsescapeshellarg(security.md). - Text is characters, not bytes:
strlen,substr,str_pad,strrev, anducfirstcount bytes and will cut a UTF-8 sequence in half. Usemb_strlen/mb_substr/mb_str_pad(php >=8.3) for anything a human reads; keep the byte functions for binary data, where they are the correct tool. DateTimeImmutableeverywhere.DateTime::modify()mutates the object that every reference shares, so a helper that "returns tomorrow" also moves the caller's date. The same call on an immutable returns a new instance and the bug cannot happen (datetime.md).- Every call that leaves the process gets an explicit timeout:
CURLOPT_TIMEOUTplusCURLOPT_CONNECTTIMEOUT, a stream contexttimeoutforfopen,PDO::ATTR_TIMEOUTfor connections.max_execution_timewill not save you — on Unix it does not count time spent inside system calls, so a hung socket read is only killed by FPM'srequest_terminate_timeout(fpm.md). - Stream instead of slurping.
file_get_contentson a 400 MB export needs 400 MB plus copies inside the default 128Mmemory_limit;fgetcsvin a loop, or a generator, holds one row. The same rule retiresfetchAll()on large result sets (files.md,performance.md).
Version Floors
The floors that shape everyday code. Individual guides carry more, inline in this same php >=X.Y form next to the instruction each one gates. Support windows and the upgrade procedure: versions.md.
| Feature | Needs |
|---|---|
Typed properties, arrow functions fn() =>, spread in calls, ??=, covariant return types |
php >=7.4 |
match, named arguments, constructor promotion, ?->, union types, attributes, str_contains/str_starts_with/str_ends_with, static return type, throw as an expression, saner string-to-number comparison, PDO defaulting to exception error mode |
php >=8.0 |
Enums, readonly properties, fibers, never, first-class callables f(...), new in initializers, array_is_list(), string-keyed array unpacking, overridable interface constants, htmlspecialchars escaping single quotes by default |
php >=8.1 |
readonly classes, DNF types, null/false/true as standalone types, locale-independent strtolower/strtoupper, dynamic properties deprecated, ${var} interpolation deprecated |
php >=8.2 |
Typed class constants, #[\Override], json_validate(), mb_str_pad(), readonly reinitialization inside __clone, stack-overflow detection via zend.max_allowed_stack_size |
php >=8.3 |
Property hooks, asymmetric visibility, new Foo()->m() without parentheses, lazy objects, array_find/array_any/array_all, mb_trim, implicit nullable parameters (Foo $x = null) deprecated |
php >=8.4 |
Pipe operator |>, #[\NoDiscard] |
php >=8.5 |
Output Gates
Before delivering PHP code, check:
declare(strict_types=1);present, and every parameter, property, and return typed as narrowly as the value allows- No
==,!=, orswitchon values whose types are not both proven;in_arrayandarray_searchcarrytrueas the strict flag - Every SQL value bound, every identifier allowlisted,
EMULATE_PREPARESoff - Every value reaching HTML, an attribute, JavaScript, a URL, or a shell escaped with that context's function, at the point of output
- Nothing user-controlled reaches
unserialize,include/require,eval,extract, a filesystem path, or aheader()value without validation - Every outbound call (HTTP, DB, subprocess, lock) has a timeout, and every
false-returning call has its return inspected - Dates are
DateTimeImmutablewith an explicit IANA timezone; human-facing text handled withmb_* json_encode/json_decodecarryJSON_THROW_ON_ERROR- Syntax and functions stay within
min_php; layout matchesstyle; new behavior arrives with a test that was seen failing first
Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/php/config.yaml. Never interview the user — record a preference the moment it is stated.
| Variable | Type | Default | Effect |
|---|---|---|---|
| min_php | 7.4 | 8.0 | 8.1 | 8.2 | 8.3 | 8.4 | 8.5 | 8.2 | Gates which Version Floors features may be emitted unguarded, and which fallback appears instead (versions.md) |
| sapi | fpm | cli | apache-mod | swoole | roadrunner | fpm | Assumptions about request lifetime, static state, timeouts, and where output goes (fpm.md, cli.md, concurrency.md) |
| framework | none | laravel | symfony | wordpress | slim | none | Whether to emit plain PHP or framework idiom, and when to hand off to a framework skill |
| strict_types | always | new-files | never | always | Whether emitted files open with declare(strict_types=1), and how coercion is discussed (types.md) |
| style | per-cs | psr-12 | custom | per-cs | Layout of emitted code and the fixer ruleset produced (static-analysis.md) |
| static_analysis | none | phpstan | psalm | phpstan | Which checker's annotations, config file, and baseline commands appear (static-analysis.md) |
| analysis_level | number (0-9) | 6 | Strictness assumed when annotating, and whether a docblock is required alongside a native type (static-analysis.md) |
| test_framework | phpunit | pest | none | phpunit | Shape of emitted tests, assertions, and data providers (testing.md) |
| db_layer | pdo | doctrine | eloquent | mysqli | pdo | Which data-access idiom every query example uses (database.md) |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
- Tooling: fixer (PHP-CS-Fixer vs PHP_CodeSniffer), Rector adoption, Xdebug vs pcov for coverage, profiler of choice, local runtime (Docker, Homebrew, Valet, WSL) — affects every emitted config block and command
- Conventions: namespace and directory layout, DTO style (arrays vs readonly classes vs enums), exception hierarchy, naming, docblock policy — affects generated code and review comments
- Platform: deployment target (FPM behind nginx, shared hosting, container, serverless, long-running runtime), OS, extension availability (
ext-intl,ext-mbstring,ext-pcntl), private Packagist — affectscomposer.md,php-ini.md,fpm.md - Safety posture: how aggressively to flag
unserialize, dynamicinclude,extract,@, missing timeouts, unpinned dependencies, and emulated prepares — affectssecurity.mdand review depth - Dependencies: banned or mandated packages (Guzzle vs Symfony HttpClient, Carbon vs native
DateTimeImmutable), tolerance for new transitive dependencies, stdlib-only constraints — affects every recommendation - Output format: whole file vs minimal diff, how much explanation, whether tests and a changelog entry accompany every change — affects the shape of the answer, never the correctness rules
- Work order: which gates run before a change is proposed rather than after — analyzer and suite green first, a plan approved before editing, a profile before any optimization — affects the sequence of every task, never the correctness rules
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
if (!strpos($s, $x)) |
Returns 0 for a match at position 0 — the correct answer reads as failure |
strpos($s, $x) !== false, or str_contains() on php >=8.0 (strings.md) |
== to compare a password hash or token |
"0e12" == "0e34" is true; both are numeric strings equal to zero |
hash_equals(), or password_verify() for passwords (security.md) |
array_filter() then json_encode() |
Keys survive, so the gap turns the array into a JSON object and the client's .map() fails |
array_values() after every filter that feeds JSON (arrays.md, json.md) |
json_decode($s) with no flags |
Returns null for the input "null" AND for a parse error — indistinguishable |
Always JSON_THROW_ON_ERROR (json.md) |
$_POST for a JSON request body |
Populated only for form-encoded and multipart bodies | json_decode(file_get_contents('php://input'), ...) (http.md) |
Trusting $_FILES['f']['type'] |
The MIME type is supplied by the client and is trivially forged | finfo_file() on the temp file plus an extension allowlist (security.md) |
session_start() at the top and never closing |
The session lock is held for the whole request, so one user's parallel requests execute one at a time | session_write_close() as soon as the last write is done (sessions.md) |
DateTime::createFromFormat('Y-m-d', $d) |
Unspecified fields default to NOW, so the date carries the current time and can roll the day | Prefix the format with ! to zero them, then check getLastErrors() (datetime.md) |
@ to silence a warning |
Hides the diagnosis and leaves the broken value in flight; on php >=8.0 it no longer even silences fatals |
Handle the failure, or narrow error_reporting deliberately (errors.md) |
mt_rand/uniqid() for a token or password reset |
Predictable: uniqid() is a formatted timestamp, mt_rand is a seeded PRNG |
random_bytes()/random_int() (security.md) |
PDO::ATTR_PERSISTENT without cleanup |
A worker inherits an open transaction, session variables, or temporary tables from the previous request | Non-persistent connections, or an explicit rollback on checkout (database.md) |
A closing ?> at the end of a PHP file |
One trailing newline becomes output, and every later header() fails with "headers already sent" |
Omit the closing tag in pure-PHP files (http.md) |
readfile() for a large download |
Fills the output buffer with the whole file before sending | fpassthru with buffers flushed, or X-Accel-Redirect/X-Sendfile (http.md) |
opcache.validate_timestamps=0 with a deploy that only swaps files |
Workers keep executing the previous release's bytecode | Reload FPM (kill -USR2) as the last deploy step (performance.md, fpm.md) |
SELECT then INSERT to avoid duplicates |
Two workers pass the check in the same millisecond | Unique index plus catching the duplicate-key error (concurrency.md) |
Where Experts Disagree
- Arrays vs objects for structured data. The array-shape school keeps everything a
list<array{id:int,…}>and lets the analyzer verify it: no boilerplate, refactors are grep-based. The value-object school makes each shape a readonly class: constructor validation, no key typos, IDE completion. Boundary: data crossing a module or process boundary earns a class; a shape used in three lines of one function does not (static-analysis.md). - Active record vs data mapper. Eloquent-style records are faster to write and let any layer reach the database; Doctrine-style mappers keep the domain persistence-free and make the unit of work explicit. Boundary: how long the codebase must outlive its storage decisions — not how large it is (
database.md). - How strict to run the analyzer. One camp treats anything below
maxas unfinished; the other stops where the false-positive rate exceeds the bug-catch rate. Boundary: a greenfield typed domain reaches high levels cheaply; a decade-old codebase baselines and ratchets, and measures success as baseline shrinkage rather than level number (static-analysis.md). - Long-running runtimes. Swoole, RoadRunner, and FrankenPHP remove per-request bootstrap and can cut latency materially; they also delete PHP's strongest safety property, that every request starts from a clean process. Boundary: adopt when bootstrap is a measured share of p95 AND the team accepts hunting state leaks — never as a default (
concurrency.md). - Framework or none. A framework supplies routing, DI, migrations, and a hiring pool; plain PHP with a few PSR packages supplies a dependency tree you can read end to end. Boundary: the number of cross-cutting concerns you would otherwise hand-write — auth, queues, and migrations together is the tipping point.
Related Skills
More Clawic skills, get them at https://clawic.com/skills/php (install if the user confirms):
laravel— Eloquent, queues, Blade, and Laravel-specific authmysql— schema design, indexes, and query plans behind PDOnginx— the server in front of PHP-FPM: buffering, timeouts, static filesregex— PCRE pattern design; this skill'sstrings.mdcovers only the PHP bindingsdebugging— language-agnostic fault isolation; the PHP-specific version is this skill'sdebugging.md
Feedback
- If useful, star it: https://clawic.com/skills/php
- Latest version: https://clawic.com/skills/php
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/php.