Skip to content

Instantly share code, notes, and snippets.

@ollieread
Last active May 7, 2022 15:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ollieread/34c878bf120ee70f9d2a869cb7a242d1 to your computer and use it in GitHub Desktop.
Save ollieread/34c878bf120ee70f9d2a869cb7a242d1 to your computer and use it in GitHub Desktop.
Improvements to PHPs Reflection

This is a collection of potential improvements that could be made to PHPs native reflection. I understand that some of this may be niche and that most of the content will be entirely opinion based, but I present them to anyone that wishes to understand.

Types

Type Value Matching

This particular improvement is based entirely on the back of my time working with dependency injection within PHP.

I propose a method, on either ReflectionType or ReflectionNamedType, that lets you check if a given value matches the type.

public function isTypeOf(mixed $value): bool;

NOTE It was suggested by @iluuu1994 that this method could be named isSubTypeOf, with an optional second method isSuperTypeOf. My only concern with this naming is that if a type is an exact match, it isn’t strictly a sub or supertype, and we don’t lump exact matches in for sub, as evidenced by is_subclass_of

This is something I have added myself in my wrapper library as Type::matches().

I would have to do some more research into this, but I'd hazard that a decent percentage of the uses of RefectionType and its ancestors are to see if types match.

Doing this in userland is possible, though it is somewhat tedious.

Type Comparison

This improvement is a partner to the above isTypeOf suggestion, except that instead of a value, it takes a type.

public function isAssignableTo(ReflectionType|string $type): bool;

NOTE As the suggestion by @iluuu1994, we could use isAssignableFrom or even have both the to and from variations if both were needed.

This is also something I have added myself in my wrapper library as Type::accepts().

After the above, I also suspect that a good percentage of the remaining uses of ReflectionType fall into this category. You’re checking to see if the two types are compatible.

New Instances

If the above two methods or mechanisms that achieve the same goal were to be added, it would make sense for developers to be able to create new type reflection instances from strings that represent types.

Something like the following.

$type = ReflectionType::fromString('?string');

If the idea of a static method isn't one that people like, you could stick with the constructors, requiring developers to use the correct type class for what they need.

$nullableString = new ReflectionNamedType('?string');
$union = new ReflectionUnionType('string', 'int');
$intersection = new ReflectionIntersectionType(MyClass::class, MyInterface::class);

It would also be possible to add a way to reflect a values type.

$type = ReflectionNamedType::from($value);

Normalising nullable types

This improvement is specific to types marked as nullable using the ?, so it doesn’t apply to union or mixed types. This improvement is also mostly redundant given some solutions to the above.

Give the type ?string, ReflectionType::allowsNull() would give you true, allowing you to accept a null value, but if you're trying to compare against the type string or a value, you have to manipulate the value of ReflectionNamedType::getName() to strip out ?.

My suggestion would be to have all types marked as nullable with ? return as ReflectionUnionType, but with getName returning the original type definition.

Again, if a mechanism that allows you to compare values and types is added directly to the type classes themselves, this would be a redundant change.

Attributes

Attributes are relatively new in the PHP world, and while the current functionality works well, there could be improvements.

Get a single attribute

There currently exists a getAttributes method on all relevant reflection classes to return an array of attributes, optionally by class name. My suggestion here is to introduce a singular version of the method.

public function getAttribute(string $name = null, int $flags = 0): ?ReflectionAttribute;

Its arguments would follow the same rules as its plural version, returning null if nothing matches, a single instance of ReflectionAttribute if there is one match, or throwing an exception if there is more than one result.

This method also brings the attribute handling more inline with parameters, properties and methods, all of which have a plural methods (ReflectionFunctionAbstract::getParameters(), ReflectionClass::getProperties(), ReflectionClass::getMethods()) and singular methods (ReflectionFunctionAbstract::getParameter(), ReflectionClass::getProperty(), ReflectionClass::getMethod()). Only parameters lacks a presence checker method.

Check if an attribute is present

This suggestion is a simple presence checker to check if an attribute is present on something.

public function hasAttribute(string $name = null, int $flags = 0): bool;

The arguments are the same as getAttributes and getAttribute, except that it returns a bool value.

This method follows the above in that it brings it more in line with how the other elements are handled.

Get the number of attributes

This suggestion is a nice to have method that is based on ReflectionFunctionAbstract::getNumberOfParameters().

public function getNumberOfAttributes(string $name = null, int $flags = 0): int;

The arguments are against the same as getAttributes, getAttribute and hasAttribute.

The purpose of this method is mainly for attributes where the number of them would have significance. Using hasAttribute, you’d have to do something like.

if ($method->hasAttribute(MyAttribute::class)) {
    $attributes = $method->getAttributes(MyAttribute::class);

    return match(count($attributes)) {
        1 => $this->handleSingular(),
        2 => $this->handleTwo(),
        3 => $this->handleThree()
    };
}

Where has the entire chunk of code could be simplified to the following.

return match($method->getNumberOfAttributes(MyAttribute::class)) {
    1 => $this->handleSingular(),
    2 => $this->handleTwo(),
    3 => $this->handleThree()
};

Create a ReflectionWithAttributes interface

The name ReflectionWithAttributes is the most straightforward name I could come up with while writing this, but I am open to better suggestions.

The idea behind this is that if we were to add more methods, like the ones mentioned above, a shared interface for ReflectionFunctionAbstract, ReflectionParameter, etc., would allow userland code to handle attributes with ease.

Hopefully, it would simplify things in terms of documentation, as it would only require documenting once. I’m actually in the process of documenting all the instances of getAttributes as the docs are missing.

Retrieve the target reflection from an attribute

Given an instance of ReflectionAttribute, there’s no way to retrieve the element that it was attached to. This isn’t a huge issue, as there’s no way to retrieve an instance without using a getAttributes method, but this would be super helpful when coupled with the above-suggested interface.

public function getTargetReflection(): Reflector;

The idea is that by introducing this method, you can retrieve an instance of ReflectionClass, ReflectionMethod, etc by calling ReflectionAttribute::getTargetReflection().

Parameters

Parameter presence checker

Currently, ReflectionClass and ReflectionObject provide a way to check for a property or method by using their name and a specific method, hasProperty and hasMethod. This works because these elements have unique names.

Function parameters have always had unique names. With the importance of their names being paramount since introducing the named parameters RFC, perhaps it’s time to introduce a method to ReflectionFunctionAbstract.

public function hasParameter(string $name): bool;

This method is identical to ReflectionClass::hasMethod() and ReflectionClass::hasProperty(), except that it checks for a parameter by that name.

I cannot think of an exact use for this, though the idea behind it was primarily one of consistency. This method, coupled with the attribute-based changes, would ensure that all reflected elements that have child elements provide a way to retrieve them all, a single one by name, and check the presence of one, also by name. The only exception to this would be attributes, where the getNumberOfAttributes method comes in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment