Tools For Better Developers

Reflection

Osm Framework Writing PHP Code

Version 0.15 ∙ 4 minutes read

PHP reflection is great. However, it comes with limitations: there is no efficient way of enumerating all descendants of a certain class, and it doesn't work with @property declarations. Osm Framework addresses these limitations, and provides a very fast reflection API that powers its metaprogramming features.

Details:

Introduction

Let's illustrate how the reflection is typically used on the example of a console application.

In order to define a console command in Osm Framework, you only need to define a class that extends Command class, and specify its name:

class Hello extends Command {
    public string $name = 'hello';

    public function run(): void {
        $this->output->writeln('Hello, world!');
    }
}        

After it's defined, you can use this command:

osm hello

How it works. Osm Framework collects all descendants of the Command class, and configure the console application with all found commands.

Further, in order to define an argument, or an option the command accepts, all you need to do it to define a property, and mark it with #[Argument] or #[Option] attribute:

use Osm\Framework\Console\Attributes\Argument;
use Osm\Framework\Console\Attributes\Option;
...
/**
 * @property string $person #[Argument]
 * @property bool $caps #[Option]
 */
class Hello extends Command {
    public string $name = 'hello';

    public function run(): void {
        $person = $this->caps
            ? mb_strtoupper($this->person)
            : $this->person;

        $this->output->writeln("Hello, {$person}!");
    }
}        

How it works. Osm Framework analyzes attributes assigned to properties, and their types, and registers all marked properties as command attributes and options. When the command is executed Osm Framework fills in provided arguments and options into the properties of the command object:

> osm hello Joe --caps
Hello, JOE!  

All of these things that greatly simplify console application development, are made available by reflection.

Classes

Note. First and foremost, Osm Framework collects information only about classes that is contained in modules of the current application.

Get the reflection information about a class using $osm_app->classes property, indexed by full class name:

use Osm\Core\App;
use My\Base\Foo;
...
global $osm_app; /* @var App $osm_app */
...
$class = $osm_app->classes[Foo:class];

Alternatively, get the reflection information about a class of a specific object $foo using its __class property:

$class = $foo->__class;

Once you obtained the class information (it's an instance of Class_ type), explore it further using its properties:

  • name - the class name, My\Base\Foo
  • generated_name - the name of a generated class that is actually instantiated instead of My\Base\Foo in order to apply dynamic traits.
  • attributes - array of class attributes, indexed by attribute class name.
  • properties - array of class properties indexed by property name. Contains both regular PHP properties, and the ones introduced using @property syntax.
  • methods - array of class methods, indexed by method name.

Properties

Get the reflection information about a property using $class->properties property, indexed by property name:

use Osm\Core\App;
use My\Base\Foo;
...
global $osm_app; /* @var App $osm_app */
...
$class = $osm_app->classes[Foo:class];
$property = $class->properties['bar'];

Note. $class->properties contains properties defined in the specified class, properties inherited from parent classes, and properties from all dynamic traits applied the specified class and its parent classes. It contains not only regular PHP properties, but also the ones introduced using @property syntax.

Property object, an instance of Property class, contains the following information:

  • name - property name.
  • type - property type. If the property is an array, then the type specifies the type of array items.
  • array - a boolean flag indicating whether the property is an array.
  • nullable - a boolean flag indicating whether the property is optional and can return null value.
  • attributes - array of property attributes, indexed by attribute class name.
  • class_name - the full name of the class containing this property.

Methods

Regarding method reflection, Osm Framework has nothing much to add. Obtain information about class methods using standard PHP ReflectionMethod class:

use My\Base\Foo;
...
$class = new \ReflectionClass(Foo::class);
$method = $class->getMethod('bar');

For more information, refer to PHP documentation.

Attributes

Get the information about PHP 8 attributes applied to a class, o a property using $class->attributes and $property->attributes properties, respectively, both indexed by the attribute class name:

use Osm\Core\App;
use My\Base\Foo;
use Osm\Core\Attributes\Name;
...
global $osm_app; /* @var App $osm_app */
...
$class = $osm_app->classes[Foo:class];
$property = $class->properties['bar'];
$attribute = $property->attributes[Name::class] ?? null;

The $property->attributes[Name::class] returns attribute instance. If a given attribute is not applied, $property->attributes[Name::class] is not set.

If the attribute class RepeatableAttribute is marked with Attribute::IS_REPEATABLE flag, then property->attributes[RepeatableAttribute::class] returns an array all attribute instances rather than a single instance:

...
foreach ($property->attributes[RepeatableAttribute::class] ?? []
    as $attribute)
{
    ...
}   

The contents of each attribute instance is defined by its class. For example, the Name attribute class, often used in Osm Framework to give a class some short, yet unique name, has a single name property:

#[\Attribute(\Attribute::TARGET_CLASS)]
final class Name
{
    public function __construct(public string $name) {
    }
}

Given an attribute is applied to a class:

#[Name('foo')]
class Foo extends Object_ {
}

When you reflect over the Foo class, get the assigned unique name foo:

use Osm\Core\App;
use My\Base\Foo;
use Osm\Core\Attributes\Name;
...
global $osm_app; /* @var App $osm_app */
...
$class = $osm_app->classes[Foo:class];
$name = $property->attributes[Name::class]?->name;

Descendants

Use $osm_app->descendants property for collecting all classes deriving from a specified base class:

use Osm\Core\App;
use My\Base\Foo;
use Osm\Core\Attributes\Name;
...
global $osm_app; /* @var App $osm_app */
...
$classes = $osm_app->descendants->classes(Foo::class); 

If, by design, all derived classes are assigned a unique name using the #[Name] attribute, use byName() method to get the class names indexed by the assigned unique name:

$classNames = $osm_app->descendants->byName(Foo::class);

Performance

All examples, presented above, work really fast, because all the reflection information is collected during compilation phase, and at runtime, it's only unserialized from the generated/{app}/app.ser. It means that it doesn't incur any significant runtime cost.