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:

  1. The instance manager checks, via the identity that __handle() (or the default rule) derives from the arguments, whether a matching instance already exists.
  2. If not, it is created and stored.
  3. After creation, the controller code runs (see §8.2).
  4. 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.

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)
}

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 = void in an abstract parent transpiles to a real PHP property, so a child's prop dir => guide getter is never consulted: $this->dir silently reads void. When children must override with a computed prop, declare the parent prop computed as well: prop dir => void.

9.6: Best practices

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.