Add troubleshooting article: PHP 8.4 implicit-nullable vendor patch
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) <noreply@anthropic.com>
This commit is contained in:
parent
631d7e8bc5
commit
724ae2a5e3
3 changed files with 229 additions and 2 deletions
225
05-troubleshooting/php-84-vendor-implicit-nullable-patch.md
Normal file
225
05-troubleshooting/php-84-vendor-implicit-nullable-patch.md
Normal file
|
|
@ -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/<app>/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/<app>/vendor/<package>/src/<File>.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/<app>/vendor/<package>/src/<File>.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 <path>
|
||||||
|
```
|
||||||
|
|
||||||
|
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/<app>/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/<app>/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/<File>.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/<app>/modules/ /var/www/<app>/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 <package>` 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
|
||||||
|
|
@ -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)
|
* [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 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)
|
* [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 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)
|
* [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)
|
* [rsync over Tailscale: Hung in TCP Teardown After Transfer Completes](05-troubleshooting/networking/rsync-tailscale-teardown-stall.md)
|
||||||
|
|
|
||||||
5
index.md
5
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.
|
> A growing reference of Linux, self-hosting, open source, streaming, and troubleshooting guides. Written by MajorLinux. Used by MajorTwin.
|
||||||
>
|
>
|
||||||
> **Last updated:** 2026-05-10
|
> **Last updated:** 2026-05-10
|
||||||
> **Article count:** 110
|
> **Article count:** 111
|
||||||
|
|
||||||
## Domains
|
## Domains
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ updated: 2026-05-10T01:30
|
||||||
| 🏠 Self-Hosting & Homelab | `02-selfhosting/` | 39 |
|
| 🏠 Self-Hosting & Homelab | `02-selfhosting/` | 39 |
|
||||||
| 🔓 Open Source Tools | `03-opensource/` | 10 |
|
| 🔓 Open Source Tools | `03-opensource/` | 10 |
|
||||||
| 🎙️ Streaming & Podcasting | `04-streaming/` | 2 |
|
| 🎙️ 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 |
|
| 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 | [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 | [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 |
|
| 2026-05-10 | [Claude Desktop MCP Mass-Disconnect After Blocking SSH Reboot](05-troubleshooting/claude-desktop-mcp-mass-disconnect-blocking-reboot.md) | Troubleshooting |
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue