Softwareentwicklung und Managed Hosting
ANEXIA
APR
11
2018

Reflection in PHP Projekten

Geschrieben am  11. April 2018 von Manuel Wutte

Unter dem Begriff „Reflection“ versteht man in der Softwareentwicklung, dass ein Programm zur Laufzeit seine eigene Struktur kennt und diese auch modifizieren kann. Diese Fähigkeit wird unter anderem auch als die sogenannte „Introspection“ bezeichnet. Im PHP-Bereich findet Reflection Anwendung bei der Sicherstellung von Typsicherheit im Programmcode. Wie das konkret funktioniert, und warum sie ein sehr nützliches Hilfsmittel ist, wird im Folgenden erklärt.

Typsicherheit in PHP

PHP ist nicht gerade bekannt dafür, dass es eine typisierte Sprache ist. Es lässt sich herzlich darüber streiten, ob man das nun positiv oder negativ betrachten soll – beide Seiten haben ihre Argumente.

Grundsätzlich unterscheidet man in PHP zwischen vier grundlegenden Datentypen: Skalar, Array, Objekt und Ressource. In den Bereich der skalaren Datentypen fallen u.a.  „Boolean“, „Integer“, „Double“ und „String“. Objekt-Klassen dagegen sind komplexe Datenstrukturen, die sich aus den oben angeführten Typen individuell zusammensetzen.

Auswirkung der schwachen Typisierung

Im Gegensatz zu stark typisierten Sprachen, wie beispielsweise Java oder C, können sich in PHP die Typen, der im Speicher befindlichen Daten dynamisch ändern. So wird beispielsweise versucht, von skalaren Typen auch stets einen Integer-Wert zu ermitteln. Auch müssen in PHP Variablen nicht explizit deklariert werden. Das bedeutet, dass der Deklaration einer neuen Variable hier nicht unbedingt ein Datentyp angegeben werden muss.

Dies hat einerseits den Vorteil, dass man flexibel in der Verwendung dieser Variable ist, da man deren Verwendung in Hinblick auf den Datentyp stets dynamisch modifizieren kann, ohne dass es zu Problemen mit dem Compiler kommt. Was nun für viele sehr verlockend klingt, birgt jedoch auch einen entscheidenden Nachteil: angenommen dem Entwickler unterläuft ein kleines Missgeschick, und er beginnt mitten im Programmcode beispielsweise ein Array plötzlich als Objekt neu zu initialisieren. Für den weiteren Verlauf könnte es problemlos klappen – was ist aber mit dem restlichen Code, welcher von dem ursprünglichen Array ausgeht?

Um das Verhalten von PHP zu demonstrieren, betrachten wir mal das folgende Beispiel:

$foo = "image";

$foo == "image"		// true
$foo == 1			// false
$foo == 0			// true
$foo === 0 			// false

Wie man sieht, entspricht der String „image“ dem numerischen Wert „0“. Dies liegt daran, dass PHP versucht, den String als erstes in einen Integer zu konvertieren, was in diesem Fall auch klappt. Würde man der intval()-Funktion diesen String übergeben, so erhält man tatsächlich den Wert „0“ – und ein Vergleich von „0“ und „0“ ist nun mal „true“. Umgehen lässt sich dies beispielsweise durch striktere typensichere Operatoren, wie „===“ und „!==“.

Die Reflection-API in PHP

Wer viel mit Frameworks arbeitet, kann sofort bestätigen, dass diese sehr stark mit Annotations oder Enums arbeiteten. Zunächst kann man diese Tatsache einfach mal akzeptieren. Wenn man nun aber bedenkt, dass PHP diese Sprach-Konstrukte eigentlich überhaupt nicht unterstützt, kommt schnell mal die Frage auf: Wozu dienen diese dann überhaupt? Mit folgendem Beispiel, wollen wir den Zweck solcher Konstrukte demonstrieren, und die Magie in dem Ganzen verdeutlichen.

Ein Beispiel

Bereiten wir eine einfache Beispiel-Klasse vor:

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;
	}
	
}

Wie man sieht, stellt diese Beispielklasse ein paar einfache Methoden und Attribute bereit. Sie wird uns in den nächsten Beispielen als einfacher Ausgangspunkt dienen.

Objekt-Dump ausgeben

Mit Hilfe der Relection-API ist es möglich, die Eigenschaften dieser Klasse von außen zu analysieren. Um einen vollständigen Export zu erhalten, stellt Reflection() die statische Methode export() zur Verfügung.

$reflection_class = new ReflectionClass("Example");

foreach ($reflection_class->getMethods() as $method) {
	echo $method->getName() . "\n";
}

Als Resultat erhält man dabei Folgendes:

  • __construct
  • getAttribute1
  • setAttribute1
  • getAttribute2

Als Parameter erwartet die ReflectionClass() lediglich den vollen Klassennamen (inkl. Namespace, sofern vorhanden) der zu analysierenden Klasse. Um die Initialisierung noch ein wenig schöner zu gestalteten, kann man hier auch direkt die Klassennamen-Auflösung von PHP nutzen:

$reflection_class = new ReflectionClass( Example::class );

Dies ist besonders dann sinnvoll, wenn viel mit Namespaces gearbeitet wird.

Neben den Methoden-Namen lassen sich aber auch andere Informationen auslesen:

$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";
}

Als Resultat erhält man dabei Folgendes:

  • __construct
    Number of parameters: 0
    Is private: No
  • getAttribute1
    Number of parameters: 0
    Is private: No
  • setAttribute1
    Number of parameters: 1
    Is private: No
  • getAttribute2
    Number of parameters: 0
    Is private: Yes

 

Attribute auslesen

Das gleiche Verhalten lässt sich übrigens auch auf Attribute anwenden. Anstelle von getMethods() verwendet man hierfür einfach 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";
}

Das Resultat ist hier vergleichbar mit jenem bei den Methoden zuvor:

  • attribute1
    Is private: Yes
  • attribute2
    Is private: No
  • attribute3
    Is private: No

 

Methoden mittels Reflection ausführen

Die Reflection-API hat nebenbei übrigens auch eine Besonderheit. So lassen sich mit Hilfe von ihr auch private Methoden direkt ausführen, was im normalen Programmkontext gar nicht möglich wäre.

$object = new Example();

$reflection_object = new ReflectionObject($object);
$method = $reflection_object->getMethod("getAttribute1");

try {
	echo $method->invoke($object);
	
} catch (ReflectionException $e) {
	echo "Forbidden!";
}

Als Ausgabe erhält man nun den String „Lorem ipsum“. Das liegt daran, dass die aufgerufene Methode getAttribute1() das Attribut attribute1 zurückliefert, welches im Konstruktor der Klasse mit diesem String initialisiert wurde.

Wie man sieht, hat der Aufruf dieser Methode wunderbar geklappt.

Versuchen wir im nächsten Schritt nun unsere private Methode getAttribute2() aufzurufen, indem wir in der getMethod() den gewünschten Methodennamen angeben:

$object = new Example();

$reflection_object = new ReflectionObject($object);
$method = $reflection_object->getMethod("getAttribute2");

try {
	echo $method->invoke($object);
	
} catch (ReflectionException $e) {
	echo "Forbidden!";
}

Als Ausgabe erhalten wir nun „Forbidden!“ wie in unserem Exception-Handling definiert. Dies liegt – wie erwartet –daran, dass man auf private Methoden außerhalb der instanziierten Klasse nicht direkt zugreifen kann.

Genau für diesen Fall bietet die Reflection-API mit der setAccessible()-Methode jedoch ein nützliches Hilfsmittel.

$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!";
}

Nun erhalten wir als Ausgabe die Zahl „36“, wie in unserer Klasse deklariert.

Mittels setAccessible() lässt sich das private Verhalten der Methode für die weitere Ausführung von dieser deaktivieren.

Allerdings ist dies für die Praxis nicht sehr empfehlenswert, da es meistens Gründe hat, warum manche Methoden als privat definiert wurden. Eine mögliche Verwendung von solchen Techniken könnte beispielsweise für Unit-Tests interessant werden.

Weiteres Beispiel: Entitäten-Objekt mit Auto-Fill

Mit diesem Beispiel möchten wir euch nun die Anwendung der Reflection-API in der Praxis zeigen. Um das entsprechend zu veranschaulichen, werden wir anhand eines Entitäten-Objekts demonstrieren, wie man es auch bereits von vielen Frameworks kennt.

Dazu bauen wir uns zuerst ein Basis-Element, von welchem wir später unsere individuellen Entitäten ableiten können. Wir gehen davon aus, dass jede Entität folgende Attribute besitzt (angelehnt an das Laravel-Framework):

  • id
  • createdAt
  • updatedAt
  • deletedAt
/**
 * 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;
	}

}

 

Auto-Fill implementieren

Wir haben nun eine Basisklasse, welche vier Attribute besitzt, mit jeweils den zugehörigen Getter- und Setter-Methoden. Im nächsten Schritt sorgen wir dafür, dass im Konstruktor ein Daten-Array übergeben werden kann, anhand dessen dann automatisch die entsprechenden Setter-Methoden ermittelt und ausgeführt werden.

Dazu schreiben wir uns zwei Helper-Klassen: eine für die String-Operationen bzgl. der Konvertierung von und zu Camel-Case, sowie eine für das 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;
	}

}

Widmen wir uns nun wieder unserer Basisklasse. Dazu erweitern wir den Konstruktor wie folgt:

/**
 * 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 );
				}

			}
		}
	}

}

Was hier passiert, ist relativ leicht zu erklären: es wird geprüft, ob ein Daten-Array übergeben wurde. Im positiven Fall wird dann geprüft, ob es sich möglicherweise um ein Objekt handelt (wie es beispielsweise beim Eloquent-ORM von Laravel der Fall wäre). Wenn ja, wird dieses zu einem Array konvertiert. Im nächsten Schritt wird dann über jedes einzelne übergebene Attribut mittels einer foreach-Schleife im Key-Value-Stil iteriert. Da jeweils der Index bekannt ist, und dieser das betreffende Attribut repräsentieren soll, gehen wir davon aus, dass die zugehörige Setter-Funktion im Stil „set<AttributName>“ benannt ist.

Sollte eine solche Setter-Methode in der aktuellen Objekt-Instanz tatsächlich existieren, wir diese mit der ReflectionMethod() instanziiert. Theoretisch wäre es nun bereits möglich, diese direkt zu verwenden. Allerdings müssen wir vorher noch den übergebenen Wert entsprechend casten, damit dieser jeweils mit dem erwarteten Datentyp der jeweiligen Setter-Methode übereinstimmt.

Dazu wird als nächstes mit Hilfe von getParameters() ausgelesen, welche Parameter an die betreffende Setter-Methode übergeben werden können. Wir gehen hier davon aus, dass jede dieser Methoden jeweils genau einen Parameter, also den zu setzenden Wert, übernehmen kann. Ist dieser Parameter definiert, wird nun mittels getType() der erwartete Datentyp retourniert, welcher über den PHP-Annotations definiert wurden. Diesen ermittelten Ziel-Datentyp übergeben wir nun unserer Helper-Klasse und erhalten die entsprechend gecastete Variable zurück.

Im letzten Schritt rufen wir nun die Setter-Methode mittels regulärem Zugriff darauf auf und übergeben dieser als Parameter den zuvor gecasteten Wert. Damit hätten wir im Grunde unsere Kernfunktionalität betreffend dem Auto-Fill implementiert.

Daten ausgeben

Um innerhalb unserer Basisklasse nun unter Nutzung der jeweiligen Getter-Methode zuzugreifen, schreiben wir uns eine kleine private Hilfs-Methode:

/**
 * 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;
}

Dieser können wir als Parameter den Namen des gewünschten Attributs übergeben. Ähnlich wie bereits beim Setzen der Werte zuvor, ermitteln wir den Namen der zugehörigen Getter-Methode unter der Annahme, dass dieser in Form von „get<AttributName>“ definiert wurde.

Existiert die Methode im aktuellen Objekt-Kontext, wird diese mittels der ReflectionMethod() ausgeführt, und als Resultat der Wert des gewünschten Attributs retourniert.

Damit wir für die Ausgabe aller Attribute auch dynamisch ermöglichen können, müssen wir wissen, welche Attribute generell in der aufgerufenen Klasse, sowie in der Basisklasse (als Elternklasse) vorhanden sind. Dazu definieren wir eine Methode namens getDocument(), welches genau dies macht:

/**
 * 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();
	}
}

Die Hilfs-Methode buildPropertyArray() dient in erster Linie dazu, doppelten Code zu vermeiden, und gleichzeitig auch dazu, Attribute, welche mit einem Unterstich („_“) beginnen, zu ignorieren. Attribute, welche mit einem Unterstrich beginnen, werden in diesem Fall nicht nach außen mittels getDocument() exportiert, bleiben also geheim.

Eigene Entitäten ableiten

Um nun mit Hilfe der Basisklasse eigene Entitäts-Klassen abzuleiten, ist es ausreichend, eine neue Klasse mit dem gewünschten Namen zu erstellen, welche von unserer zuvor erstellten Basisklasse erbt.

In dieser können nun nach dem gleichen Prinzip individuelle Attribute definiert werden – mehr ist hier nicht notwendig.

/**
 * 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;
	}

	
}

Um unsere Entitäts-Klasse zu testen, definieren wir ein kleines Daten-Array, mit ein paar Test-Werten. Dieses soll in unserem Fall eine externe Datenquelle simulieren.

$data = [
	"id"	=> 14,
	"title"	=> "Lorem ipsum",
];

Im nächsten Schritt initialisieren wir unsere neu erstellte Entitäts-Klasse mit diesem Daten-Array und können im Anschluss daran das vollständige Ergebnis mittels getDocument() exportieren:

$entity = new Dummy( $data );

print_r( $entity->getDocument() );

 

Fazit

Mittels Reflection kann in der modernen Softwareentwicklung das Programm „intelligent“ agieren, da dieses während der Laufzeit in der Lage ist, die Eigenschaften eigener Funktionen zu analysieren und sogar zu modifizieren.

Die Implementierung von Reflection in PHP ist jedoch so umfangreich, dass wir hier nur einen kleinen Teil der Möglichkeiten demonstriert haben. Es gibt noch unzählige weitere Funktionen, um den Programmcode noch weiter zu zerlegen.

Zusammengefasst können wir feststellen, dass man mittels Reflection eine sehr mächtige Technologie in die Hände gelegt bekommt, mit der man wirklich enorm viel bewerkstelligen kann.