记忆

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 — laravel owns Eloquent, queues, Blade, and Laravel auth

Quick Reference

SituationPlay
== compares wrong, a numeric string misbehaves, floats do not add up=== everywhere; "0e12" == "0e34" is true → hash_equalstypes.md
Keys vanish, order is wrong, a filtered list encodes as a JSON objectarray_filter keeps keys; array_values() before encoding → arrays.md
Accents break, strlen disagrees with what you see, a regex silently failsmb_* for text, /u on every pattern, check preg_last_error()strings.md
Class design: traits, static::, readonly, enums, magic methods, cloneself:: binds at compile time, static:: at call time → oop.md
Exceptions, error handlers, warnings you cannot see, fatal errorsCatch \Throwable; fatals need register_shutdown_functionerrors.md
Injection, XSS, uploads, unserialize, passwords, tokensBind every value, escape per output context, random_bytes for secrets → security.md
Which PHP 8 feature to use, and how it bitesmatch 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 installsCommit the lock for apps, never for libraries → composer.md
White page, silent 500, "it worked yesterday", a hang with no outputSymptom → 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 disagreephp --ini per SAPI; .user.ini is cached for 300s → php-ini.md
502, 504, "server reached pm.max_children", workers eating RAMSize from measured RSS; slowlog names the hung function → fpm.md
Console scripts, daemons, cron, signals, exit codesmax_execution_time is 0 on CLI; recycle workers on a job count → cli.md
PHPUnit setup, flaky or falsely-passing tests, coverage, mocksassertSame not assertEquals; static state leaks between tests → testing.md
PHPStan or Psalm errors, baselines, coding standard, automated refactorsBaseline 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 => falsedatabase.md
Request/response handling, JSON bodies, uploads, outbound HTTP callsJSON bodies live in php://input, not $_POSThttp.md
Parallel requests from one user run one at a time; logins do not sticksession_start() holds an exclusive lock — session_write_close() early → sessions.md
Dates off by a day or an hour, DST, parsing, storageDateTimeImmutable + IANA zones; createFromFormat inherits today → datetime.md
json_decode returns null, big IDs lose digits, json_encode returns falseAlways pass JSON_THROW_ON_ERRORjson.md
Reading or writing files, CSV, streams, temp files, permissionsStream instead of slurping; write temp then rename()files.md
Parallel work, queues, locks across processes, long-running runtimesShare-nothing per request; proc_open deadlocks if you read one pipe → concurrency.md
Anything elseCore 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

  1. 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 your int parameter. Return values are checked against the mode of the file where the function is DECLARED (types.md).
  2. === and !== by default; == only where both operand types are proven. Worked case: "0e12" == "0e34" is true because both are numeric strings equal to 0 — a hash compared with == accepts the wrong password. Compare digests with hash_equals($known, $given), which is also constant-time.
  3. Check anything that can return false with !== false, never with truthiness. strpos($haystack, $needle) returns 0 for a match at offset 0, and if (!strpos(...)) reads that correct answer as a miss; the same shape breaks preg_match (0 = no match, false = engine error) and file_get_contents ("" is a valid file).
  4. 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).
  5. 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 needs json_encode with the HEX flags, one going into a URL needs rawurlencode, one going into a shell needs escapeshellarg (security.md).
  6. Text is characters, not bytes: strlen, substr, str_pad, strrev, and ucfirst count bytes and will cut a UTF-8 sequence in half. Use mb_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.
  7. DateTimeImmutable everywhere. 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).
  8. Every call that leaves the process gets an explicit timeout: CURLOPT_TIMEOUT plus CURLOPT_CONNECTTIMEOUT, a stream context timeout for fopen, PDO::ATTR_TIMEOUT for connections. max_execution_time will not save you — on Unix it does not count time spent inside system calls, so a hung socket read is only killed by FPM's request_terminate_timeout (fpm.md).
  9. Stream instead of slurping. file_get_contents on a 400 MB export needs 400 MB plus copies inside the default 128M memory_limit; fgetcsv in a loop, or a generator, holds one row. The same rule retires fetchAll() 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.

FeatureNeeds
Typed properties, arrow functions fn() =>, spread in calls, ??=, covariant return typesphp >=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 modephp >=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 defaultphp >=8.1
readonly classes, DNF types, null/false/true as standalone types, locale-independent strtolower/strtoupper, dynamic properties deprecated, ${var} interpolation deprecatedphp >=8.2
Typed class constants, #[\Override], json_validate(), mb_str_pad(), readonly reinitialization inside __clone, stack-overflow detection via zend.max_allowed_stack_sizephp >=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) deprecatedphp >=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 ==, !=, or switch on values whose types are not both proven; in_array and array_search carry true as the strict flag
  • Every SQL value bound, every identifier allowlisted, EMULATE_PREPARES off
  • 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 a header() value without validation
  • Every outbound call (HTTP, DB, subprocess, lock) has a timeout, and every false-returning call has its return inspected
  • Dates are DateTimeImmutable with an explicit IANA timezone; human-facing text handled with mb_*
  • json_encode/json_decode carry JSON_THROW_ON_ERROR
  • Syntax and functions stay within min_php; layout matches style; 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.

VariableTypeDefaultEffect
min_php7.4 | 8.0 | 8.1 | 8.2 | 8.3 | 8.4 | 8.58.2Gates which Version Floors features may be emitted unguarded, and which fallback appears instead (versions.md)
sapifpm | cli | apache-mod | swoole | roadrunnerfpmAssumptions about request lifetime, static state, timeouts, and where output goes (fpm.md, cli.md, concurrency.md)
frameworknone | laravel | symfony | wordpress | slimnoneWhether to emit plain PHP or framework idiom, and when to hand off to a framework skill
strict_typesalways | new-files | neveralwaysWhether emitted files open with declare(strict_types=1), and how coercion is discussed (types.md)
styleper-cs | psr-12 | customper-csLayout of emitted code and the fixer ruleset produced (static-analysis.md)
static_analysisnone | phpstan | psalmphpstanWhich checker's annotations, config file, and baseline commands appear (static-analysis.md)
analysis_levelnumber (0-9)6Strictness assumed when annotating, and whether a docblock is required alongside a native type (static-analysis.md)
test_frameworkphpunit | pest | nonephpunitShape of emitted tests, assertions, and data providers (testing.md)
db_layerpdo | doctrine | eloquent | mysqlipdoWhich 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 — affects composer.md, php-ini.md, fpm.md
  • Safety posture: how aggressively to flag unserialize, dynamic include, extract, @, missing timeouts, unpinned dependencies, and emulated prepares — affects security.md and 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

TrapWhy it failsDo instead
if (!strpos($s, $x))Returns 0 for a match at position 0 — the correct answer reads as failurestrpos($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 zerohash_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() failsarray_values() after every filter that feeds JSON (arrays.md, json.md)
json_decode($s) with no flagsReturns null for the input "null" AND for a parse error — indistinguishableAlways JSON_THROW_ON_ERROR (json.md)
$_POST for a JSON request bodyPopulated only for form-encoded and multipart bodiesjson_decode(file_get_contents('php://input'), ...) (http.md)
Trusting $_FILES['f']['type']The MIME type is supplied by the client and is trivially forgedfinfo_file() on the temp file plus an extension allowlist (security.md)
session_start() at the top and never closingThe session lock is held for the whole request, so one user's parallel requests execute one at a timesession_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 dayPrefix the format with ! to zero them, then check getLastErrors() (datetime.md)
@ to silence a warningHides the diagnosis and leaves the broken value in flight; on php >=8.0 it no longer even silences fatalsHandle the failure, or narrow error_reporting deliberately (errors.md)
mt_rand/uniqid() for a token or password resetPredictable: uniqid() is a formatted timestamp, mt_rand is a seeded PRNGrandom_bytes()/random_int() (security.md)
PDO::ATTR_PERSISTENT without cleanupA worker inherits an open transaction, session variables, or temporary tables from the previous requestNon-persistent connections, or an explicit rollback on checkout (database.md)
A closing ?> at the end of a PHP fileOne 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 downloadFills the output buffer with the whole file before sendingfpassthru with buffers flushed, or X-Accel-Redirect/X-Sendfile (http.md)
opcache.validate_timestamps=0 with a deploy that only swaps filesWorkers keep executing the previous release's bytecodeReload FPM (kill -USR2) as the last deploy step (performance.md, fpm.md)
SELECT then INSERT to avoid duplicatesTwo workers pass the check in the same millisecondUnique 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 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 max as 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.

More Clawic skills, get them at https://clawic.com/skills/php (install if the user confirms):

  • laravel — Eloquent, queues, Blade, and Laravel-specific auth
  • mysql — schema design, indexes, and query plans behind PDO
  • nginx — the server in front of PHP-FPM: buffering, timeouts, static files
  • regex — PCRE pattern design; this skill's strings.md covers only the PHP bindings
  • debugging — language-agnostic fault isolation; the PHP-specific version is this skill's debugging.md

Feedback

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 的优先级。

相关技能

围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。

作者 Iván135 次安装6 星标

按配置的 JDK 版本诊断 Java 与 JVM 问题(从 NPE 到容器 OOM),给出可直接套用的代码与配置。

作者 Iván130 次安装9 星标

解读 TypeScript 类型错误,设计 API、tsconfig 与 .d.ts 的类型方案。

185 次安装6 星标

按文档化的 Python 规则手册和固定检查清单排查、审阅与编写代码。

156 次安装6 星标

按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。

245 次安装3 星标

诊断 GraphQL 缺陷并加固 schema、resolver 与客户端,处理 N+1、null 传播、订阅失效与联邦拆分。

91 次安装2 星标