We're working on a prominent new feature for Flare in the coming months. I hope to share more information about it in the coming weeks, but for now, we're going to keep it a secret.
We're rewriting the internals of some of the Flare packages for this feature, which led to a problem. It goes like this: we have an item that we store in an array with similar items like this:
/**
* @template T
*/
class Store
{
/**
* @param array<T> $items
*/
public function __construct(
protected array $items = []
) {
}
/**
* @param T $item
*/
public function addItem(mixed $item): void
{
$this->items[] = $item;
}
}
There's nothing too fancy right now. The Store should keep a configured number of items, so we drop the first item added when we add a new item and the limit has been reached. We are always more interested in the items added at the process's end.
Such a thing can be easily implemented as such:
/**
* @template T
*/
class Store
{
/**
* @param array<T> $items
*/
public function __construct(
protected array $items = [],
protected int $maxEntries = 200,
) {
}
/**
* @param T $item
*/
public function addItem(mixed $item): void
{
$this->items[] = $item;
if (count($this->items) > $this->maxEntries) {
array_shift($this->items);
}
}
}
Now starts the complexity. We define a completely new structure called a Node. A Node can have children, which are again Nodes. We're basically creating a tree structure.
Here's the thing: our Node also has an array of items, the identical items we used in our Store earlier:
/**
* @template T
*/
class Node
{
/**
* @param array<Node> $children
* @param array<T> $items
*/
public function __construct(
public readonly string $id,
public array $children = [],
public array $items = [],
)
{
}
}
We update our requirements so that when adding an item to the store, we'll also add that item to a node. To find the node, we use a MagicNodeFinder (something I just made up to keep this article a bit clearer). The MagicNodeFinder will just return a node where the item should be stored, but it is dependent on the time it runs, so it will return different nodes based on the time it has been called.
Our Store now looks like this:
/**
* @template T
*/
class Store
{
/**
* @param array<T> $items
*/
public function __construct(
protected array $items = [],
protected int $maxEntries = 200,
) {
}
/**
* @param T $item
*/
public function addItem(mixed $item): void
{
$node = (new MagicNodeFinder())->execute();
$this->items[] = $item;
$node->items[] = $item;
if (count($this->items) > $this->maxEntries) {
array_shift($this->items);
}
}
}
We're almost there. We need to update the item removal code so that items can be removed from the node when the item limit is reached.
Although this might look easy, it isn't since MagicNodeFinder cannot be trusted to return the correct node where the item is stored.
There's no way to know in which node the item is stored, and even if we knew the node, we'd have to traverse the tree every time we need to remove an item, resulting in wasted computing time.
Introducing the WeakMap
In PHP 8.0, WeakMaps were added to the language. Weakmaps are maps that hold objects as keys but without raising the reference count for that object. Thus, whenever an object stored in the WeakMap is garbage collected(it doesn't exist anymore because it went out of scope or was unset), it is also immediately removed from the map.
That all sounds difficult; let's take a look at an example. We refactor our Node class like this:
/**
* @template T
*/
class Node
{
/**
* @param string $id
* @param array<Node> $children
* @param WeakMap<T, null> $items
*/
public function __construct(
public readonly string $id,
public array $children = [],
public WeakMap $items = new WeakMap(),
) {
}
}
Let's add an item to our node:
$item = new Item('Some info here');
$node = new Node(42);
$node->items[$item] = null;
We count the number of items in the WeakMap:
$node->items->count(); // 1
If we garbage collect our item by unsetting it:
unset($item);
What happens to the WeakMap's count?
$node->items->count(); // 0
Cool! The item was automatically removed from the map by garbage collecting the item.
Back to our Store
Let's see how we can use this Weakmap within our Store. The addItem method of our Store looks like this when using a WeakMap in Node:
/**
* @param T $item
*/
public function addItem(mixed $item): void
{
$node = (new MagicNodeFinder())->execute();
$this->items[] = $item;
$node->items[$item] = null;
if (count($this->items) > $this->maxEntries) {
array_shift($this->items);
}
}
By shifting the items array, the object will, due to scoping rules, be unset when the end of the method is reached. Thus, it will also be removed from the WeakMap, which is excellent! We're done; no extra changes are required here!
Conclusions
WeakMap was a feature added to PHP that I thought I would never use. Boy, was I wrong! It made our code a lot more performant and easier to read.
We have great things in the pipeline and would love to show them in the next months. See you soon!
Continue reading
Lessons from the deep end
20 months ago, we started building Performance Monitoring as Flare’s next big feature, never expecting Laravel’s rapid commercial growth to put us in direct competition with their own tools. This is our honest take on those 20 months went, how we’re adapting to this new reality, and where we’re heading next while staying true to who we are. A dive into the deep end, without knowing how far down it goes.
Alex
Connect your AI agent to Flare to automatically fix production and performance problems in PHP and Laravel projects
You can now use our MCP server to connect your AI agent to Flare. This way your AI has all context it needs to diagnose and fix production and performance problems.
Freek
Subscribe to Backtrace, our quarterly Flare newsletter
No spam, just news & product updates