围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。
记忆
PHP
按语言层规则编写、调试、审查 PHP:严格类型、正确转义、合理配置 FPM 与 OPcache。
它能做什么
围绕 PHP 语言本身的编写、调试与审查:strict_types、=== 比较、mb_* 处理文本、DateTimeImmutable 处理日期、PDO 关闭模拟预编译并绑定参数、按输出上下文转义、session 锁处理、FPM/OPcache 调优、PHPUnit 与 PHPStan 流程。常见触发场景:白屏、静默 500、"headers already sent"、内存或执行时间超限、Composer 安装后 Class not found、json_encode 返回 false、字符乱码、日期偏移、PHP 版本升级时的废弃警告。框架内部(Eloquent、队列、Laravel 认证)交由独立的 laravel 技能处理。
什么时候用它
- 排查白屏与没有任何报错的 500
- 审查 PDO 查询、参数绑定与输出转义
- 跨 PHP 8.x 小版本升级并清理废弃警告
- 为 nginx + PHP-FPM 估算 pm.max_children 与 OPcache 配置
技能文档
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 '' 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
listand 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.
常见问题
- 为什么用 `==` 比较哈希时,错误口令也会通过?
- 两个都为 0 的数字字符串比较相等,因此 "0e12" == "0e34" 为真。该技能要求一律使用 ===,并用 hash_equals 比较摘要。
- 为什么同一用户的并发 AJAX 请求会被串行执行?
- session_start() 会持有 session 文件的排他锁。该技能要求在长任务前调用 session_write_close() 提前释放。
- 为什么改了 php.ini 却看不到效果?
- .user.ini 默认缓存 300 秒,且 CLI 与 Web SAPI 读取的 ini 路径不同。该技能会按 SAPI 走查 php --ini,并解释 ini 的优先级。
相关技能
按配置的 JDK 版本诊断 Java 与 JVM 问题(从 NPE 到容器 OOM),给出可直接套用的代码与配置。
解读 TypeScript 类型错误,设计 API、tsconfig 与 .d.ts 的类型方案。
按文档化的 Python 规则手册和固定检查清单排查、审阅与编写代码。
按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。
诊断 GraphQL 缺陷并加固 schema、resolver 与客户端,处理 N+1、null 传播、订阅失效与联邦拆分。