Lay every plugin out like core: src/<Bucket>/<Class>.php - #34
Merged
Conversation
Namespacing left the plugins half-migrated. Every class moved into FOG\Plugins\<Plugin>, but the files stayed where they were, so a plugin declared FOG\Plugins\LDAP\LDAPManager out of a file called class/ldapmanager.class.php and core needed a scan-and-cache mechanism -- 176 file_get_contents on a cold cache -- to reconcile the two. The layout now follows the namespace. A plugin is laid out exactly like core and discovered by exactly the mechanism core uses on itself: <plugin>/src/<Bucket>/<Class>.php declares FOG\Plugins\<Segment>\<Bucket>\<Class> with the file name equal to the class name and strtolower(<Segment>) equal to the plugin's directory name. The directory does not move: it is plugins.pName, the ?node= value, the <node>.view permission string and the js/ URL, so there is no schema migration and no RBAC churn. class/ splits three ways by what a class extends -- FOGController to src/Items/, FOGManagerController to src/Managers/, plain FOGBase helpers to src/Util/. pages/ hooks/ events/ reports/ tasks/ reg-task/ map straight across. Report classes keep their underscored names, as core's own do, because the menu key, the base64 `f` parameter and the permission node are all derived from that name. Every move is a git mv, so per-file history follows. Gates: - tests/plugin-layout.test.php replaces plugins-are-namespaced.test.php. It holds all three rules -- file name equals class name including case, path equals namespace, segment lowercases to the directory -- plus a floor, no class_alias() and no leftover pre-1.6 directory. The test it replaces matched on the six discovery suffixes and had no minimum, so after this move it would have matched zero files and exited 0. - tests/references-resolve.test.php is new, and exists because core-references-are-qualified.test.php cannot see this failure. Bucketing means `class NtfyHandler extends Ntfy` now spans two namespaces: the bare name still names a class this tree declares, so the older gate stays green, php -l is happy, and the fatal arrives mid-request. This asks the question PHP asks -- given this file's namespace and its use imports, does the name resolve? All three layout rules and the resolution gate were mutation-verified. Requires fogproject core that understands this layout; older core and these plugins is a server with no working plugins, which is why the FOG_PLUGINS_VERSION pin moves in the same release. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Aug 31, 2026
mastacontrola
added a commit
to FOGProject/fog-docs
that referenced
this pull request
Aug 31, 2026
Mirrors FOGProject/fogproject#1540 and FOGProject/fog-plugins#34. A plugin's PHP moved out of class/ pages/ hooks/ events/ reports/ tasks/ and into src/<Bucket>/<Class>.php, declaring FOG\Plugins\<Segment>\<Bucket>\<Class> -- the same PSR-4 arrangement core already used on itself. The plugin DIRECTORY does not move: it is still the routing node, the permission string and the js/ path. plugin-development.md is re-ported from the fogproject copy, so it carries the rewritten §2, §3, §4.2-4.5, §7a, §9a, §10 and §11, loses the "Discovery still works by filename" subsection that taught the rule this reverses, and gains §11b -- the port table for a plugin still on the pre-1.6 layout. plugin-schema-migrations.md's Key files table still named lib/fog/*.class.php paths that moved when core itself went PSR-4; the reference implementation is now lib/plugins/location/src/Managers/LocationManager.php. reports.md is the user-facing half and was teaching a path that no longer exists. A report is packages/web/src/Reports/<Class>.php, or <plugin>/src/Reports/<Class>.php inside a plugin, and the underscores in Fleet_Report are load bearing -- the menu label, the base64 `f` parameter and the permission node are all derived from that name. Two adjacent references corrected while here: the storage-node hook guide pointed at lib/fog/storagegroup.class.php and told authors to write a *.hook.php in lib/hooks/, and the CSV guide named the Location plugin's hook by its old filename. Translations are left alone. scripts/translate.mjs regenerates a page when its source hash moves, so the French copies refresh themselves once a provider is configured. 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.
Plugins were left half-migrated by the namespacing work. Every class went into
FOG\Plugins\<Plugin>, but the files stayed where they were — so a plugin declaredFOG\Plugins\LDAP\LDAPManagerout of a file calledclass/ldapmanager.class.php, and core carried a scan-and-cache mechanism (176file_get_contentson a cold cache) to reconcile the two.The layout now follows the namespace. A plugin is laid out exactly like core, and discovered by exactly the mechanism core uses on itself:
with the file name equal to the class name and
strtolower(<Segment>)equal to the plugin's directory name.The directory does not move. It is
plugins.pName, the?node=value, the<node>.viewpermission string and thejs/URL — so there is no schema migration, no URL change and no RBAC churn. Only the PHP moves.Decision and reasoning: ADR 0035.
What moved where
class/*.class.phpextendingFOGControllersrc/Items/class/*.class.phpextendingFOGManagerControllersrc/Managers/class/*.class.phpextendingFOGBasesrc/Util/capone/reg-task/caponetasking.class.phpsrc/Util/CaponeTasking.phppages/ hooks/ events/ reports/ tasks/src/Pages/ Hooks/ Events/ Reports/ Tasks/class/splits three ways because it used to hold everything; which bucket a file belongs in is decided by what its class extends. Report classes keep their underscored names (LDAP_Report), exactly as core names its own, because the menu key, the base64fparameter and the permission node are all derived from that name.Every move is a
git mv, so per-file history follows.Gates
tests/plugin-layout.test.phpreplacesplugins-are-namespaced.test.php. It holds all three rules — file name equals class name including case, path equals namespace, segment lowercases to the directory — plus a floor, noclass_alias(), and no leftover pre-1.6 directory. The test it replaces matched on the six discovery suffixes and had no minimum count, so after this move it would have matched zero files, printed "ok: 0 plugin class file(s)" and exited 0.tests/references-resolve.test.phpis new, and exists becausecore-references-are-qualified.test.phpcannot see this failure. Bucketing meansclass NtfyHandler extends Ntfynow spans two namespaces: the bare name still names a class this tree declares, so the older gate stays green,php -lis happy, and the fatal arrives mid-request on a live server. This asks the question PHP asks — given this file's namespace and itsuseimports, does the name resolve?All three layout rules and the resolution gate were mutation-verified (each made to fail before being trusted). Full suite: 15/15 green.
Third-party plugins
This breaks any plugin written against the pre-1.6 layout, and that was accepted deliberately as the price of one consistent way to write FOG code. The failure is loud rather than silent: a plugin directory with no
src/but with aclass/gets one line in the log naming itself and the guide. Porting is a file move and a namespace line — no logic changes — and §11b ofdocs/plugin-development.mdis the port table.Ordering
Old core with these plugins, or new core with the old plugins, is each a server with no working plugins. So this merges and tags v1.6.23 first, and the
FOG_PLUGINS_VERSIONpin moves in the same fogproject PR that teaches core the new layout.