You are writing a class autoloader for a PHP plugin system without Composer; how should it behave within the spl_autoload_register() stack?
answer
- one callback per plugin prefix
- return silently for foreign names
- $prepend = true jumps the queue
- $throw is ignored since PHP 8.0
- a thrown exception stops the queue
basics
~20 sRegister one callback per plugin that handles only its namespace prefix, maps names to files under the plugin directory, requires the file only if it exists and returns silently otherwise, so other autoloaders, including Composer's, still run.
solid answer
~40 sEach plugin gets a callback that **claims only its own namespace prefix**, for example `Plugins\Gallery\`, maps the rest of the name to a file under that plugin's `src/` directory, and `require`s the file only if `is_file()` confirms it. For any other name it **returns silently**: autoloaders form a queue, and throwing an exception would stop the remaining callbacks and turn `class_exists()` checks into failures. `spl_autoload_register($cb, true, $prepend)` appends by default; `$prepend = true` puts the callback first, which is only needed when a plugin must win over a general loader. The `$throw` argument has been ignored since PHP 8.0: invalid callbacks always throw `TypeError`, and passing `false` only raises a notice. Unloading a plugin means `spl_autoload_unregister()` with the same callable, which is easiest when the plugin keeps a reference to its closure.
code
php · 18 lines<?php
declare(strict_types=1);
$pluginLoader = static function (string $class): void {
$prefix = 'Plugins\\Gallery\\';
if (!str_starts_with($class, $prefix)) {
return; // not ours: stay silent
}
$file = __DIR__ . '/plugins/gallery/src/'
. str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
if (is_file($file)) {
require $file;
}
};
spl_autoload_register($pluginLoader); // appended after the host loader
var_dump(count(spl_autoload_functions()) >= 1); // bool(true)
spl_autoload_unregister($pluginLoader); // plugin disabledgo deeper
Know that several autoloaders can be registered and that each should only load classes from its own namespace.
Explain queue order, the $prepend flag, why returning silently matters, and that $throw is ignored since PHP 8.0.
Build a prefix table with PSR-4 mapping, keep loaders cheap on misses, validate externally supplied class names and support unregistering disabled plugins.
Decide whether plugins may bring their own dependencies, since shared namespaces across plugins create version conflicts no autoloader can resolve.
## The scenario A self-hosted application loads plugins from `plugins/<name>/`, each with its classes in `plugins/<name>/src/` under a namespace such as `Plugins\Gallery\`. There is no Composer in the plugin workflow: plugins are dropped into a directory. The host must make each plugin's classes autoloadable without interfering with the host's own autoloader or with other plugins. ## How the stack behaves PHP keeps a per-request **queue of autoloaders**. For an undefined class it calls each callback in order with the fully qualified class name and stops as soon as the class exists. The details that matter here: - **Order.** Callbacks run in registration order. `spl_autoload_register($cb, true, true)`, with the third argument `$prepend` set to `true`, puts the new callback at the **front** of the queue. - **Duplicates.** Registering the same callable again is ignored. - **Exceptions.** If a callback throws, the lookup stops immediately and the exception propagates from whatever triggered the load, such as `new` or `class_exists()`. - **`$throw`.** Since PHP 8.0 the second parameter is ignored: `spl_autoload_register()` always throws `TypeError` for an invalid callback, and passing `false` only emits a notice, "Argument #2 ($do_throw) has been ignored". - **Inspection.** `spl_autoload_functions()` returns the queue, and `spl_autoload_unregister($cb)` removes a callback. | Parameter | Default | Effect in PHP 8.5 | |---|---|---| | `$callback` | `null` | the loader; `null` registers the built-in `spl_autoload()` | | `$throw` | `true` | ignored; `false` only triggers a notice | | `$prepend` | `false` | `true` puts the callback at the head of the queue | ## Rules for a well-behaved plugin autoloader 1. **Claim a prefix.** Handle only names starting with the plugin's namespace prefix. Everything else belongs to someone else. 2. **Map with PSR-4.** Strip the prefix, turn namespace separators into `/`, append `.php`, and resolve under the plugin's `src/` directory using an absolute path built from the plugin root. 3. **Check before requiring.** `is_file($path)` first; a missing file is not an error, just a miss. 4. **Return silently.** No exceptions, no warnings, no return value. PSR-4 states exactly this for autoloaders. 5. **Append by default.** Prepending should be reserved for a deliberate override, because a prepended loader runs first for **every** class that needs autoloading in the request, including the host's own classes. 6. **Keep the callable.** Store the closure so the host can unregister it when a plugin is disabled. ```php <?php declare(strict_types=1); final class PluginAutoloader { /** @var array<string, string> prefix => base directory */ private array $roots = []; public function add(string $prefix, string $dir): void { $this->roots[rtrim($prefix, '\\') . '\\'] = rtrim($dir, '/') . '/'; } public function load(string $class): void { foreach ($this->roots as $prefix => $dir) { if (str_starts_with($class, $prefix)) { $file = $dir . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php'; if (is_file($file)) { require $file; } return; } } } } $loader = new PluginAutoloader(); $loader->add('Plugins\Gallery', __DIR__ . '/plugins/gallery/src'); spl_autoload_register($loader->load(...)); ``` A single registered method that consults a prefix table scales better than one closure per plugin, because every miss costs one callback call instead of one per plugin. ## Pitfalls specific to plugins - **Overlapping prefixes.** Two plugins claiming the same prefix load whichever file is found first. Check the longest prefix first, or reject overlaps at install time. - **Class names from outside.** Plugin manifests or requests sometimes name classes to instantiate. Validate those names against the plugin's prefix before passing them to `new` or `class_exists()`, so the autoloader cannot be steered to unexpected files. - **Ordering assumptions.** A host loader registered after a greedy plugin loader that throws on misses will never run for the host's classes; that is the failure mode the "return silently" rule prevents. - **Duplicated libraries.** Two plugins shipping their own copy of the same library under the same namespace compete for one class name: whichever loads first wins for the whole request. ## Why not require everything up front? Requiring every plugin file at boot costs time on every request and loads code paths the request never uses. The autoloader loads exactly the classes a request touches, and with OPcache the repeated compilation cost is avoided as well.
- In PHP, when would you pass $prepend = true to spl_autoload_register()?When a loader must be consulted before the existing ones, for example to substitute a patched copy of a class, or when a fast map-based loader should answer before a slower directory-scanning one. The cost is that it runs first for every lookup, so it must reject foreign names cheaply, with a prefix check before any file access.
- In PHP, what happens to the other autoloaders if one callback throws an exception for an unknown class?The engine stops walking the queue as soon as a callback leaves an exception pending, so later autoloaders never run for that lookup, and the exception propagates from the triggering operation. A `class_exists()` check that should have returned `false` now throws instead, which is why PSR-4 forbids autoloaders from throwing.
- In PHP 8.5, what does spl_autoload_register($cb, false) do differently from spl_autoload_register($cb)?Nothing useful. Since PHP 8.0 the `$throw` argument is ignored: registration errors always throw `TypeError`. Passing `false` only emits a notice that the argument was ignored, so modern code leaves it at its default and passes `true` only as a placeholder when it needs `$prepend`.
saying these in an interview costs you the question
- A plugin autoloader should throw when it cannot find a class
- Passing false as $throw makes registration errors silent in PHP 8
- Prepending every plugin loader is harmless because it only runs for plugin classes
- The autoloader's return value decides whether later autoloaders run
- Each plugin must call require_once on all its classes at boot