9: Instance Management
Phlo uses its own instance manager to initialize and reuse objects efficiently and predictably. This system determines when controller code runs, how instances are stored, and how circular references are prevented.
9.1: The basics
When you define a .phlo file, the build phase turns it into a class.
Every call to an object via %name goes through the instance manager (phlo() in /phlo/phlo.php).
Example:
prop title = 'Welcome'
route GET home => $this->main
method main => view($this->home)
view home:
<h1>$this->title</h1>
When the route /home is requested:
- The instance manager checks, via the identity that
__handle()(or the default rule) derives from the arguments, whether a matching instance already exists. - If not, it is created and stored.
- After creation, the controller code runs (see §8.2).
- Then the requested method is called.
9.2: Controller code
All code in a .phlo file that does not belong to route, prop, static, method, function, view, <style> or <script> is controller code.
This code runs after instantiation, once the instance fully exists.
Example:
prop ready = false
%session->start()
$this->ready = true
The last two lines are controller code because they sit at top level.
- This code runs after construction, each time the manager creates a new instance: once for a shared instance, on every call for classes whose
__handle()returnsnull. - The difference from
__constructis that the instance fully exists by the time controller code runs, which prevents circular references and incomplete objects.
9.3: The role of `__handle()`: instance identity
__handle() is an optional static you define yourself to control the identity of instances. It is interwoven with __construct: the transpiler grafts your constructor's parameter list onto it, so the handle expression speaks in constructor arguments, and the instance manager calls it with the same arguments it is about to construct with:
static __handle => "img/$file"
method __construct(public string $file)
The return value decides how the manager treats the instance:
__handle() returns |
Meaning |
|---|---|
| a string | The registry key: every call producing the same key returns the same shared instance (multiton by argument). %img for the same file is one object; a flag can be part of the identity too, as in "INI/$path$filename".(!$parse ? '/0' : void). |
null |
Never cached: every call constructs a fresh instance (cookiewall, field). |
true |
Reuse the instance registered under the class name and objImport(...) the fresh arguments into it. |
A class without __handle() follows the default rule: called without arguments it is a singleton keyed by its name; called with arguments it is a fresh, unregistered instance.
You define __handle(); the instance manager calls it. Never call it yourself.
9.4: Lazy initialization
Because controller code runs only after construction, instances can reference each other without triggering unwanted recursive creation.
Example:
a.phlo:
prop message = 'A ready'
b.phlo:
prop message = 'B ready'
main.phlo:
route GET test => $this->show
method show {
dx(%a->message, %b->message)
}
%aand%bare created lazily.- The controller code in both files runs once their instance fully exists.
- You can reference instances from each other freely, because the instance already exists before its controller code runs.
9.5: The obj base class: powertools
Every transpiled class extends obj, and obj is more than __get/__set. These are the tools you reach for when a class needs to behave dynamically.
The ad-hoc value object. obj is also the everyday container you create directly: obj(x: 1, y: 2) gives you a live object in one call, no class definition needed. Reading a key that does not exist returns null instead of a warning, so optional data needs no isset dance. Assign a closure and it becomes a bound member: after $point->sum = fn() => $this->x + $this->y, both $point->sum and $point->sum() evaluate it with $this bound to the object. foreach iterates the stored data and json_encode($point) serializes exactly that data (closures and computed values stay out), so an obj passes cleanly into views, payloads and JSON responses.
$point = obj(x: 1, y: 2)
$point->sum = fn() => $this->x + $this->y
$total = $point->sum
Interception hooks. Implement objCall, objGet or objSet to trap the access chain. Returning null falls through to the normal behavior; anything non-null short-circuits:
method objGet($key) => $this->cache[$key] ?? null
method objCall($method, ...$args) => str_starts_with($method, 'find') ? $this->finder($method, $args) : null
method objSet($key, $value) => $key === 'id' ? true : null
objGet runs before data/closure/method/prop lookup on every read, objCall on every unknown method call, and objSet before every write (a non-null return swallows the write). This is the mechanism behind decorators, lazy loading and read-only guards.
Bound closures. Assign a closure and it binds to the instance: $obj->greet = fn() => "Hi $this->name", later $obj->greet() runs with $this bound. Handy for per-instance behavior without subclassing.
Data API. objImport(name: 'x', age: 3) bulk-assigns and returns $this (chainable). objKeys(), objValues() and objLength() inspect the data; objClear() wipes it. Iterating an obj (foreach $record AS $key => $value) and json_encode($record) expose exactly the stored data. Every write flips objChanged, the dirty flag the ORM uses to decide whether objSave writes anything.
Computed prop caching. prop x => ... caches on first access; the argument form caches per argument set. The same applies to computed statics, cached per class.
Worker persistence. prop objPers = true makes an instance survive between worker-mode requests: the phlo() registry only keeps objPers instances on its per-request reset. Right for DB connections and parsed config; wrong for anything request- or user-scoped.
Lesson. A plain prop in a parent class SHADOWS a computed prop in a child.
prop dir = voidin an abstract parent transpiles to a real PHP property, so a child'sprop dir => guidegetter is never consulted:$this->dirsilently readsvoid. When children must override with a computed prop, declare the parent prop computed as well:prop dir => void.
9.6: Best practices
- Use controller code for initial setup, not for request-dependent logic.
- Place controller code at the top or directly below props for readability.
__constructcaptures the instance arguments; keep it to that, ideally with promoted parameters (method __construct(public string $file)), because__handle()derives the instance's identity from those same arguments before construction. Heavy work belongs in lazy props; app-level boot in controller code.- Let instances initialize themselves lazily via
%nameinstead of creating them manually. - Use controller code deliberately to resolve circular references.