From 724ae2a5e39a7537b8acffdca514c8c45ef7be3e Mon Sep 17 00:00:00 2001 From: MajorLinux Date: Sun, 10 May 2026 12:52:25 -0400 Subject: [PATCH] Add troubleshooting article: PHP 8.4 implicit-nullable vendor patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the Castopod/UuidModel incident from 2026-05-10. PHP 8.4 deprecated implicit-nullable parameters (`function f(int $x = null)`). Old vendor libraries spam E_DEPRECATED warnings; CodeIgniter wraps each in a 23-frame stack trace; per-minute spark cron amplifies into 53-80 MB/day log bleed and 22% sustained CPU floor on small VPS boxes. Documents the four-line sed fix AND the substring-match gotcha that extended the fix from 30 seconds to 30 minutes — bare `int \$limit = null` patterns substring-match `?int \$limit = null` elsewhere in the file and produce illegal `??type` syntax. Covers anchored sed patterns, reference-parameter handling (&\$db), the lint-after-every-edit rule, and a bonus section on hunting stray developer debug prints (`log_message('critical', 'ITS HEEEEEEEEEEEERE')`). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../php-84-vendor-implicit-nullable-patch.md | 225 ++++++++++++++++++ SUMMARY.md | 1 + index.md | 5 +- 3 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 05-troubleshooting/php-84-vendor-implicit-nullable-patch.md diff --git a/05-troubleshooting/php-84-vendor-implicit-nullable-patch.md b/05-troubleshooting/php-84-vendor-implicit-nullable-patch.md new file mode 100644 index 0000000..bca8ee9 --- /dev/null +++ b/05-troubleshooting/php-84-vendor-implicit-nullable-patch.md @@ -0,0 +1,225 @@ +--- +title: Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages +domain: troubleshooting +category: troubleshooting +tags: + - php + - php-8.4 + - codeigniter + - castopod + - composer + - vendor + - deprecation + - troubleshooting +status: published +created: 2026-05-10 +updated: 2026-05-10 +--- + +# Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages + +> **TL;DR** — PHP 8.4 deprecated implicit-nullable parameters (`function f(int $x = null)` without `?int`). Old vendor packages that haven't been updated will spam `E_DEPRECATED` warnings on every load. CodeIgniter (and similar frameworks) wrap each warning in a 23-frame stack trace, which on a per-minute cron multiplies into hundreds of MB/day of logs and a noticeable CPU floor on small VPS boxes. The fix is a four-line `sed` patch — but be very careful: a naive sed pattern can substring-match an *already-nullable* parameter and produce illegal `??type` syntax. + +--- + +## Symptom + +You're running a CodeIgniter 4 app (Castopod, BookStack, etc.) on PHP 8.4 with an older vendored library that hasn't been updated to declare nullable types properly. The combination produces: + +- **Sustained CPU floor** on a 1 vCPU box (typically 15–25% baseline) when the framework's spark/cron scheduler runs every 60 seconds +- **Massive daily log volume** in `writable/logs/log-YYYY-MM-DD.log` — 50–100 MB per day is common +- Each WARNING line is followed by a **23-frame stack trace** through Composer's autoloader, the framework's autoloader, and the application's command/model entry point +- The actual scheduler task may report `Failed:` even though it logs no obvious error — the deprecation is fatal in some PHP/CodeIgniter combinations + +The deprecation warnings look like: + +``` +WARNING - 2026-05-10 16:33:01 --> [DEPRECATED] +Vendor\Package\SomeModel::doFindAll(): Implicitly marking parameter $limit as nullable is deprecated, +the explicit nullable type must be used instead in +VENDORPATH/vendor/package/src/SomeModel.php on line 287. +``` + +## Why this matters more than a typical deprecation + +Three multipliers turn "minor PHP deprecation" into "the box is on fire": + +1. **Per-minute cron** — `php spark tasks:run` runs every 60 seconds. Each run loads the framework, hits the deprecation, dumps a stack trace. +2. **CodeIgniter's error handler is verbose** — it catches `E_DEPRECATED` and writes a full backtrace to disk. There's no debug-vs-production split here. +3. **Small VPS boxes have a thin idle margin** — on a 1 vCPU droplet, sustained 22% from PHP startup overhead + log writes is enough to trip a default `>85% / 5min` DigitalOcean alert during traffic spikes. + +## Diagnostic chain + +### 1. Confirm the symptom is deprecation cascade, not autoload failure + +The stack trace makes this look like an autoload error — it isn't. Check the WARNING line itself: + +- **`[DEPRECATED] ... Implicitly marking parameter ... as nullable`** → vendor library + PHP 8.4 mismatch (this article applies) +- **`Class 'X' not found`** → actual autoload problem (different fix) + +### 2. Identify the PHP version + +```bash +php -v +``` + +If it's 8.4+, implicit-nullable is now `E_DEPRECATED`. (PHP 8.4.0 was released 2024-11-21; many distros bumped during 2025–26.) + +### 3. List the offending lines + +The log itself names them. Grep for the unique vendor-path pattern: + +```bash +grep 'DEPRECATED' /var/www//writable/logs/log-$(date +%Y-%m-%d).log \ + | awk -F'on line ' '{print $2}' | sort -u +``` + +You'll typically see three to six line numbers in one file — each parameter that needs `?` prefixing. + +### 4. Inspect each line before patching + +```bash +F=/var/www//vendor//src/.php +sed -n '287p;520p' "$F" # Show only the lines named by the warnings +``` + +Look for **already-prefixed** parameters in the same function or nearby — if `?type $foo = null` already exists in the file, your sed pattern must not match it. + +## The fix — anchored sed + +**Step 1: Backup.** + +```bash +F=/var/www//vendor//src/.php +sudo cp -p "$F" "$F.bak.$(date +%Y%m%d-%H%M%S)" +``` + +**Step 2: Apply patches with anchors.** Don't use bare patterns like `int \$limit = null` — they'll substring-match against `?int \$limit = null` (an already-nullable parameter elsewhere in the file) and produce `??int $limit = null`, which PHP rejects as a `ParseError: unexpected token "??"`. + +Anchor on the function signature: + +```bash +sudo sed -i \ + -e 's|^\(\s*protected function doFindAll(\)int \$limit = null|\1?int $limit = null|' \ + -e 's|^\(\s*protected function doUpdateBatch(\)array \$set = null, string \$index = null|\1?array $set = null, ?string $index = null|' \ + "$F" +``` + +For constructors with reference operators (`&$db`), include the `&` in the anchor: + +```bash +sudo sed -i 's|ConnectionInterface &\$db = null|?ConnectionInterface \&$db = null|' "$F" +``` + +**Step 3: Lint immediately.** + +```bash +sudo php -l "$F" +# Must print: No syntax errors detected in +``` + +If lint fails, restore from the backup and try a tighter anchor — don't chain another sed onto a broken file. + +**Step 4: Verify the runtime.** + +```bash +sudo -u www-data php /var/www//spark tasks:run | grep -E '(Executed|Failed)' +``` + +The previously-Failing task should now show `Executed`. + +**Step 5: Confirm the log bleed stops.** Wait 60s, then: + +```bash +LOG=/var/www//writable/logs/log-$(date +%Y-%m-%d).log +SINCE=$(date -d '60 seconds ago' '+%H:%M:%S') +awk -v t="$SINCE" '/DEPRECATED/ && $4>=t' "$LOG" | wc -l +# Expect: 0 +``` + +## The substring-match gotcha (the one that bit me) + +This is the failure mode that turns a 30-second fix into a 30-minute incident: + +```bash +# DANGEROUS +sed -i 's|int \$limit = null|?int $limit = null|' "$F" +``` + +That pattern matches both: + +- `protected function doFindAll(int $limit = null, …)` — the line you want to fix +- `protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100)` — somewhere else in the file, where there's an `int $limit = null` substring **inside** an already-nullable signature you don't want to touch + +After sed, the second line becomes `??array $set = null` (or similar) — illegal in PHP. The first time the autoloader tries to load the file, you get: + +``` +ParseError: syntax error, unexpected token "??", expecting variable +at vendor/.../src/.php:426 +``` + +Recovery is restore-from-backup, then re-apply with anchored patterns. **Always lint before reload, before flush, before next anything.** + +## Are reference parameters tricky? Yes. + +`&$db` (pass-by-reference) needs the ampersand preserved when adding the `?` prefix: + +| Before | After | +|---|---| +| `ConnectionInterface &$db = null` | `?ConnectionInterface &$db = null` | +| `array &$rows` | `?array &$rows` | + +In sed, escape the ampersand in the replacement (`\&`) because unescaped `&` in the replacement means "the matched text." Easy way to test the right escaping: run sed with `--debug` or do a dry-run with `-n` and `p`. + +## Bonus: hunt for stray debug prints while you're in there + +When you're already grepping the application source for one issue, scan for sloppy `log_message('critical', ...)` calls left in by upstream developers. Real-world finds include: + +- `log_message('critical', 'ITS HEEEEEEEEEEEERE');` — left in Castopod's `modules/Fediverse/Filters/FediverseFilter.php` line 62, firing on every fediverse request, contributing 195 CRITICAL entries to one day's log +- `log_message('critical', 'TODO');` +- `log_message('critical', 'wtf');` + +```bash +grep -rE "log_message\(['\"]critical['\"]" /var/www//modules/ /var/www//app/ \ + | grep -v -E 'TODO|FIXME' \ + | head -10 +``` + +These are usually safe to remove (or downgrade to `debug` level) — they don't represent real failure conditions, just developer artifacts. + +## Why not just upgrade the vendor package? + +`composer update ` is the proper fix. But: + +- Many PHP applications (Castopod especially) ship pre-built `vendor/` and don't expect composer to be installed at runtime +- A major version bump (`v1.x → v2.x`) implies API changes that the application may not handle +- `composer update` may pull in cascading dependency updates you don't want + +Hot-patching is the right answer when: + +- The application doesn't ship with `composer.json` referencing the package directly +- The fix is purely syntactic (parameter type declarations) +- A future application release will likely include the upgraded vendor anyway + +Just **document the patch** and add a follow-up task to re-apply (or skip) after the next application upgrade. Without that note, the next time the box is rebuilt or upgraded, you'll spend another evening chasing the same stack trace. + +## Specific examples observed in the MajorsHouse fleet + +### Castopod 1.20+ on PHP 8.4 + +`vendor/michalsn/codeigniter4-uuid/src/UuidModel.php` v1.3.1 — four nullable-prefix corrections needed: + +| Line | Original | Patched | +|---|---|---| +| 54 | `__construct(ConnectionInterface &$db = null, ValidationInterface $validation = null)` | `__construct(?ConnectionInterface &$db = null, ?ValidationInterface $validation = null)` | +| 287 | `doFindAll(int $limit = null, int $offset = 0)` | `doFindAll(?int $limit = null, int $offset = 0)` | +| 520 | `doUpdateBatch(array $set = null, string $index = null, …)` | `doUpdateBatch(?array $set = null, ?string $index = null, …)` | + +Line 426 (`doInsertBatch(?array $set = null, ?bool $escape = null, …)`) was already correct — the substring-match gotcha above was triggered by it. + +Upstream `michalsn/codeigniter4-uuid` v2.0.0 (released 2024) declares all parameters with explicit `?type` syntax and has no deprecation warnings. Castopod hadn't upgraded the dependency as of Castopod 1.20. + +## See also + +- [Castopod Posts Don't Appear on Mastodon — Diagnosing the Federation Path](security/castopod-broadcast-not-on-mastodon.md) — tttpod-specific diagnostic +- [PHP RFC: Deprecate implicitly nullable parameter types](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types) — the canonical PHP 8.4 reference diff --git a/SUMMARY.md b/SUMMARY.md index c3ebec3..59c2eb1 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -98,6 +98,7 @@ updated: 2026-05-10T00:10 * [Pi-hole AI Blocklist Blocks Claude Desktop (ERR_CONNECTION_REFUSED)](05-troubleshooting/networking/pihole-blocks-claude-desktop.md) * [Claude Desktop MCP Server Started via wsl.exe Sees Empty Environment (WSLENV)](05-troubleshooting/wsl-env-claude-desktop-mcp.md) * [Claude Desktop MCP Mass-Disconnect After Blocking SSH Reboot](05-troubleshooting/claude-desktop-mcp-mass-disconnect-blocking-reboot.md) + * [Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages](05-troubleshooting/php-84-vendor-implicit-nullable-patch.md) * [Ollama Drops Off Tailscale When Mac Sleeps](05-troubleshooting/ollama-macos-sleep-tailscale-disconnect.md) * [Ollama: `ollama run` with Piped Stdin Bypasses Chat Template + SYSTEM Prompt](05-troubleshooting/ollama-chat-template-pipe-stdin-bypass.md) * [rsync over Tailscale: Hung in TCP Teardown After Transfer Completes](05-troubleshooting/networking/rsync-tailscale-teardown-stall.md) diff --git a/index.md b/index.md index 6dc8054..52ecba4 100644 --- a/index.md +++ b/index.md @@ -7,7 +7,7 @@ updated: 2026-05-10T01:30 > A growing reference of Linux, self-hosting, open source, streaming, and troubleshooting guides. Written by MajorLinux. Used by MajorTwin. > > **Last updated:** 2026-05-10 -> **Article count:** 110 +> **Article count:** 111 ## Domains @@ -17,7 +17,7 @@ updated: 2026-05-10T01:30 | 🏠 Self-Hosting & Homelab | `02-selfhosting/` | 39 | | 🔓 Open Source Tools | `03-opensource/` | 10 | | 🎙️ Streaming & Podcasting | `04-streaming/` | 2 | -| 🔧 General Troubleshooting | `05-troubleshooting/` | 47 | +| 🔧 General Troubleshooting | `05-troubleshooting/` | 48 | --- @@ -217,6 +217,7 @@ updated: 2026-05-10T01:30 | Date | Article | Domain | |---|---|---| +| 2026-05-10 | [Patching PHP 8.4 Implicit-Nullable Deprecations in Vendor Packages](05-troubleshooting/php-84-vendor-implicit-nullable-patch.md) — generalized from a Castopod/UuidModel incident; covers the substring-match gotcha that turns a 30-second fix into a 30-minute one | Troubleshooting | | 2026-05-10 | [Logwatch Fleet Setup — Surviving Package Upgrades](02-selfhosting/monitoring/logwatch-fleet-setup.md) — added Fedora CA bundle missing diagnosis, journald-vs-mail.log methodology note, and bounce-source-must-be-real-mailbox section | Self-Hosting | | 2026-05-10 | [ClamAV Fleet Deployment with Ansible](02-selfhosting/security/clamav-fleet-deployment.md) — added DigitalOcean monitoring caveat for 1vCPU droplets (with follow-up note: per-droplet relaxed alert can still trip; accept-the-page decision) | Self-Hosting | | 2026-05-10 | [Claude Desktop MCP Mass-Disconnect After Blocking SSH Reboot](05-troubleshooting/claude-desktop-mcp-mass-disconnect-blocking-reboot.md) | Troubleshooting |