Skip to content

Lay every plugin out like core: src/<Bucket>/<Class>.php - #34

Merged
mastacontrola merged 1 commit into
mainfrom
psr4-layout
Aug 31, 2026
Merged

Lay every plugin out like core: src/<Bucket>/<Class>.php#34
mastacontrola merged 1 commit into
mainfrom
psr4-layout

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

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 declared FOG\Plugins\LDAP\LDAPManager out of a file called class/ldapmanager.class.php, and core carried 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, no URL change and no RBAC churn. Only the PHP moves.

Decision and reasoning: ADR 0035.

What moved where

From To
class/*.class.php extending FOGController src/Items/
class/*.class.php extending FOGManagerController src/Managers/
class/*.class.php extending FOGBase src/Util/
capone/reg-task/caponetasking.class.php src/Util/CaponeTasking.php
pages/ 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 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 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.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 on a live server. 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 (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 a class/ 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 of docs/plugin-development.md is 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_VERSION pin moves in the same fogproject PR that teaches core the new layout.

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>
@mastacontrola
mastacontrola merged commit ec101b3 into main Aug 31, 2026
2 checks passed
@mastacontrola
mastacontrola deleted the psr4-layout branch August 31, 2026 15:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant