Skill: ivangdavila/php (version 1.0.1)

--- name: PHP slug: php version: 1.0.1 description: Write solid PHP avoiding type juggling traps, array quirks, and common security pitfalls. metadata: {"clawdbot":{"emoji":"🐘","requires":{"bins":["php"]},"os":["linux","darwin","win32"]}} --- ## Quick Reference | Topic | File | |-------|------| | Loose typing, ==, ===, type juggling, strict_types | `types.md` | | Associative arrays, iteration, array functions | `arrays.md` | | Traits, interfaces, visibility, late static binding | `oop.md` | | Encoding, interpolation, heredoc, regex | `strings.md` | | Exceptions, error handling, @ operator | `errors.md` | | SQL injection, XSS, CSRF, input validation | `security.md` | | PHP 8+ features, attributes, named args, match | `modern.md` | ## Critical Rules - `==` coerces types: `"0" == false` is true — always use `===` for strict comparison - `in_array($val, $arr)` uses loose comparison — pass `true` as third param for strict - `strpos()` returns 0 for match at start — use `=== false` not `!strpos()` - Never concatenate SQL — use prepared statements with PDO - `htmlspecialchars($s, ENT_QUOTES)` all output — prevents XSS - `isset()` returns false for null — use `array_key_exists()` to check key exists - `foreach ($arr as &$val)` — unset `$val` after loop or last ref persists - `static::` late binding vs `self::` early binding — `static` respects overrides - `@` suppresses errors — avoid, makes debugging impossible - Catch `Throwable` for both `Error` and `Exception` — PHP 7+ - `declare(strict_types=1)` per file — enables strict type checking - `strlen()` counts bytes — use `mb_strlen()` for UTF-8 character count - Objects pass by reference-like handle — clone explicitly with `clone $obj` - `array_merge()` reindexes numeric keys — use `+` operator to preserve keys

path: arrays.md
content: # Array Traps

- `array_merge()` reindexes — use `+` operator to preserve numeric keys
- Unset doesn't reindex — `unset($arr[1])` leaves gap, `array_values()` to fix
- `foreach` by reference — `foreach($arr as &$v)` keeps ref after loop, unset it
- `$arr[] = x` vs `$arr[0] = x` — first appends, second replaces
- `array_filter()` no callback — removes falsy INCLUDING `"0"` and `0`
- `array_map` null callback — zips arrays together, not what you expect
- `array_keys()` strict — pass third param `true` for strict comparison
- Negative index doesn't wrap — `$arr[-1]` is literal key `-1`, not last element


path: errors.md
content: # Error Traps

- `@` suppresses errors — hides problems, never use in production
- Exception vs Error — `\Error` is separate hierarchy, catch `\Throwable`
- `set_error_handler` — doesn't catch fatal errors
- `try/finally` — finally runs even on return, but not on `exit()`
- Uncaught exception — fatal error, process dies
- `error_reporting(0)` — still logs to file if configured, not silent
- `trigger_error` for warnings — won't throw exception


path: modern.md
content: # PHP 8+ Traps

- Named args break rename — `foo(name: $x)` breaks if param renamed
- Match is exhaustive — no matching arm throws `UnhandledMatchError`
- Nullsafe `?->` — returns null, doesn't short-circuit further ops
- Union types null — `int|null` not same as `?int` in some contexts
- Attributes reflection — `#[Attr]` needs ReflectionAttribute to read
- Constructor promotion + default — `public int $x = 0` in signature
- `str_contains`/`str_starts_with` — PHP 8+, polyfill for older
- Enums can't extend — backed enums need type, cases are singletons


path: oop.md
content: # OOP Traps

- Late static binding — `self::` binds at define time, `static::` at call time
- Abstract class can't instantiate — but CAN have constructor for children
- Trait method conflicts — must resolve with `insteadof` or `as`
- Interface constants — can't override in implementing class
- Clone is shallow — nested objects still shared, implement `__clone`
- `instanceof` with string — `$obj instanceof $className` works dynamically
- Readonly properties — can only set once, in constructor or declaration
- Constructor promotion — `public function __construct(public $x)` declares property


path: security.md
content: # Security Traps

- SQL injection — use prepared statements, NEVER concatenate user input
- XSS — `htmlspecialchars($input, ENT_QUOTES, 'UTF-8')` on all output
- CSRF — verify token on state-changing requests
- File upload — check MIME type, extension, AND magic bytes
- `include($userInput)` — remote file inclusion, validate path strictly
- `unserialize()` — can execute code, use `json_decode()` instead
- `extract($_POST)` — overwrites variables, including `$isAdmin`
- Session fixation — `session_regenerate_id(true)` on login
- Weak comparison in auth — `"0e123" == "0e456"` is true, breaks hash compare


path: strings.md
content: # String Traps

- Encoding hell — `strlen()` counts bytes not chars, `mb_strlen()` for UTF-8
- `strpos()` returns 0 — `if(strpos(...))` fails when found at start, use `!== false`
- Single vs double quotes — `"$var"` interpolates, `'$var'` is literal
- Heredoc indentation — closing identifier must not be indented (PHP <7.3)
- Regex delimiters — `/pattern/` or `#pattern#`, forgetting causes error
- `preg_replace` with `/e` — removed in PHP 7, was code injection vector
- Null byte — `"file\0.txt"` truncates at null in some functions


path: types.md
content: # Type Traps

- `==` coerces types — `"0" == false` is true, always use `===`
- `"10" == "10.0"` — string comparison converts to numbers if both numeric
- `0 == "any"` before PHP 8 — legacy code still has this bug
- `in_array()` loose — pass `true` as third param for strict
- `switch` uses loose comparison — use `match` in PHP 8+ for strict
- `empty("0")` is true — "0" is falsy, use `=== ""` or `strlen()`
- Type declarations coerce — `int` param accepts "123", use `strict_types`


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)