Bucket the last 52 flat-namespace classes into src/ - #1528
Merged
Conversation
Pages, hooks, reports and events were the only core classes left in a flat
`namespace FOG;`, sitting under lib/ with a class_alias trailer each. They are
PSR-4 files under src/{Pages,Hooks,Reports,Events} now, declaring
FOG\Pages\HostManagement, FOG\Hooks\BootItem, FOG\Reports\Audit_Report and
FOG\Events\HostList, with the 52 aliases deleted. There is no flat FOG\
namespace in core any more, and every lib/ subdirectory except router/ and
plugins/ is empty of code.
ADR 0013 said these could not move because "PSR-4 does not do discovery".
That was true and was not the constraint. The constraint was that the three
discovery sites derived a BARE class name from basename($file) and resolved it
from the global namespace, so the files needed an alias -- and the alias was
what the rest of the migration was retiring. Discovery had to learn a second
file shape first; it had not been taught one. That is a property of the
loader, not of PSR-4.
So each site now reads two sources and merges them: FOGBase::coreitems($bucket)
for core, and the existing fileitems() walk for plugins, which keep the
<plugin>/<dir>/<name>.<type>.php shape and the global namespace (ADR 0009).
coreitems() filters Initiator::srcFileList(), already built and already cached,
so it adds no walk to a request. FOGBase::classFromDiscoveredFile() then
derives the name -- stripping the discovery extension when the file carries one
and .php otherwise -- and hands it to qualify(), which spans both shapes with
no branch on provenance: it maps a name src/ declares onto its FQCN and passes
anything else through untouched.
startClassFromFiles() takes the extension rather than a negative string length,
since it can no longer assume one shape.
Reports needed one deliberate save. A report's filename was lowercase and three
things read it as such: the menu label, the base64 `f` URL parameter, and the
keys of Authorization::REPORT_NODES, which is a permission gate. The PSR-4
filename is Audit_Report.php, so loadCustomReports() now lowercases what it
used to get lowercased for free. Without that call the same report answers to a
different URL and a different permission node than before -- a silent
authorization change, not a cosmetic one.
Verified by booting a pre-move and a post-move tree against the live database
and diffing what discovery actually found: report labels, page registration,
core hook and event resolution, and both listener registries are
byte-identical. Mutating coreitems() to return [] takes reports 17 -> 4 and
pages 1 -> 0, so the probe bites. That mutation also exposed a hole in the
first version of it: every core hook ships $active = false, so the listener
registry is identical whether core hook discovery works or finds nothing, and
the probe now resolves those classes directly.
bin/psr4-scan.php buckets the four kinds by ancestry rather than 52 table rows,
ReportManagement before FOGPage since a report's chain reaches both. The
phpstan baselines were repointed by hand -- 190 paths and 81 message FQCNs --
rather than regenerated, which would have swept up unrelated drift.
tests/psr4-bridge.test.php gains a second probe file for the bridge's
case-insensitive prefix. The assertion it replaces, in autoload.test.php, was a
fake gate: it ran after the class had already been loaded, so PHP's own
case-insensitive class table answered and the bridge was never consulted.
Mutating strncasecmp to strncmp did not fail it. The new one does.
Out-of-tree plugins spelling \FOG\ReportManagement need
\FOG\Pages\ReportManagement; fog-plugins carries that change for the eight
bundled reports. The bridge refuses the flat spelling with an error naming the
correct FQCN rather than resolving it, so the failure is legible.
Co-Authored-By: Claude <noreply@anthropic.com>
mastacontrola
force-pushed
the
namespace-discovery-classes
branch
from
August 30, 2026 21:17
f54301f to
dfca303
Compare
osiktech
pushed a commit
to osiktech/fogproject
that referenced
this pull request
Sep 9, 2026
FOGProjectGH-1528 moved the last 52 flat-namespace core classes into src/{Pages,Hooks,Reports,Events}, so \FOG\ReportManagement became \FOG\Pages\ReportManagement. Eight bundled plugin reports extend it and fatal with "Class FOG\ReportManagement not found" the moment someone opens one -- the autoloader refuses the flat spelling and logs the correct FQCN rather than resolving it, so the failure is legible, but the report page is gone. fog-plugins v1.6.21 carries the eight edits. Without this bump an install still fetches v1.6.20 against a core that cannot load it, so the fix exists and does not reach anybody: bin/fetch-plugins.sh reads the pinned tag straight out of this constant. Co-Authored-By: Claude <noreply@anthropic.com>
osiktech
pushed a commit
to osiktech/fogproject
that referenced
this pull request
Sep 9, 2026
configureHttpd()'s retirement sweep matched *.class.php only. FOGProjectGH-1528 retired 52 more files the same way -- every core page, hook, report and event moved from lib/{pages,hooks,reports,events}/<lowercase>.<type>.php to src/<Bucket>/<Class>.php -- so on FOG_copy_back_old=yes all 52 survive the upgrade. They are not the harmless case the existing comment describes. A stale *.class.php is inert because autoload() answers a bare name out of src/ first. A stale *.report.php is FOUND: ReportManagement::loadCustomReports() merges core's src/Reports with the fileitems() walk that picks up plugin reports, and that walk reaches lib/reports/ too. Every core report is then discovered twice and the Reports menu renders each of them twice. Reproduced on a live 1.6 install rather than argued: restoring the 52 pre-FOGProjectGH-1528 files into a deployed webroot took the discovered report list from 17 entries to 30, 13 of them duplicates. Page and report rendering stayed green throughout, which is why this needed the discovery probe to see at all. The keep-if-still-shipped test is unchanged and is what keeps the wider match safe -- lib/router/ still ships its .class.php files and they are matched, tested and kept exactly as before. tests/oldcopy-retires-moved-classes.test.sh gains three checks: a retired file of each of the four kinds is removed, a discovery-extension file the release still ships is spared, and a bundled plugin's report one level deeper is left to the fog-plugins release. Both mutations fail it -- narrowing the find back to *.class.php, and dropping the keep test, which is the obvious wrong fix. Its extractor also had to change. It grabbed the loop out of functions.sh and stopped at the first `done < <(find ` line; the find is a multi-line -o list now, so that terminator would cut the snippet mid-command. It stops on -print0 instead. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pages, hooks, reports and events were the only core classes left in a flat
namespace FOG;, sitting underlib/with aclass_aliastrailer each. They arePSR-4 files under
src/{Pages,Hooks,Reports,Events}now, declaringFOG\Pages\HostManagement,FOG\Hooks\BootItem,FOG\Reports\Audit_ReportandFOG\Events\HostList, with the 52 aliases deleted. There is no flatFOG\namespace in core any more, and every
lib/subdirectory exceptrouter/andplugins/is empty of code.Why ADR 0013 said this was impossible, and why it wasn't
ADR 0013 recorded that these could not move because "PSR-4 does not do discovery".
That was true and was not the constraint. The constraint was that the three
discovery sites derived a bare class name from
basename($file)and resolved itfrom the global namespace, so the files needed an alias — and the alias was exactly
what the rest of the migration was retiring. Discovery had to learn a second file
shape first; nobody had taught it one. That is a property of the loader, not of
PSR-4. The ADR carries the correction.
How discovery works now
Each site reads two sources and merges them:
FOGBase::coreitems($bucket)for core,and the existing
fileitems()walk for plugins, which keep the<plugin>/<dir>/<name>.<type>.phpshape and the global namespace (ADR 0009).coreitems()filtersInitiator::srcFileList(), which is already built and alreadycached, so it adds no directory walk to a request.
FOGBase::classFromDiscoveredFile()then derives the name — stripping the discovery extension when the file carries one
and
.phpotherwise — and hands it toqualify(), which spans both shapes with nobranch on provenance: it maps a name
src/declares onto its FQCN and passes anythingelse through untouched.
startClassFromFiles()takes the extension rather than a negative string length, sinceit can no longer assume one shape.
The one thing that was nearly an authorization change
A report's filename used to be lowercase, and three things read it as such: the menu
label, the base64
fURL parameter, and the keys ofAuthorization::REPORT_NODES—which is a permission gate. The PSR-4 filename is
Audit_Report.php, soloadCustomReports()now lowercases what it used to get lowercased for free. Withoutthat one call the same report answers to a different URL and a different permission
node than it did before. That is a silent authorization change, not a cosmetic one,
and
tests/usertracking-permission-split.test.phpnow pins the real transform ratherthan a hardcoded list.
Verification
Booted a pre-move and a post-move tree against the live database and diffed what
discovery actually found — report labels, page registration, core hook and event
resolution, and both listener registries. Byte-identical.
The probe was mutation-tested, not assumed: making
coreitems()return[]takesreports 17 → 4 and pages 1 → 0. That same mutation exposed a hole in the first version
of the probe — every core hook ships
$active = false, so the listener registry isidentical whether core hook discovery works or finds nothing — and the probe now
resolves those classes directly.
[OK] No errors.tests/psr4-bridge.test.phpgains a second probe file for the bridge'scase-insensitive prefix. The assertion it replaces, in
autoload.test.php, was a fakegate: it ran after the class had already been loaded, so PHP's own case-insensitive
class table answered and the bridge was never consulted. Mutating
strncasecmptostrncmpdid not fail it. The new one does.tests/stale-class-file-list.test.phphad a real bug of the same family — it passed-strlen('.hook.php')where the parameter is now the extension itself.Notes for reviewers
path:lines and 81 messageFQCNs — rather than regenerated, which would have swept up unrelated drift. One
pre-existing stale entry for
inventory_report.report.phpis deliberately left alone.bin/psr4-scan.phpbuckets the four kinds by ancestry rather than 52 table rows.The rule order matters:
ReportManagementbeforeFOGPage, since a report's chainreaches both.
use FOG\Pages\ReportManagement;. Theyextends ReportManagementunqualified, which used to resolve through the bridge.Downstream — this needs FOGProject/fog-plugins#32
Merge this first, then the plugins PR, then cut a fog-plugins release.
Out-of-tree plugins spelling
\FOG\ReportManagementneed\FOG\Pages\ReportManagement.Eight bundled reports in
fog-pluginsdo exactly that, and they fatal the moment someoneopens the report once this lands. The bridge refuses the flat spelling with an error
naming the correct FQCN rather than resolving it, so the failure is legible — but it is
still a failure.
Note that
fog-plugins' owntests/core-references-are-qualified.test.phpdoes notcatch this and cannot: it is a syntax gate asking whether a class reference carries a
leading backslash, not whether the name resolves. It stayed green through the whole
breakage.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VTEiBnDpNrgzu1tSqt1318