A static flag does stop a second call to a setter from overwriting a shared default viewer. It is a workable guard for a strict “set once per process” policy, but it does not solve the problem that usually prompts it: how a controller gets its viewer when some controllers are created by a dispatcher and others, such as an error controller, are resolved along a different path. In most applications, an explicit dependency or container-managed wiring is the sounder design, and the flag only earns its place when one process-wide viewer is a real requirement.
Why the error controller misses the setup
In a SitePoint forum thread from late September 2026, a developer described a common routing setup. The dispatcher creates routed controllers and calls setViewer() on each one before running the action. An error controller, however, is resolved through an error handler rather than the dispatcher, so that setup step never runs for it. The error page is the one place that most needs a working viewer, and it is the place that gets none.
As an Amazon Associate I earn from qualifying purchases.
The workaround under discussion moves a default viewer onto the base controller and protects its setter with a static local variable, so that bootstrap code can set the default once and every controller can fall back to it. The pattern looks like this:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
abstract class BaseController
{
protected ?ViewerInterface $viewer = null;
private static ?ViewerInterface $default_viewer = null;
public static function setDefaultViewer(ViewerInterface $viewer): void
{
static $viewerSet = false;
if ($viewerSet) {
return;
}
$viewerSet = true;
self::$default_viewer = $viewer;
}
public function viewer(): ViewerInterface
{
return $this->viewer ?? self::$default_viewer;
}
}
What the flag guards and what it leaves open
The flag and the viewer are separate pieces of state, and it helps to keep them apart when reasoning about the code.
#1 Best Overall
- The flag protects the setter, not the dependency.
$viewerSetonly controls whether the assignment runs again. Controllers still readself::$default_viewerdirectly, so the dependency remains invisible to anyone reading a constructor. - A second call fails silently. The guard returns without an error, warning, or log entry. Whichever code path runs first decides the viewer for the rest of the process, even if a later caller meant to change it.
- Static state persists across tests. Unless a test suite resets the property, a viewer set in one test is visible to every test that runs afterwards in the same PHP process.
- Inheritance shares the flag. Since PHP 8.1, a method that is inherited without being overridden shares its static variables with the parent method. Child classes that call the inherited setter therefore share one flag, which fits a single process-wide default but would surprise anyone who expected per-class configuration.
- Order matters if the setter grows. The flag is set before the property is assigned. For a plain assignment this does not matter in practice, but if the setter later performs work that can fail, the flag would record a configuration that never completed. Assign first, then set the flag, once the value is known to be valid.
The uninitialized default
If neither an instance viewer nor the static default has been set, viewer() evaluates to null. Because the method declares a non-nullable return type, PHP raises a TypeError at the point of return. That failure happens when a page is rendered, which may be far from the bootstrap code that forgot to call the setter. A safer version checks for a missing viewer and throws an exception that names the setter, so the misconfiguration surfaces at the call site with a clear message.
Three ways to wire the viewer
The real choice is not between a flag and no flag. It is about where the dependency is declared and which code paths the wiring covers.
Rank #2
| Approach | Where the dependency appears | Coverage of resolution paths | Main cost | Fits when |
|---|---|---|---|---|
| Static setter with a one-time flag | Hidden static state on the base class | Only where bootstrap or the dispatcher calls the setter | Silent second call; shared state persists across tests | One viewer for the whole process, and every entry point runs the same bootstrap |
| Constructor injection | Constructor signature | Every object the container builds | Explicit plumbing, including child constructors that forward base-class dependencies | Controllers genuinely need a viewer to do their job |
| Container resolving callback | Container configuration | Every resolution the container performs | Matching rules, caching, and recursion must be defined carefully | The container creates the objects and the same setter wiring applies to all of them |
Constructor injection
Constructor injection makes the dependency part of the class’s contract. You register an implementation for ViewerInterface with the container, typically a binding in a service provider in Laravel, and the container supplies it wherever a controller is built, including the error controller. The cost is plumbing. The base controller takes the viewer as a constructor argument, and each child constructor must pass it up:
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →abstract class BaseController
{
public function __construct(protected ViewerInterface $viewer) {}
}
final class ErrorController extends BaseController
{
public function __construct(ViewerInterface $viewer, private Logger $logger)
{
parent::__construct($viewer);
}
}
When many controllers share a base-class dependency, this forwarding can become repetitive. That repetition is often a hint that the viewer is not really a controller concern, and a dedicated renderer service may describe the design more accurately.
Container resolving callbacks
The poster in the forum thread eventually replaced the static setter with a callback that runs after the container resolves an object. The callback checks whether the object matches a configured class and then calls the setter on it, which reaches objects built through the error handler as well as those built by the dispatcher. Laravel’s service container documentation describes resolving callbacks, and Symfony’s dependency-injection documentation describes configured method calls, so both frameworks support this general technique. The exact APIs and lifecycle rules differ between them, and the poster’s custom container is not the same thing.
The callback approach raises questions that the thread did not settle, and any implementation has to answer them explicitly:
Rank #4
- Does the match cover a class, its subclasses, or an interface it implements?
- Does the callback run for instances that the container returns from a cache, or only for newly built ones?
- If the callback resolves other objects, how is recursion prevented?
The poster reported that the approach worked in early tests with their own container. That is one developer’s report, not an independent evaluation of the implementation.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →A forum participant, m_hutley, asked on September 23, 2026 at 12:09pm: “do you REALLY want ‘do it once’, or do you actually want ‘do it when its needed'”. That question is the most useful test of the whole design, and it is a design question rather than an official recommendation from PHP or a framework.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Choosing the scope
Work through these steps before deciding whether the flag belongs in the code.
- List the controllers that actually render templates. If only some of them do, do not require a viewer on every controller just to simplify dispatcher setup. Inject it into the ones that need it.
- Test the “once per process” requirement. Check whether tests, API endpoints, or different request contexts ever need a different viewer. If they do, a process-wide default is the wrong shape.
- If one viewer is genuinely shared, keep the guard but make it loud. Throw an exception when a second, different viewer is passed, and reset the static state in test setup.
- If the controller truly depends on a viewer, inject it. Bind the interface in the container and accept the constructor plumbing. Move repeated base-class forwarding into a renderer service if it grows.
- If the container builds every controller and the same setter wiring applies to all of them, use a resolving callback. Document the matching rules, caching behaviour, and recursion guard in the container’s own code, not only in the callback.
Across all five steps, the question is the same: does the viewer belong to the controller’s contract, or is it a process setting that the container should apply? The answer decides whether the static flag is a sensible guard or a way of hiding a dependency that should be visible.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




