From 66dd785b33a32654fb0a7876e4921858ea401ca0 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Mon, 31 Aug 2026 07:44:42 -0500 Subject: [PATCH] Plugins declare their own namespace, and no longer alias themselves back Follows FOGProject/fogproject#1535 and FOGProject/fog-plugins#33, which put every plugin class into FOG\Plugins\ and taught core to resolve it. ## plugin-development.md Section 7a said "stay in the global namespace", and carried a subsection telling authors that a page, hook, event, report or task in a namespace of their own must end in class_alias(__NAMESPACE__ . '\Foo', 'Foo'). Both are withdrawn. A plugin class declares namespace FOG\Plugins\; -- one flat namespace per plugin, whatever subdirectory the file sits in -- and needs no alias, because core now reads each plugin file's own namespace declaration and maps the bare short name onto the FQCN itself. The alias rule was worth deleting rather than restating: a plugin page that got it wrong did not declare the class its filename promised, which is an uncaught TypeError out of FOGPageManager's constructor and a bodyless 500 on every page of the site. New subsections cover what replaced it -- discovery still works by filename, an unconverted plugin keeps loading unchanged, two plugins may now ship a class of the same name, and the one place a bare name still bites (a class name your own code holds in a plain string). The get_class($this) example is now a plugin class, since that advice is true of plugins too. ## 1.6/management/web/reports.md Found while checking that nothing else still recommended an alias, and it was stale for a second reason. The custom-report example declared `namespace FOG;` and `extends ReportManagement`, which resolves FOG\ReportManagement -- a name that stopped existing when core's pages moved into FOG\Pages, and which the autoloader now refuses outright rather than bridging. Copying the example gave a fatal, not a report. It is a global-namespace file extending \FOG\Pages\ReportManagement now, with no class_alias, plus a note on where a report shipped inside a plugin differs. Note translations/fr/development/plugin-development.md is now stale; left untouched rather than machine-translated. Co-Authored-By: Claude --- docs/1.6/management/web/reports.md | 16 ++- docs/development/plugin-development.md | 149 +++++++++++++++++-------- 2 files changed, 114 insertions(+), 51 deletions(-) diff --git a/docs/1.6/management/web/reports.md b/docs/1.6/management/web/reports.md index 3576262..6c09ac1 100644 --- a/docs/1.6/management/web/reports.md +++ b/docs/1.6/management/web/reports.md @@ -277,11 +277,10 @@ A minimal list report: ``` php /reports/example_report.report.php` declares `namespace +FOG\Plugins\;`. Nothing else about it changes, and it needs no +`class_alias()` either — see the plugin development guide. + The table is wired up in JavaScript, which for a plugin lives in the plugin's own `js/` directory and for a single uploaded file can be echoed inline from `file()`: diff --git a/docs/development/plugin-development.md b/docs/development/plugin-development.md index 8755b52..612292f 100644 --- a/docs/development/plugin-development.md +++ b/docs/development/plugin-development.md @@ -200,6 +200,8 @@ existed keeps working untouched. ### 4.2 Model — `class/helloworld.class.php` ```php +namespace FOG\Plugins\Helloworld; + class HelloWorld extends \FOG\Base\FOGController { protected $databaseTable = 'helloWorld'; @@ -222,6 +224,8 @@ The manager owns table creation and **schema evolution**. This is the most important part to get right, so it gets its own section (§5). The shape: ```php +namespace FOG\Plugins\Helloworld; + class HelloWorldManager extends \FOG\Base\FOGManagerController { public $tablename = 'helloWorld'; @@ -511,14 +515,26 @@ Global configuration lives in the `globalSettings` table. **Read this before you write a class.** Core moved to PSR-4 under `packages/web/src/` and is now reachable **only by its fully qualified name**. -Bare `FOGController`, `Host`, `Hook` no longer resolve to anything. +Bare `FOGController`, `Host`, `Hook` no longer resolve to anything. Plugins +have since followed core: every plugin class now declares its own namespace +too. ### The rule -**Stay in the global namespace. Reference core by its FQCN, with a leading -backslash.** That is what every one of the bundled plugins does: +**Declare `namespace FOG\Plugins\;` at the top of +every file, and reference core by its FQCN with a leading backslash.** The +namespace segment is `ucfirst()` of the plugin's directory name — mechanical, +no lookup table, no exceptions: `helloworld/` → `FOG\Plugins\Helloworld`, +`ldap/` → `FOG\Plugins\Ldap`. The **subdirectory is not part of it** — every +class in a plugin, whatever mix of `class/`, `pages/`, `hooks/`, `events/`, +`reports/` and `tasks/` it ships, shares that one flat namespace. +(`FOGController::getManager()` derives a model's manager class as +`qualify(shortName($this) . 'Manager')`, so a model and its manager have to +resolve in the same namespace or the derivation breaks.) ```php +namespace FOG\Plugins\Helloworld; + class HelloWorld extends \FOG\Base\FOGController {} class HelloWorldManager extends \FOG\Base\FOGManagerController {} class HelloWorldManagement extends \FOG\Base\FOGPage {} @@ -526,8 +542,9 @@ class AddHelloWorldMenuItem extends \FOG\Base\Hook {} class HelloWorldHeartbeat extends \FOG\Base\PluginTask {} ``` -The names are **bucketed**, matching the directory they live in — there is no -flat `FOG\Host`. The ones a plugin actually reaches for: +This is what every bundled plugin does now — copying one gets you the house +style. The names you reference on core stay **bucketed**, matching the +directory they live in under `src/` — there is no flat `FOG\Host`: | Bare name you used to write | Now | |---|---| @@ -544,42 +561,75 @@ flat `FOG\Host`. The ones a plugin actually reaches for: | `Host`, `Image`, `User`, `TaskType`, … | `\FOG\Items\` | | `HostManager`, `TaskTypeManager`, … | `\FOG\Managers\Manager` | -A `use` import at the top of a global-namespace file works too, and is the -better shape if you name a class many times: +A `use` import works too, and is the better shape if you name a class many +times — it sits below your own `namespace` declaration, not instead of it: ```php +namespace FOG\Plugins\Helloworld; + use FOG\Base\FOGController; class HelloWorld extends FOGController {} ``` -Both are correct. The FQCN form is what the bundled plugins use, so copying one -of them gets you the house style. - -### ⚠️ If you declare a namespace, you must alias yourself back - -The autoloader finds a **page, hook, event, report or task** by lowercasing the -filename and expecting a class of that exact name. Those files are discovered, -not imported — nothing ever writes their name in a `use` statement. So if you -put one in your own namespace, the class FOG looks for does not exist and your -page silently never registers. - -If you want a namespace, end each such file the way core's own `lib/pages/` -files do: - -```php -namespace Vendor\HelloWorld; - -class HelloWorldManagement extends \FOG\Base\FOGPage { /* ... */ } - -class_alias(__NAMESPACE__ . '\\HelloWorldManagement', 'HelloWorldManagement'); -``` - -Model and manager classes are reached through `FOGBase::getClass('HelloWorld')`, -which resolves by short name, so they have the same requirement. - -**The simplest correct answer is not to declare a namespace at all**, which is -why none of the bundled plugins do. +Both forms are correct. The FQCN form is what most of the bundled plugins use, +so copying one of them gets you the house style. + +### Discovery still works by filename + +File layout does not change, and neither does the rule about it: a **page, +hook, event, report or task** is still found by its filename, and the class +it declares must still be named after that file — `class +HelloWorldManagement` in `helloworldmanagement.page.php`, same as always. The +`.page.php` / `.hook.php` / `.event.php` / `.report.php` / `.task.php` suffix +is how the file gets found at all, so nothing moved and nothing was renamed. +This is namespacing, not a move to PSR-4 autoloading. + +What changed is what core does once it has found the bare name. It now reads +each plugin file's actual `namespace` declaration and maps the plugin's bare +short name to its namespaced FQCN, so discovery, `getClass('HelloWorld')`, +`FOGController::getManager()`, `FOGPage::$childClass`, `Route::_newEntity()` +and `Authorization`'s object-scope lookup all keep resolving the class from +the bare spelling you've always used. You never have to spell the namespace +out anywhere except the `namespace` declaration itself. + +**`class_alias()` is no longer needed, and should not be used.** It used to +be the workaround for exactly this gap: discovery derived a bare class name +from `basename($file)`, so a plugin that declared its own namespace had to +alias itself back into the global one or its page/hook/event/report silently +never registered. That gap is closed at the source now — core builds the +bare-name → FQCN map from the declaration itself — so there is nothing left +for an alias to do, and shipping one just adds a second, redundant name for +the same class. + +### Backwards compatible + +A plugin that declares **no** namespace keeps working exactly as it did +before this change. Core reads each plugin file's actual `namespace` +declaration rather than assuming every plugin has one now — an unconverted +third-party plugin produces no entry in the map and keeps resolving by its +bare name, in the global namespace, same as always. Nothing here requires a +third-party author to do anything before their plugin keeps loading. + +### Two plugins, one class name, no collision + +Because the namespace segment comes from the plugin's own directory, two +plugins can now each ship a class called `Settings` without one shadowing the +other. Before this, every plugin class shared one global basename keyspace — +whichever plugin's `Settings` loaded first silently won, and the other's was +unreachable. + +### The one place a bare name still bites + +Core resolves a bare name wherever *it* does the resolving: `getClass()`, +`getManager()`, discovery, `$childClass`, `Route::_newEntity()`, +`Authorization`. A class name **your own code** holds in a plain string is +resolved exactly as written, with no such mapping behind it. So +`self::getClass('LDAPGroupManager')` inside a plugin is fine — core resolves +it — but a raw `new $someString` or `is_subclass_of($x, 'SomeClass')` naming +a bare plugin class in your own code is not. Spell those fully qualified +(`\FOG\Plugins\Ldap\LDAPGroupManager`) or route them through +`self::getClass()` instead. ### What the failure looks like @@ -605,16 +655,17 @@ line is why, and this section is the fix. ### `get_class($this)` returns a namespaced name -Unchanged advice, and still the one thing that bites plugins which *produce* a -class name rather than consume one — comparing it to a literal, building a -column name or an array key from it, putting it in a filename or a log line: +Unchanged advice, and now true of plugin classes too — it bites whatever +*produces* a class name rather than consumes one: comparing it to a literal, +building a column name or an array key from it, putting it in a filename or a +log line: ```php -// Wrong: 'FOG\Items\Host', and the comparison silently fails. -if (get_class($obj) === 'Host') { /* ... */ } +// Wrong: 'FOG\Plugins\Helloworld\HelloWorld', and the comparison silently fails. +if (get_class($obj) === 'HelloWorld') { /* ... */ } -// Right: 'Host', namespaced or not. -if (self::shortName($obj) === 'Host') { /* ... */ } +// Right: 'HelloWorld', namespaced or not. +if (self::shortName($obj) === 'HelloWorld') { /* ... */ } ``` `FOGBase::shortName()` takes an object or a class-name string, strips any @@ -626,7 +677,10 @@ ADR 0013 originally kept a `class_alias()` in every core file re-exporting it into the global namespace, and called that alias the 1.6 plugin ABI. **All 202 were deleted before 1.6.0 shipped and the ADR is amended accordingly** — there was no released 1.6 for the promise to have been made to, and carrying the shim -through a major version bought compatibility with nothing. +through a major version bought compatibility with nothing. Plugins have now +followed the same path: the `class_alias()` a plugin author would once have +had to write to stay reachable is gone too, for the same reason — core +resolves the namespaced class directly instead of leaning on a shim to do it. ## 7b. Composer dependencies @@ -959,11 +1013,12 @@ Fire your own events with `&`-by-reference args so listeners can mutate them - **Core is FQCN-only.** `extends FOGController` is a fatal error, not a deprecation — see §7a. The autoloader logs one line naming the class and the name to use before the request dies, so check the error log first. -- **A namespace of your own means aliasing yourself back.** Pages, hooks, - events, reports and tasks are found by filename and must declare exactly that - class name in the global namespace. Put one in a namespace without a - `class_alias()` and it never registers — no error, the feature simply is not - there. §7a has the shape. Not declaring a namespace avoids the whole question. +- **Every plugin class declares `namespace FOG\Plugins\;`.** + One flat namespace per plugin, whatever subdirectory the file lives in — a + model and its manager must resolve in the same one. No `class_alias()` + needed, not even for a page/hook/event/report/task: core maps the bare name + to the namespaced FQCN for you. §7a has the shape. A plugin with no + namespace declared keeps working unchanged. - **Filename = `strtolower(ClassName)` + suffix.** A mismatch means the class won't autoload. Silently, for most classes — but not for your manager: install refuses outright if `class/manager.class.php` exists and does