[PHP] Reusing Anonymous Classes
I recently had need for an anonymous class that would be generated by a function call. The function would take certain arguments, and use those to control the properties available in the class.
The idea was that the class itself would also have a constructor that could accept certain necessary data, and so I could control a class's functionality dynamically, and also populate its data dynamically.
If you've ever actually used PHP's anonymous classes, you will understand that I immediately hit some very hard walls. Where I had imagined something akin to template programming in C++, what I found was much closer to PHP's long-standing default object, stdClass.
Before we get into those details, let's take a look at what I was hoping would be possible. For the sake of an easy example, let's try to create a function that will accept a list of properties, and generate a class that contains. The class should then accept a list of values, and populate the properties with those values.
Let's call the function makeStruct, and let's have it generate a class that will take some values to fill in those properties:function makeStruct(...$properties)
This looks like a function that will take a list of properties and return a class. The class would then accept a list of values in its constructor, and combine them with the properties to create an associative array called $props.
{
return new class ($properties)
{
private $m_propNames = $properties;
private $m_props = [];
public function __construct(...$values)
{
$curVal = reset($values);
foreach($properties as $prop)
{
$this->m_props[$prop] = $curVal;
$curVal = next($values);
}
}
};
}
We might use the function thusly:$vector = makeStruct('x', 'y');
And we might expect to set up our magic getters and setters to be able to access 10 and 20 under their corresponding x and y names.
$position = new $vector(10, 20);
This might seem downright intuitive to someone familiar with how anonymous functions work. You return a class and then call that variable when you want to use it, right?
Unfortunately, PHP's anonymous classes don't work this way at all. In fact, it is impossible to generate a reusable class like this.
But that doesn't mean that dynamically generating a reusable class is impossible.
First, let's clear up some basic issues. The biggest problem with PHP's anonymous classes is that we can't really dynamically generate their structure. This means we can't change the properties and methods of the class based on the arguments we send it.
What we can do instead is store all of the properties in a single array, and use the magic __get and _set functions to control their values.
We can do the same thing for methods, using the magic __call and __callstatic to run closures stored in an array.
This means we lose any possible IDE support for properties and methods in our anonymous class, but that's okay - we probably couldn't expect good support even if we were able to generate real properties and methods dynamically.
The next big problem we have to deal with is the fact that creating a class this way doesn't just generate a new class. What it actually does is generate a new class, and create a new object of that class. There's no way to decouple that process in order to generate a class before you use it. That also means you can't just generate a class and use the class to create a bunch of objects. Which, unfortunately, is exactly what we want to do!
So how do we get around this limitation? Well I mentioned earlier that anonymous functions work differently: when you generate an anonymous function, you create a closure that you can then run whenever you like. You don't just create the function and run it at the same time. This means we can use anonymous functions to give our anonymous class the same flexibility.
Let's see what that looks like, and we can add the getters and setters for good measurefunction makeStruct(...$properties)
So now when we call makeStruct and pass it some properties, it returns an anonymous function. If we call that anonymous function and pass it some values for the properties we set earlier, it returns an anonymous class with those properties set to the corresponding values.
{
return function(...$values) use($properties)
{
return new class ($properties, $values)
{
private $m_props = [];
private $m_propNames = [];
public function __construct($properties, $values)
{
$this->m_propNames = $properties;
$curVal = reset($values);
foreach($properties as $prop)
{
$this->m_props[$prop] = $curVal;
$curVal = next($values);
}
}
public function __get($propName)
{
return $this->m_props[$propName];
}
public function __set($propName, $value)
{
if(in_array($propName, $this->m_propNames, true))
{
$this->m_props[$propName] = $value;
}
}
};
};
}
We also have magic __get and __set functions so we can access those properties we set earlier, and also ensure that we aren't allowed to create any new properties.
Here's what this looks like in use:$vector = makeStruct('x', 'y');
Which will output:
$position = $vector(10, 20);
echo $position->x, ' x ', $position->y;
10 x 20
And that's basically all there is to it. Now we can call a function that will set up the class's properties, and then essentially pass it the constructor arguments we need to in order to create a new object of that class.
There are still a few caveats because we can't really treat $vector like a class. We can't extend from $vector, and we can't check if $position is a $vector directly. That can be a problem if you need to check that you're dealing with a $vector object, as this code demonstrates:function vectorProcessor($vec)
Not a vectorWe can get around this issue by comparing objects created by $vector instead of comparing directly to $vector, like so:
{
global $vector;
if(get_class($vec) == $vector)
{
echo 'Is a vector';
}
elseif(get_class($vec) == get_class($vector))
{
echo 'Is a vector';
}
else
{
echo 'Not a vector';
}
}
vectorProcessor($position);function vectorProcessor($vec)
Is a vectorThis is workable as long as instantiating the $vector class is safe and isn't too resource intensive.
{
global $vector;
$test = $vector();
if(get_class($vec) == get_class($test))
{
echo 'Is a vector';
}
else
{
echo 'Not a vector';
}
}
But what if we never want to generate an object of the $vector class unnecessarily? Maybe the constructor does a lot of work that we don't want to repeat, or connects to a database, etc.
If for whatever reason we need to be able to compare against the $vector class without actually creating a new $vector, we can do it by taking the anonymous function inside $vector one step deeper.
function makeStruct(...$properties)
If you're wondering what on earth we've done here, we have replaced that anonymous function that returned our anonymous class with another anonymous class!
{
return new class ($properties)
{
private $class_properties;
private $className;
public function __construct($properties)
{
$this->class_properties = $properties;
}
public function getClass()
{
return $this->className;
}
public function __invoke(...$values)
{
$outputClass = new class ($this->class_properties, $values)
{
private $m_props = [];
private $m_propNames = [];
public function __construct($properties, $values)
{
$this->m_propNames = $properties;
$curVal = reset($values);
foreach($properties as $prop)
{
$this->m_props[$prop] = $curVal;
$curVal = next($values);
}
}
public function __get($propName)
{
return $this->m_props[$propName];
}
public function __set($propName, $value)
{
if(in_array($propName, $this->m_propNames, true))
{
$this->m_props[$propName] = $value;
}
}
};
$this->className = get_class($outputClass);
return $outputClass;
}
};
}
Okay we may have reached inception here, but the result is that we can still use the same old code to create the $position and $velocity objects with no problem. The important difference is that now we can add properties and methods to $vector, besides just using it to create $vector objects.
In this case, we have added a private property called className and a method to access it called getClass().
We moved the code from the anonymous function into the $vector class's __invoke method, allowing us to create new $vector objects the same way as always, but before we return the object we save it's class name into the className property.
Now instead of comparing between two objects created with $vector, we can simply use the getClass method to check if an object is a $vector object.
We can update our vectorProcessor function to take advantage of this:function vectorProcessor($vec)
So now, even though it took about 3 times as much code as originally hoped, we have most of the functionality we wanted. Of course, we have also pretty much reached the limits of PHP for dynamic class generation.
{
global $vector;
if(get_class($vec) == $vector->getClass())
{
echo 'Is a vector';
}
else
{
echo 'Not a vector';
}
}
It's worth mentioning that generating methods dynamically is just as simple as generating properties, thanks to PHP's anonymous functions.