Just read this. Nice take:
https://generalistprogrammer.com/tutorials/php-traits-complete-guide
Thinking out loud… tell me if I’m wrong.
I’m mulling the idea of using Traits to compose static classes with no state.
The static class would act as a Facade. It could implement an Interface.
The static class would be paired with a state object, which could contain all the state. This pairing would allow a clean separation of data from algorithms.
The Traits would contain no state. State, or portions of state, are passed in as parameters to methods.
Maybe it would be possible to create no-state objects, to allow for class-based dispatch (a kind of polymorphism).
Notebooking
This is the first draft. It worked, but it it useful?
<?php
interface AnimalInterface {
static function sayHello(Animal $a);
static function doPoo(Animal $a);
static function doEat(Animal $a);
}
class Animal implements AnimalInterface {
use Speak;
use Poop;
use Eat;
}
trait Speak {
static function sayHello(Animal $a)
{
echo 'Says: ' . $a->speech . "\n";
}
}
trait Poop {
static function doPoo(Animal $a)
{
echo 'Defecates: ' . $a->defecate . "\n";
}
}
trait Eat {
static function doEat(Animal $a)
{
echo 'Eats: ' . $a->food . "\n";
}
}
class AnimalState {
public $state = [];
function init()
{
$this->state['dog'] = new Dog();
$this->state['cat'] = new Cat();
$this->state['bird'] = new Bird();
}
}
class Dog extends Animal {
public readonly string $speech;
public readonly string $defecate;
public readonly string $food;
public readonly int $age;
function __construct($age = 1)
{
$this->speech = 'woof';
$this->defecate = 'dog loaf';
$this->food = 'kibble';
$this->age = $age;
}
}
class Cat extends Animal {
public readonly string $speech;
public readonly string $defecate;
public readonly string $food;
public readonly int $age;
function __construct($age = 1)
{
$this->speech = 'mew';
$this->defecate = 'hairpoo';
$this->food = 'tuna';
$this->age = $age;
}
}
class Bird extends Animal {
public readonly string $speech;
public readonly string $defecate;
public readonly string $food;
public readonly int $age;
function __construct($age = 1)
{
$this->speech = 'chirp';
$this->defecate = 'birdstool';
$this->food = 'millet';
$this->age = $age;
}
}
$s = new AnimalState();
$s->init();
echo "\ndog\n";
Animal::sayHello( $s->state['dog'] );
Animal::doPoo( $s->state['dog'] );
Animal::doEat( $s->state['dog'] );
echo "\ncat\n";
Animal::sayHello( $s->state['cat'] );
Animal::doPoo( $s->state['cat'] );
Animal::doEat( $s->state['cat'] );
echo "\nbird\n";
Animal::sayHello( $s->state['bird'] );
Animal::doPoo( $s->state['bird'] );
Animal::doEat( $s->state['bird'] );
// we create a new bird to replace the old one
$s->state['bird'] = new Bird(5);
echo "\nthe new bird has age: " . $s->state['bird']->age . "\n\n";
var_dump($s);This code has interesting qualities.
First, the AnimalState is the only thing that holds state.
Everything else doesn’t hold state.
The Dog, Cat, and Bird types are stateful classes, and instantiated, but aren’t mutable. All the properties are readonly.
Look at line 109. I cannot set the value of $age, because it’s readonly, but I can initialize a new Bird of 5 years old. Some languages also have this restriction, where named values are immutable.
Method names and property names matter. For all the parts to work, they must conform. The AnimalInterface enforces the method names. Nothing enforces the property names.
Is it useful? I’m not sure. It’s a lot of extra boilerplate.
What’s Missing?
PHP 8.3.6 doesn’t support public readonly protected(set). If it did, I think I could move the variable declarations up into Animal, and it would act like a contract for the subclasses.
I tried moving the properties like $speech into the Trait (rather than in the Class like it is above). It linted with php -l, failed with phpstan, and didn’t run. It refused to initialize the $speech property from the Trait because it was out of the Dog scope.
Here’s the json_encode($s) output:
{
\"state\": {
\"dog\": {
\"speech\": \"woof\",
\"defecate\": \"dog loaf\",
\"food\": \"kibble\",
\"age\": 1
},
\"cat\": {
\"speech\": \"mew\",
\"defecate\": \"hairpoo\",
\"food\": \"tuna\",
\"age\": 1
},
\"bird\": {
\"speech\": \"chirp\",
\"defecate\": \"birdstool\",
\"food\": \"millet\",
\"age\": 5
}
}
}Code language: JSON / JSON with Comments (json)
It’s OK, but it needs a _class property so it can rehydrate correctly (which is easy to add).
$s->state['bird']->age = 100; // this code passes php’s lint, but it’s a bug.
Ideally, this bug would be caught at compile time, because assigning a value to a readonly property throws an error when run.
I found this good post about PHP linters.
phpstan found the bug with –level 3.
(I tried installing psalm, but my PHP version was too low.)
More Thinking
In a real-world scenario, you would probably have a more elaborate state structure of nested arrays, terminating in these read-only objects, or arrays of read-only objects.
Your application would be a bunch of Interfaces and implementation, read-only objects, and some logic to make the app happen.
I think you’d have lots of code like this:
$s->state['x']['y']['z'] =
SomeService::doAThingTo( $s->state['x']['y']['z'] );Code language: PHP (php)
You’d have that mixed in with a lot of traditional logic.
You’d write functions to manipulate the state yourself.
Some things could get easy, like saving the app state with each request: use a flatter namespace, and save it to Redis. (Examples: ‘config’, ‘app’, ‘user’, ‘documents’.)
Retrieve the state from Redis. Add a feature to lazy-load the data, and write subsets of the state.
AnimalState doesn’t need to be initialized with objects. It’s just an array. It should probably use magic methods to allow $s->dog to return the Dog object.
Setter magic methods could verify that the new value is of the same class as the old value; so a Dog object must be replaced with another Dog object.
DocBlocks might be able to add a type annotation, but I don’t know if it would be honored.
Magic getters could map a key like ‘dog’ to a property named ‘_dog’, which has a type signature that phpstan might recognize. The magic getter should be coded up with a switch statement rather than doing string mangling and isset().
Example:
public readonly Dog $_dog;
function __get($key): Animal
{
switch($key) {
case 'dog': return $this->_dog;
default: // some error situation, die nicely
}
}Code language: PHP (php)
The robot overlords can understand that code path, and won’t need to “reason” about some string mangling code like this (code not checked):
public $state;
...
function __get($key): object
{
$kkey = "_$key";
if (isset($this->state[$kkey])) {
return $this->state[$kkey];
} else {
// die politely
}
}Code language: PHP (php)
I realize the latter is more general, but that begs the question: should everything be generalized and flexible at runtime, even if it’s the thing holding the application state?
The answer in 2026 is “no”.