PHP: Type Hints

6. Oktober 2015

Wer schon einmal mit PHP 5+ gearbeitet hat, kennt sie bestimmt: Type-Hints. Es gibt sie für arrays, Klassen & Schnittstellen, callables und … das war es schon. Type-Hints für primitive Datentypen gibt es nicht:

Type Hints können nicht mit skalaren Typen wie int oder string verwendet werden.

Das ändert sich mit PHP 7, welches im November erscheint. Aber nicht alle werden und können direkt auf PHP 7 wechseln (obwohl es sich durchaus lohnt). Doch es gibt eine Möglichkeit, die auch für PHP 5+ funktioniert.

Nicht passende Type-Hints werfen einen E_RECOVERABLE_ERROR Fehler, also etwas, das man abfangen kann. Dank PHP’s set_error_handler Funktion kann man sich in den Prozess einklinken, den Fehler abfangen und selbst eine Überprüfung durchführen.
Nehmen wir an, wir wollen das folgende Programm mit dem Type-Hint int benutzen:

<?php
	
function print_age(int $age) {
	print 'Ich bin ' . $age . ' Jahre alt.';
}

print_age(28);

Das würde, ohne unser zutun, in PHP 5+ folgenden Fehler werfen:

Catchable fatal error: Argument 1 passed to print_age() must be an instance of int, integer given, called in /code/YstJGO on line 7 and defined in /code/YstJGO on line 3

(Man beachte das Catchable)

Nun benutzen wir den set_error_handler, um in den Prozess einzugreifen. Dazu verwenden wir folgendes Snippet:

<?php

if (version_compare(PHP_VERSION, '7.0.0', '<')) {
    define('BASIC_TYPE_HINT_REGEX', '#^Argument \d+ passed to .+? must be an instance of ([a-z]+), ([a-z]+) given#i');

    function basic_type_hint($err_lvl, $err_msg)
    {
        if ($err_lvl === E_RECOVERABLE_ERROR) {
            if (preg_match(BASIC_TYPE_HINT_REGEX, $err_msg, $matches)) {
                if ($matches[1] === $matches[2]) {
                    return true;
                }

                switch ($matches[1]) {
                    case 'int':
                        return $matches[2] === 'integer';
                    case 'bool':
                        return $matches[2] === 'boolean';
                    case 'float': // Since PHP treats float as double
                        return $matches[2] === 'double';
                    default:
                        return false;
                }
            }
        }

        return false;
    }

    set_error_handler('basic_type_hint');
}

Kurze Beschreibung: Sofern wir uns in einer Version unter PHP 7.0.0 befinden, überprüft dieses Snippet die Fehler-Meldung auf einen typischen Type-Hint Fehler und ermittelt die relevanten Informationen. PHP erwartet, dank unseres int Type-Hints, eine Klasse vom Typ int, bekommt aber tatsächlich einen integer, was natürlich keine Klassen-Instanz ist.
Wir vergleichen nun das erwartete mit dem übergebenen Typ. Stimmt dies mit unserer Erwartung überein, dann geben wir true zurück, brechen also die Fehler-Prozedur an dieser Stelle ab.
Und schon bekommen wir die erwartete Ausgabe: Ich bin 28 Jahre alt.

Möchte man dieses Verhalten auch in eigenen Namensräumen nutzen, muss man den Code jedoch an zwei Stellen ein wenig anpassen.
Der Regex würde sich wie folgt ändern:

define('BASIC_TYPE_HINT_REGEX', '#^Argument \d+ passed to .+? must be an instance of ([a-z]+[\w_\\\]*), ([a-z]+)(?:\sof .+?)? given#i');

Der relevante Teil ist dieser: ([a-z]+[\w_\\\]*). Wir akzeptieren also nicht nur wie bisher [a-z]+, sondern auch Alpha nummerische Zeichen sowie Backslashes dahinter.

Die zweite Änderung kommt direkt nach dieser Zeile:

if (preg_match(BASIC_TYPE_HINT_REGEX, $err_msg, $matches)) {

Um den tatsächlichen Type-Hint herauszubekommen, müssen wir die optionalen Namensräume, sofern gegeben, splitten:

if (strpos($matches[1], '\\') !== false) {
    $arr        = explode('\\', $matches[1]);
    $matches[1] = array_pop($arr);
}

Also sieht der ganze Code jetzt folgendermaßen aus:

<?php

if (version_compare(PHP_VERSION, '7.0.0', '<')) {
    define('BASIC_TYPE_HINT_REGEX', '#^Argument \d+ passed to .+? must be an instance of ([a-z]+[\w_\\\]*), ([a-z]+)(?:\sof .+?)? given#i');

    function basic_type_hint($err_lvl, $err_msg)
    {
        if ($err_lvl === E_RECOVERABLE_ERROR) {
            if (preg_match(BASIC_TYPE_HINT_REGEX, $err_msg, $matches)) {
                if (strpos($matches[1], '\\') !== false) {
                    $arr        = explode('\\', $matches[1]);
                    $matches[1] = array_pop($arr);
                }

                if ($matches[1] === $matches[2]) {
                    return true;
                }

                switch ($matches[1]) {
                    case 'int':
                        return $matches[2] === 'integer';
                    case 'bool':
                        return $matches[2] === 'boolean';
                    case 'float':
                        return $matches[2] === 'double';
                    case 'object':
                        return $matches[2] === 'instance';
                    default:
                        return false;
                }
            }
        }

        return false;
    }

    set_error_handler('basic_type_hint');
}

Ich hoffe, das dies für den ein oder anderen eine Hilfe ist. Für mich und meine Kollegen war es das jedenfalls. 🙂

1 Comment
  • Pingback: Compilerbau, Game-Design und mehr » Dynamische Typisierung: Vorteile von PHP

  • Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.


    *