Install
openclaw skills install @ivangdavila/phpWrites, 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.
openclaw skills install @ivangdavila/phpUser 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/.
laravel owns Eloquent, queues, Blade, and Laravel auth| 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.
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).=== 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.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).PDO::ATTR_EMULATE_PREPARES => false (database.md).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).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.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).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).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).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 |
Before delivering PHP code, check:
declare(strict_types=1); present, and every parameter, property, and return typed as narrowly as the value allows==, !=, or switch on values whose types are not both proven; in_array and array_search carry true as the strict flagEMULATE_PREPARES offunserialize, include/require, eval, extract, a filesystem path, or a header() value without validationfalse-returning call has its return inspectedDateTimeImmutable with an explicit IANA timezone; human-facing text handled with mb_*json_encode/json_decode carry JSON_THROW_ON_ERRORmin_php; layout matches style; new behavior arrives with a test that was seen failing firstUser-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:
ext-intl, ext-mbstring, ext-pcntl), private Packagist — affects composer.md, php-ini.md, fpm.mdunserialize, dynamic include, extract, @, missing timeouts, unpinned dependencies, and emulated prepares — affects security.md and review depthDateTimeImmutable), tolerance for new transitive dependencies, stdlib-only constraints — affects every recommendation| 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) |
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).database.md).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).concurrency.md).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's strings.md covers only the PHP bindingsdebugging — language-agnostic fault isolation; the PHP-specific version is this skill's debugging.mdPart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/php.