The term “reflection” in software development means that a program knows its own structure at runtime and can also modify it. This capability is also referred to as “introspection”. In the PHP area, reflection is used to ensure type safety in the program code. As web developer at Anexia I know PHP like the back of my hand and I just use reflection quite often. Below I will explain how it works in practical terms, and why it is a very useful tool.
PHP is not exactly known for being a typed language. It is debatable whether this should be considered as positive or negative – both sides have their arguments.
In principle, PHP has four different basic data types: scalar, array, object and resource. The scalar data types include, among others: Boolean, integer, double, and string. Object classes, on the other hand, are complex data structures that are individually composed of the types listed above.
Unlike strongly typed languages, such as Java or C, the types of data in the memory can change dynamically in PHP. For example, there is always an attempt to determine an integer value for scalar types. Variables also do not have to be declared explicitly in PHP. This means that the declaration of a new variable does not necessarily have to specify a data type in this case.
On the one hand, this gives you the advantage of being flexible in the use of these variables, since you can always dynamically modify their use with respect to the data type without causing compiler problems. What may sound very appealing to many, however, also has a decisive disadvantage: perhaps the developer makes a small error, and he suddenly begins to reinitialize an array as an object in the middle of the program code. It could work fine for the rest of the process – but what about the remaining code that is based on the original array?
To demonstrate the behavior of PHP, let us consider the following example:
$foo = "image"; $foo == "image" // true $foo == 1 // false $foo == 0 // true $foo === 0 // false
As you can see, the “image” string corresponds to the numeric value “0”. This is because PHP first tries to convert the string to an integer, which also works fine in this case. If you transferred this string to the intval() function, you would actually obtain the value “0” – and a comparison of “0” and “0” is “true” after all.
This can be avoided, for example, by stricter type-safe operators, such as “===” and “! ==”.
If you work a lot with frameworks, you can immediately confirm that they make heavy use of annotations or enums. Although you can initially simply accept this fact, when you consider that PHP does not actually support these language constructs at all, the following question quickly poses itself: What are they actually good for? In the following example, we want to demonstrate the purpose of such constructs and point out the magic of it all.
Let’s prepare a simple example class:
class Example { private $attribute1; protected $attribute2; public $attribute3; const PI = 3.1415; public function __construct() { $this->attribute1 = "Lorem ipsum"; $this->attribute2 = 36; $this->attribute3 = 2 * self::PI; } public function getAttribute1() { return $this->attribute1; } public function setAttribute1($attribute1) { $this->attribute1 = $attribute1; } private function getAttribute2() { return $this->attribute2; } }
As you can see, this example class provides some simple methods and attributes. We will use it as a simple starting point in the next examples.
The reflection API makes it possible to analyze the properties of this class from the outside. To obtain a full export, reflection() provides the static export() method.
$reflection_object = new ReflectionClass( Example::class ); Reflection::export( $reflection_object );
The export of our example class would then look as follows:
Class [ <user> class Example ] { @@ [...][...] 2-30 - Constants [1] { Constant [ float PI ] { 3.1415 } } - Static properties [0] { } - Static methods [0] { } - Properties [3] { Property [ <default> private $attribute1 ] Property [ <default> protected $attribute2 ] Property [ <default> public $attribute3 ] } - Methods [4] { Method [ <user, ctor> public method __construct ] { @@ [...][...] 12 - 16 } Method [ <user> public method getAttribute1 ] { @@ [...][...] 19 - 21 } Method [ <user> public method setAttribute1 ] { @@ [...][...] 23 - 25 - Parameters [1] { Parameter #0 [ <required> $attribute1 ] } } Method [ <user> private method getAttribute2 ] { @@ [...][...] 27 - 29 } } }
If you for example want to know which methods are provided in the “Example” class, you can output this relatively easily using the following construct:
$reflection_class = new ReflectionClass("Example"); foreach ($reflection_class->getMethods() as $method) { echo $method->getName() . "\n"; }
As a result, you get the following:
As a parameter, ReflectionClass() expects only the full class name (including the namespace, if any) of the class being analyzed. To make initialization look even better, you can also directly use PHP’s class name resolution—
$reflection_class = new ReflectionClass( Example::class );
this is especially useful if you are working a lot with namespaces.
In addition to the method names, however, other information can also be read out:
$reflection_class = new ReflectionClass( Example::class ); foreach ($reflection_class->getMethods() as $method) { echo $method->getName() . "\n"; echo "Number of parameters: " . $method->getNumberOfParameters() . "\n"; echo "Is private: " . ($method->isPrivate() ? 'Yes' : 'No') . "\n\n"; }
As a result, you get the following:
The same behavior can incidentally also be applied to attributes. Instead of using getMethods() you simply use getProperties().
$reflection_class = new ReflectionClass(Example::class); foreach ($reflection_class->getProperties() as $property) { echo $property->getName() . "\n"; echo "Is private: " . ($property->isPrivate() ? 'Yes' : 'No') . "\n\n"; }
The result is comparable to that of the previous methods:
The reflection API incidentally also has a special feature. It can be used to execute private methods, something that would not be possible in the normal program context.
$object = new Example(); $reflection_object = new ReflectionObject($object); $method = $reflection_object->getMethod("getAttribute1"); try { echo $method->invoke($object); } catch (ReflectionException $e) { echo "Forbidden!"; }
You now get the string “Lorem ipsum” as an output. This is because the activated method getAttribute1() returns the attribute1 attribute, which was initialized in the constructor of the class with this string. As you can see, activating this method worked wonderfully.
In the next step, we will now try to activate our private method getAttribute2() by entering the desired method name in getMethod():
$object = new Example(); $reflection_object = new ReflectionObject($object); $method = $reflection_object->getMethod("getAttribute2"); try { echo $method->invoke($object); } catch (ReflectionException $e) { echo "Forbidden!"; }
We now get “Forbidden!” as an output, as defined in our exception handling. As expected, this is because you cannot directly access private methods outside the instantiated class.
However, this is precisely where the reflection API provides a useful tool with setAccessible().
$object = new Example(); $reflection_object = new ReflectionObject($object); $method = $reflection_object->getMethod("getAttribute2"); try { $method->setAccessible(true); echo $method->invoke($object); } catch (ReflectionException $e) { echo "Forbidden!"; }
We now get the number “36” as an output, as declared in our class. Using setAccessible(), the method’s private behavior can be deactivated for its further execution.
However, this is generally not recommended in practice, as there are usually reasons why some methods have been defined as private. One possible use of such techniques could be interesting for unit testing, for example.
With this example, we would now like to show you how to use the reflection API in practice. To illustrate this accordingly, we will use an entity object, as you have already seen with many frameworks.
To do so, we first build a base element from which we can later derive our individual entities. We assume that each entity has the following attributes (based on the Laravel framework):
/** * Class BaseElement */ abstract class BaseElement { /** * @var int $id */ private $id; /** * @var \DateTime $createdAt */ private $createdAt; /** * @var \DateTime $updatedAt */ private $updatedAt; /** * @var \DateTime $deletedAt */ private $deletedAt; /** * @var array */ private $_hidden = [ 'deleted_at' ]; /** * BaseElement constructor. * * @param array $data */ public function __construct( $data = null ) { } /** * Returns the ID. * * @return int */ public function getId() { return $this->id; } /** * Set the ID. * * @param int $id */ public function setId( int $id ) { $this->id = $id; } /** * Returns the creation date. * * @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * Set the creation date. * * @param \DateTime $createdAt */ public function setCreatedAt( $createdAt ) { $this->createdAt = $createdAt; } /** * Returns the update date. * * @return \DateTime */ public function getUpdatedAt() { return $this->updatedAt; } /** * Set the update date. * * @param \DateTime $updatedAt */ public function setUpdatedAt( $updatedAt ) { $this->updatedAt = $updatedAt; } /** * Returns the delete date. * * @return \DateTime|null */ public function getDeletedAt() { return $this->deletedAt; } /** * Set the delete date. * * @param \DateTime|null $deletedAt */ public function setDeletedAt( $deletedAt ) { $this->deletedAt = $deletedAt; } }
We now have a base class with four attributes, each with the associated getter and setter methods. In the next step, we ensure that a data array can be transferred in the constructor, which then automatically determines and executes the corresponding setter methods.
For this we write two helper classes: one for the string operations concerning the conversion from and to camel-case, as well as one for the type-casting:
class StringHelper { /** * Translates a camel case string into a string with * underscores (e.g. firstName -> first_name) * * @param string $str String in camel case format * * @return string $str Translated into underscore format */ public static function fromCamelCase( $str ) { $str[0] = strtolower( $str[0] ); $func = create_function( '$c', 'return "_" . strtolower($c[1]);' ); return preg_replace_callback( '/([A-Z])/', $func, $str ); } /** * Translates a string with underscores * into camel case (e.g. first_name -> firstName) * * @param string $str String in underscore format * @param bool $capitalise_first_char If true, capitalise the first char in $str * * @return string $str translated into camel caps */ public static function toCamelCase( $str, $capitalise_first_char = false ) { if ( $capitalise_first_char ) { $str[0] = strtoupper( $str[0] ); } $func = create_function( '$c', 'return strtoupper($c[1]);' ); return preg_replace_callback( '/_([a-z])/', $func, $str ); } }
class IoHelper { /** * Helper function for casting variables into desired types. * * @param $value * @param string $type * * @return float|int|bool|string */ public static function castValue( $value, $type = 'string' ) { $type = strtolower($type); switch ( $type ) { case 'string': $value = (string) $value; break; case 'int': case 'integer': $value = (int) $value; break; case 'double': $value = (double) $value; break; case 'float': $value = (float) $value; break; case 'bool': case 'boolean': $value = (bool) $value; break; default: $value = $value; } return $value; } }
Now let’s once again focus on our base class. To do so, we extend the constructor as follows:
/** * BaseElement constructor. * * @param array $data */ public function __construct( $data = null ) { // check if fill data was provided if ( $data ) { // cast object into array if it's an object if ( is_object( $data ) ) { $data = (array) $data; } // iterate over data array foreach ( $data as $key => $value ) { // build up name for equivalent setter-function $setterFunction = 'set' . StringHelper::toCamelCase( $key, true ); // check if desired setter-functions exists and if so, execute it if ( method_exists( $this, $setterFunction ) ) { // get reflection method $reflectionMethod = new \ReflectionMethod( $this, $setterFunction ); // get parameters for reflection method $reflectionParameters = $reflectionMethod->getParameters(); // check if desired parameter at position 0 exists if ( isset( $reflectionParameters[0] ) ) { // detect desired data type by reading doc-comments $type = strtolower( (string) $reflectionParameters[0]->getType() ); $castedValue = IoHelper::castValue( $value, $type ); // call setter-function with casted parameter $this->$setterFunction( $castedValue ); } } } } }
It is relatively easy to explain what happens here: it checks whether a data array has been transferred. If this is the case, it then checks whether it is an object (as would be the case, for example, with the Laravel eloquent ORM). If so, it is converted into an array. In the next step, a foreach loop is used to iterate via each individual transferred attribute in the key value style. Since the index is known, and it should represent the relevant attribute, we assume that the associated setter function is named in the “set <AttributName>” style.
If such a setter method actually exists in the current object instance, we instantiate it using ReflectionMethod(). Theoretically, it would already be possible to use it directly. However, we must first cast the transferred value accordingly to ensure that it matches the expected data type of the respective setter method.
To do so, getParameters() is first used to read out which parameters can be transferred to the respective setter method. We assume in this case that each of these methods can accept exactly one parameter, i.e., the value to be set. If this parameter is defined, then the expected data type, which was defined via the PHP annotations, is returned using getType(). We now transfer this determined target data type to our helper class and get the correspondingly casted variable back.
In the last step, we now activate the setter method via regular access and transfer the previously casted value to it as a parameter. In principle, we have now implemented our core functionality regarding auto-fill.
To now access data within our base class using the respective getter method, we write a little private helper method:
/** * Tries to find an adequate Getter-function for the specified attribute key, and returns its value. * * @param $key * * @return null */ private function getElementByKey( $key ) { // build up name for equivalent setter-function $getterFunction = 'get' . StringHelper::toCamelCase( $key, true ); // check if desired setter-functions exists and if so, execute it if ( method_exists( $this, $getterFunction ) ) { // get reflection method $reflectionMethod = new \ReflectionMethod( $this, $getterFunction ); $reflectionMethodName = $reflectionMethod->getName(); return $this->$reflectionMethodName(); } return null; }
We can transfer the name of the desired attribute to it as a parameter. Similar to the way we set the values previously, we determine the name of the associated getter method assuming that it was defined in the “get <AttributName>” style.
If the method exists in the current object context, it is executed using ReflectionMethod(), and as a result the value of the desired attribute is returned.
In order for us to dynamically enable the output of all attributes, we must know which attributes are generally present in the activated class as well as in the base class (as parent class). To do so, we define a method named getDocument(), which does exactly that:
/** * Build up the public exposed data array. * * @param bool $buildRelations * * @return array */ public function getDocument() { $reflectionClass = new \ReflectionClass( $this ); $properties = []; // determine properties of parent class from current context $this->buildPropertyArray( $reflectionClass->getParentClass()->getProperties(), $properties ); // determine properties of current class $this->buildPropertyArray( $reflectionClass->getProperties(), $properties ); // build up data array $data = []; foreach ( $properties as $property ) { if ( ! in_array( StringHelper::fromCamelCase( $property ), $this->_hidden ) ) { $data[ $property ] = $this->getElementByKey( $property ); } } return $data; } /** * Internal helper function for extracting valid (public) attributes from ReflectionProperty() array. * * @param $reflectionProperties * @param $data */ private function buildPropertyArray( $reflectionProperties, &$data ) { foreach ( $reflectionProperties as $property ) { $propertyName = $property->getName(); if ( substr( $propertyName, 0, 1 ) == '_' ) { continue; } $data[] = $property->getName(); } }
The auxiliary method buildPropertyArray() mainly helps to avoid duplicate code and also to ignore attributes that begin with an (“_”) underscore. Attributes that begin with an underscore are not exported externally via getDocument() in this case, and therefore remain confidential.
To derive your own entity classes using the base class, you merely have to create a new class with the desired name which will inherit data from our previously created base class.
Individual attributes can now be defined within it according to the same principle—nothing else is necessary here.
/** * Class Dummy */ class Dummy extends BaseElement { /** * @var string $title */ private $title; /** * Returns the title. * * @return string */ public function getTitle() { return $this->title; } /** * Set the title. * * @param string $title */ public function setTitle( string $title ) { $this->title = $title; } }
To test our entity class, we define a small data array using a few test values. This should simulate an external data source in our case.
$data = [ "id" => 14, "title" => "Lorem ipsum", ];
In the next step, we initialize our newly created entity class using this data array and can then export the full result using getDocument():
$entity = new Dummy( $data ); print_r( $entity->getDocument() );
The program can use reflection to operate in an “intelligent” manner in modern software development since it is able to analyze and even modify the characteristics of it own functions during runtime.
However, the implementation of reflection in PHP is so broad that we have demonstrated only a limited number of the possibilities here. There are countless other functions to further break down the program code.
In summary, we can see that reflection provides you with a very powerful technology that can really help you accomplish a great deal. Actually, we are using it a lot at Anexia.