Repository navigation
chore(phpstan): load module language constants via explicit allowlist #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # Hosting a library that grabs global handlers | ||
|
|
||
| Some libraries take things that belong to the whole process: PHP's error and exception | ||
| handlers, shutdown functions, the session, the output buffer, `ini` settings, autoloaders, | ||
| headers. They do it because they were written for an application that owns the request. | ||
| Inside XOOPS your module does not own the request — it is one of many things loading during | ||
| a boot, and XoopsLogger, DebugBar and the error screen all have claims on the same globals. | ||
|
|
||
| So you will sometimes want a library's features **without** one of the side effects it | ||
| installs. This page is about how to do that without breaking the library. | ||
|
|
||
| ## The rule | ||
|
|
||
| > **Refuse the side effect at the library's own injection seam. Do not let it happen and | ||
| > then undo it.** | ||
|
|
||
| Most libraries that grab a global offer a way to say no: a constructor argument, a config | ||
| flag, a driver or facade object, a factory you can subclass. Passing your own object through | ||
| that seam means the side effect never happens. Undoing it afterwards means it happened, and | ||
| you are now relying on the library not noticing. | ||
|
|
||
| ## Why "undo it afterwards" fails | ||
|
|
||
| It usually works, right up until the library tidies up after itself. | ||
|
|
||
| Global hooks are almost always installed and removed as a **matched pair**: `register()` and | ||
| `unregister()`, `enable()` and `disable()`, and often a destructor that calls the second one | ||
| whether or not you asked. PHP's own handler functions make this concrete — | ||
| `set_error_handler()` **pushes** a frame and `restore_error_handler()` **pops** one; they are | ||
| not setter and getter. | ||
|
|
||
| So if the library pushes two frames and you quietly pop one of them back, the library's | ||
| teardown still pops two. One of those pops takes a frame that was never the library's, and | ||
| the request ends with the error handler one level below where it started — usually somewhere | ||
| that silently swallows notices. Nothing throws. Nothing logs. You find it months later. | ||
|
|
||
| The worked example this rule came from: a Whoops provider needed the exception and shutdown | ||
| handlers but not the error handler. `restore_error_handler()` immediately after | ||
| `$whoops->register()` looked like the obvious fix, passed its tests, and was wrong — | ||
| `Run::unregister()`, called by `Run::__destruct()`, restores **both** handlers. The fix that | ||
| holds is to pass `Run::__construct()` a `Whoops\Util\SystemFacade` subclass whose | ||
| `setErrorHandler()` and `restoreErrorHandler()` do nothing, so Whoops is never given the | ||
| error handler and its own pairing stays balanced by construction. | ||
|
|
||
| ## Does it apply here? | ||
|
|
||
| | Use the seam when | Do something else when | | ||
| |---|---| | ||
| | The library exposes a facade, driver, factory or constructor argument for its global hooks | It offers only a global `enable()` with no injection point | | ||
| | Its lifecycle is paired — register/unregister, enable/disable, or a destructor that cleans up | Nothing is ever torn down, so there is no pairing to break | | ||
| | You want a documented **subset** of what it installs by default | You actually want everything it installs | | ||
| | The subset is part of your module's contract with the site | Refusing would break the feature you are shipping | | ||
|
|
||
| ### When there is genuinely no seam | ||
|
|
||
| Undo-after is not the fallback. It is the thing this page tells you not to do, and having no | ||
| alternative does not make it safe — it makes the integration **unsupported until you have | ||
| done the work to know it is safe**. Concretely, before you ship a `restore_*`: | ||
|
|
||
| - read the library's setup path, its explicit teardown, **and** its destructor, and know | ||
| which of them touch the global you are taking back; | ||
| - know whether the library's teardown assumes it still owns what you took — that is the | ||
| failure that motivated this page, and it is invisible until something destructs; | ||
| - **write a test that asserts the process-wide state is what it was**, exercised across the | ||
| library's full lifetime including teardown. Not a comment. A comment records your belief; | ||
| a test notices when the belief stops being true, which is usually at the next upgrade. | ||
|
|
||
| If you cannot do those three, do not host the library that way. Ask upstream for a seam — | ||
| that is how `SystemFacade` came to exist in Whoops — or keep the side effect and design | ||
| around it. | ||
|
|
||
| ## Before you ship one | ||
|
|
||
| 1. **Prefer an official option.** A documented config flag beats a subclass. | ||
| 2. **Use a public seam.** Never reflection, never a vendor edit, never `@` on the symptom. | ||
| 3. **Copy the parent signature verbatim.** Adding a type to an inherited untyped parameter | ||
| is a fatal at class-declaration time, on every request. Refactoring and static-analysis | ||
| tools propose exactly this — write a comment saying no, because you will be asked again. | ||
| (Adding a *return* type where the parent declares none is fine and covariant-safe.) | ||
| 4. **Check the library does not use what you refused.** If it stores the return value of the | ||
| method you neutered, returning `null` may not be harmless. | ||
| 5. **Test the teardown, not just the setup.** The bug above is invisible if you only assert | ||
| state after `register()`. Assert it after `unregister()` too — and after the object goes | ||
| out of scope, if it has a destructor. | ||
| 6. **Re-check on upgrade.** You are depending on a shape the library did not promise to | ||
| keep. Pin the dependency or re-run the teardown test when you bump it. | ||
|
|
||
| ## Libraries this comes up with | ||
|
|
||
| `SystemFacade` is Whoops' door and nothing else's. Tracy, Monolog, Symfony's `ErrorHandler` | ||
| and Sentry each install global hooks through a different one, and some of them through | ||
| several. The reflex to carry is not an API name: | ||
|
|
||
| > **Look for the injection point before reaching for `restore_*`.** | ||
|
|
||
| --- | ||
|
|
||
| *Origin: XOOPS 2.7.3's error-screen provider seam, where the first attempt at this was | ||
| wrong in the way described above. The seam-specific decision — why xWhoops refuses the error | ||
| handler in particular — lives in the error-screen provider ADR that ships with that feature. | ||
| The rule is general and lives here.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,6 +177,28 @@ namespace { | |
| { | ||
| public function __construct() {} | ||
| } | ||
|
|
||
| // ------------------------------------------------- error-screen ownership (2.7.3+) | ||
|
|
||
| // The provider API published by htdocs/include/debugconfig.php. A module that wants to | ||
| // own the error screen calls these from its install / update / uninstall hooks. | ||
| // | ||
| // SINCE 2.7.3, and this stub does not say otherwise. A `core27` profile describes the | ||
| // LATEST 2.7 API, not what a given site is running -- so a module whose min_xoops is | ||
| // below 2.7.3 must keep its function_exists() guard around every call. Declaring a | ||
| // symbol here makes analysis stop complaining; it does not make the function exist. | ||
| // | ||
| // See docs/hosting-a-library-that-grabs-global-handlers.md for the companion rule on | ||
| // what a provider must not do once it has the seat. | ||
|
|
||
| /** Record this module as the error-screen owner. $force takes an occupied seat. */ | ||
| function xoops_recordErrorScreenOwner(string $dirname, bool $force = false): bool {} | ||
|
|
||
| /** The recorded owner's dirname, or '' when the seat is free. */ | ||
| function xoops_getRecordedErrorScreenOwner(): string {} | ||
|
|
||
| /** Release the seat, but only if $dirname currently holds it. */ | ||
| function xoops_releaseErrorScreenOwner(string $dirname): bool {} | ||
|
Comment on lines
+194
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
runtime_files="$(fd -t f '^debugconfig\.php$' . || true)"
if [[ -z "$runtime_files" ]]; then
echo "No debugconfig.php found."
exit 0
fi
while IFS= read -r file; do
rg -n -A4 -B2 'function xoops_(record|release)ErrorScreenOwner' "$file"
done <<< "$runtime_files"
rg -nP '\bxoops_(record|release)ErrorScreenOwner\s*\([^)]*\b(dirname|token)\s*:' \
--glob '*.php' . || trueRepository: XoopsModules27x/module-devops Length of output: 193 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
echo '--- stub declarations and nearby documentation ---'
sed -n '175,205p' stubs/xoops.stub
echo '--- local ownership API references ---'
rg -n -C2 'xoops_(record|release)ErrorScreenOwner|dirname:|token:' . \
--glob '*.php' --glob '*.stub' --glob '*.inc' --glob '*.md' || true
echo '--- XOOPS 2.7.3 runtime declaration ---'
url='https://raw.githubusercontent.com/XOOPS/XoopsCore27/master/htdocs/include/debugconfig.php'
curl -fsSL "$url" | rg -n -A5 -B3 \
'function xoops_(record|release)ErrorScreenOwner' || trueRepository: XoopsModules27x/module-devops Length of output: 3715 Match the runtime parameter names. XOOPS 2.7.3 declares 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| namespace Xmf\Module { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guidance names
blocks.phpas a conventional language-definition file, but the default allowlist omits it. Modules with scanned block code must therefore customize the list or retain avoidable unknown-constant errors and baseline entries.