Facebook
Twitter
Google+
Kommentare
17

Genie oder Wahnsinn: call_user_func_assoc_array

Wir hatten ja schon mal vor einiger Zeit einen Genie oder Wahnsinn Beitrag. Da ich heute mal wieder was gebastelt habe, will ich mit dieser Rubrik auch weiter machen.

Ihr kennt ja alle die PHP-Funktion call_user_func_array. Diese nimmt einen callback und eine Anzahl von sortierten Parametern und ruft das in Kombination auf. Die Parameter müssen dabei aber schon in der Reihenfolge sein, wie sie die aufzurufende Methode haben will. Ich fand das irgendwie doof. Ich würde gerne ein assotiatives Array reingeben und er baut  mir den Aufruf so zusammen, wie das passt. Also als Beispiel:

function doSomething($argument2, $argument1)
{
  echo 'Argument1: '.$argument1;
  echo 'Argument2: '.$argument2;
}  
call_user_func_assoc_array( 'doSomething', array( 'argument1' => 'arg1', 'argument2' => 'arg2' );

Ihr seht, dass ich die Parameter unten in der „falschen“ Reihenfolge drinnen habe. Ich habe heute also, weil ich es für ein Projekt gebraucht habe, diese Methode implementiert. Und zwar wie folgt:

function call_user_func_assoc_array($function, array $param_arr)
{
  if (is_array($function))
  {
    $object = $function[0];
    $method = $function[1];

    $reflectedListener = new \ReflectionClass($object);
    $reflectedMethod = $reflectedListener->getMethod($method);
    $parameters = $reflectedMethod->getParameters();
  }
  else
  {
    $reflectedFunction = new \ReflectionFunction($function);
    $parameters = $reflectedFunction->getParameters();
  }

  foreach ($parameters as $parameter)
  {
    $name = $parameter->getName();
    if (array_key_exists($name, $param_arr))
    {
      $orderedParameters[] = $param_arr[$name];
    }
    else
    {
      throw new \Exception('Parameter "'.$name.'" not set.');
    }
  }

  call_user_func_array($function, $orderedParameters);
}

Für die meisten Fälle funktioniert es auch schon. Die Syntax wie array( ‚B‘, ‚parent::doSomthing‘ ) als Callback muss man sich noch mal separat anschauen. Defensiver könnte das auch sein, aber es ist erst mal ein Prototyp, der vielleicht den ein oder anderen inspiriert. Die nächsten Tage werde ich dann auch noch präsentieren, wofür das gut war. Aber jetzt dürft ihr erst mal entscheiden, ob das eine total doofe Idee ist oder ob man so ein paar echt nette Probleme lösen kann.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

17 Comments

  1. Guten Morgen, also, find ich erstmal ziemlich pfiffig. Aber ist das Anlegen eines Reflection-Objectes und das Durchlaufen einer Schleife für den Aufruf einer Methode nicht irgendwie ziemlich teuer? Bin mal gespannt, worauf Du hinauswillst.
    VG, Carsten.

    Reply
  2. So wahnsinnig ist das garnicht. Symfony2 benutzt selbe Mechanik für das Routing (named parameters) und ich fände eine native implementierung von dieser Funktion gar nicht verkehrt. PDO hat ja auch seine bindParam-Gelöt, warum nicht auch Callbacks? 😉

    Reply
  3. Mir ist ehrlich gesagt noch nicht mal ein Fall untergekommen, bei dem ich call_user_func_array gebraucht hätte. Geschweige denn eine Alternative mit Argumenten in beliebiger Reihenfolge.

    Reply
  4. Nun ich denke das sieht nach einem Work-Around aus.
    Wenn eine Funktion oder Klasse nicht dafür konzipiert ist, Parameter in beliebiger Reihenfolge zu verwenden, dann sollte hier die Funktion angepasst werden.
    Aus Kompatibilitätsgründen kann man Reflektionen verwenden, aber ansonsten würde ich von dem Hin- und Hergestupse abraten.
    Außerdem denke ich das bei Verwendung einer IDE hier Zeit verloren geht.

    Zend verwendet in solchen Fällen als Parameter ein Array, und sucht sich entsprechend der definierten Keys die Werte der Parameter dazu heraus.
    Das Konzept verwende ich mittlerweile auch, gerade wenn es viele Parameter oder Parameter unbestimmter Reihenfolge sein können.

    Reflektionen sind auf alle Fälle eine schöne Sache wenn man so eine universale Lösung bauen möchte, der Ansatz ist also finde ich nicht verkehrt.

    Beachte bei der Instanzierung einer Reflektion den __construct falls er Parameter hat, dann sollte alles noch universeller sein 😉

    Reply
  5. @Michael:
    Das hängt sicher davon ab, woran du arbeitest. Faustregel dürfte sein: Wenn du eine Abstraktionsschicht für etwas baust, was beliebige Parameter entgegen nimmt, ist irgendwo ein call_user_func_array drin. Als Beispiel fällt mir auch sofort eine Datenbankabstraktionsschicht ein, die als Unterbau auch MySQLi nutzen können soll – MySQLi_STMT::bind_param und MySQLi_STMT::bind_result sind hier die „Übeltäter“.

    Reply
  6. Wenn die Methode so viele Parameter hat und man ggf. nur einen davon angeben muss/will, würde ich eher den Parameter als ein eigenes Objekt beschreiben.

    Allein der Gedanke daran, den Aufruf der Methode an deren Parameternamen zu binden, würde mir missfallen.

    Aber ansonsten ist Deine Implementierung schon lustig 😉

    Reply
  7. Ich benutze das ebenfalls für mein Routing.
    Gerade beim erkennen von Parametern in der URL kann man sich nicht immer auf die Reihenfolge des Auftauchens im späteren Array verlassen.

    Du solltest noch einen nicht vorhandenen Wert mit dem default Wert aus der Reflection befüllen lassen und erst danach in die Fehlerbehandlung wechseln.

    Reply
  8. Du hast die Standardbelegungen vergessen, wie der Kollege vor mir auch schon bemerkt hat. Es klappt außerdem nicht für Funktionen mit einer undefinierten Anzahl von Parametern.

    Nettes Thema – neu ist das allerdings nicht. Wir machen das bei unserem Routing auch genauso.

    Vermutlich machen es alle, die einen Auto-Discover-Mechanismen implementieren genau auf diese Weise oder ähnlich.

    Ich benutze das um bei einer Client-Server-Kommunikation eine transparente Schnittstelle auf beiden Seiten zu erzeugen. Client und Server kennen beide die Originalklasse. Der Client generiert sich über die Reflection einen Stub, der die Anfrage an den Server weiterleitet. Der Server findet dann die Originalfunktion, prüft die Parameterliste und ruft über ReflectionMethod::invokeArgs() die Implementierung auf.

    Ich würde dir im Übrigen empfehlen dein ‚call_user_func_array($function, $orderedParameters)‘ ebenfalls durch ‚$reflectedMethod->invokeArgs($orderedParameters)‘ zu ersetzen. Das ist sauberer.

    Ich für meinen Teil mach übrigens zusätzlich noch eine Prüfung des erwarteten Datentyps für jeden Parameter. Man muss die Objekte ja schließlich nach der Übertragung via HTTP wieder deserialisieren und die skalaren Variablen müssen/sollten auf den richtigen Typ gecastet werden.

    Reply
  9. Ach ja – das mit den Methoden als Objekte verpacken und zwischen Server und Client verschicken, anstelle einer Parameterliste, geht natürlich auch.

    Da sollte es auch ein GOF-Pattern geben: Strategy dürfte passen.

    Sieht ganz einfach aus:

    class MyMethod {
    private $p1;
    public function setP1($v) { $this->p1 = $v; }
    public function __invoke() { /* Implementierung */ }
    }

    class MyClient {
    // …
    public sendRequest($o) { $this->server->send(serialize($o)); }
    }

    class MyServer {
    public handleRequest($o) { $o = unserialize($o); $o->setDb($this->db); return $o(); }
    }

    Ist ein elegantes Pattern, welches ich für komplexe Suchabfragen verwende.
    Der Controller baut die Suche zusammen und übergibt an den Client. Der Client serialisiert das Objekt und schickt es an den Server. Der Server packt das Objekt wieder aus, übergibt nur noch seine Datenbankverbindung und führt die Suche aus. Schnittstelle und Implementierung der eigentlichen Funktion bleiben für Server und Client völlig transparent.
    Für bestimmte Anwendungsfälle ist das eine sehr elegante Lösung.

    Reply
  10. Ja, habe ich. Das Yana Framework arbeitet zum Beispiel so. Da gibt es einen Auto-Discover-Mechanismus für das Plugin-System. Allerdings habe ich dazu die PHP-Reflection-API erweitert. Diese verarbeitet damit auch die @param und @return Annotations im Code.

    Du kannst meiner Reflection die Parameter übergeben und sie ruft die Funktion direkt auf. Dadurch erreichst du eine bessere Kapselung. Etwa in dieser Form:

    $method = new MyReflectionMethod($object, $metodName);
    $method->setParams($array); // throws InvalidValueWarning
    $method->invoke($object);

    Da ich schon mal dabei war, kann die Implementierung aber noch deutlich mehr. Sie findet zum Beispiel alle Implementierungen einer Funktion unabhängig von der Klasse und beherrscht Broadcast und Multicast.
    Das ganze Framework ist Open-Source (yanaframework.net) und du kannst dir da in der aktuellen Development-Version angucken, wie es gelöst ist.

    Aber: für deine Zwecke hat meine Lösung einfach viel zu viele Features.

    Als einführendes Beispiel in das Thema genügt das hier ja schon völlig, wenn man noch die von Clemens genannten Fehler beseitigt. Die Default-Parameter müssen zwingend berücksichtigt werden, sonst bringt das nichts. 😉

    Reply
  11. Hier ist übrigens ein leicht modifiziertes Beispiel aus dem PHP-Manual:

    $reflection->invokeArgs($object, ‚methodNameHere‘, array(‚arg3‘ => ‚three‘, ‚arg1‘ => ‚one‘));

    public function invokeArgs($object, $method, array $args = array())
    {
    $reflection = new ReflectionMethod($object, $method);

    $pass = array();
    foreach($reflection->getParameters() as $param)
    {
    /* @var $param ReflectionParameter */
    if(isset($args[$param->getName()]))
    $pass[] = $args[$param->getName()];
    else
    $pass[] = $param->getDefaultValue();
    }
    return $reflection->invokeArgs($object, $pass);
    }

    Wenn man ReflectionMethod erweitert, kann man die Methode invokeArgs() in dieser Form überschreiben. Dann funktioniert es so, wie in dem von mir beschriebenen Beispiel, dass ich auch im Yana Framework verwende.

    Reply
  12. Ich wollte mal darauf aufmerksam machen dass dein normaler Rss Feed mit phm|network einen XML Fehler aufweist und deshalb leider nicht von meinen RSS Reader gelesen werden kann.
    Ansonsten habe ich leider nichts zu diesem Thema zu sagen 😉
    Paloran

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen