mitmario.dev

Eine zweite Abhängigkeit

PHP Sandbox 3 Min Lesezeit 3 BeispieleLektion 4 von 6

In Lektion 9.3 hast du ein Paket installiert. In vendor/ lagen danach drei. Diese Lektion erklärt, warum, und was daraus folgt.

Pakete benutzen selbst Pakete

Ein Wunsch, drei Pakete
<?php

$wunsch = json_decode(file_get_contents("composer.json"), true);
$tatsache = json_decode(file_get_contents("composer.lock"), true);

$gewuenscht = [];
foreach ($wunsch["require"] as $name => $bereich) {
    // php ist keine Bibliothek, sondern eine Bedingung an die
    // Laufzeit. Es zaehlt hier deshalb nicht mit.
    if ($name !== "php") {
        $gewuenscht[$name] = $bereich;
    }
}

echo "Gewuenscht:  ", count($gewuenscht), "\n";
echo "Installiert: ", count($tatsache["packages"]), "\n\n";

foreach ($tatsache["packages"] as $paket) {
    $woher = isset($gewuenscht[$paket["name"]]) ? "selbst gewaehlt" : "mitgebracht";
    echo str_pad($paket["name"], 20), str_pad($paket["version"], 10), $woher, "\n";
}

Die Autoren von ramsey/uuid haben sich beim Rechnen mit sehr großen Zahlen an dieselbe Regel gehalten wie du: nicht selbst schreiben, was es schon gibt. Also steht in ihrer composer.json ein require auf brick/math, und in brick/math steckt vielleicht noch etwas anderes.

Das nennt sich Abhängigkeitsbaum, und Composer läuft ihn vollständig ab, bevor es irgendetwas lädt. Erst wenn eine Kombination gefunden ist, mit der alle Bedingungen gleichzeitig erfüllt sind, wird heruntergeladen. Deshalb liegen in vendor/ mehr Ordner, als in deiner composer.json stehen. ls vendor im Terminal zeigt brick, composer und ramsey: Der mittlere gehört Composer selbst, dort liegt der Autolader, und unter ramsey liegen zwei Pakete, ls vendor/ramsey zeigt sie.

Das Beispiel liest beide Listen und stellt sie gegenüber: ein selbst gewähltes Paket, zwei mitgebrachte. Ansehen kannst du dir denselben Baum auch mit einem Befehl: Tipp composer show --tree ins Terminal oder nimm ihn aus der Befehlsleiste, während dieses Beispiel im Editor vorn liegt. Vier Bedingungen stehen darin:

  • ramsey/uuid verlangt brick/math im Bereich >=0.8.16 <=0.18
  • und ramsey/collection als ^1.2 || ^2.0, wobei der doppelte senkrechte Strich „oder” heißt
  • dazu selbst php ^8.0
  • brick/math wiederum verlangt php ^8.2, ramsey/collection verlangt php ^8.1

Jede Zeile ist eine Bedingung, und alle müssen gleichzeitig stimmen. Die drei Angaben zu PHP sind dabei die interessantesten: Auf einem Hosting mit PHP 8.1 wäre hier schon Schluss, weil brick/math mehr verlangt. Composer sagt das vor dem Herunterladen und nicht danach. Andersherum fragt composer why brick/math: Wer hat dieses Paket verlangt? Die Antwort ist eine Zeile, ramsey/uuid 4.9.3 requires brick/math (>=0.8.16 <=0.18). Die Versionen sind die aus der Maschine dieses Kurses; auf deinem Rechner können sie neuer sein, die Form bleibt.

Nicht die neueste, sondern die neueste, die passt

Composer holt nicht stur die höchste Versionsnummer. Es sucht die höchste, mit der alle Bedingungen zusammen aufgehen. Steht in deiner composer.json eine ältere Version von brick/math fest, dann nimmt Composer eben eine etwas ältere Version von ramsey/uuid, nämlich die letzte, die mit deiner Festlegung noch einverstanden ist.

Geht das nicht mehr auf, bricht die Installation ab. Composer nennt dann beide Seiten samt der Bedingung, an der es scheitert, und schlägt vor, welche Angabe man lockern könnte. Das ist der Moment, in dem eine feste Versionsnummer in der eigenen composer.json sich rächt: Sie hält nicht nur dein Paket fest, sondern auch alles, was davon abhängt.

Und ein Paket, das nichts mitbringt

Und eines, das nichts mitbringt
<?php

// Dieselbe Auswertung, ein anderes Paket.

$wunsch = json_decode(file_get_contents("composer.json"), true);
$tatsache = json_decode(file_get_contents("composer.lock"), true);

$gewuenscht = [];
foreach ($wunsch["require"] as $name => $bereich) {
    if ($name !== "php") {
        $gewuenscht[$name] = $bereich;
    }
}

echo "Gewuenscht:  ", count($gewuenscht), "\n";
echo "Installiert: ", count($tatsache["packages"]), "\n\n";

foreach ($tatsache["packages"] as $paket) {
    echo str_pad($paket["name"], 20), $paket["version"], "\n";
}

nikic/fast-route ist das Gegenstück. Ein Wunsch, ein Paket, kein Baum. Die einzige Bedingung darin ist eine PHP-Version, und die ist so alt, dass sie heute jeder erfüllt: composer show --tree zeigt hier eine Zeile mit php >=5.4.0 darunter, sonst nichts.

Das ist eine Eigenschaft, auf die man beim Aussuchen achten kann. Ein Paket ohne Abhängigkeiten ist leichter zu überblicken, aktualisiert sich seltener und kann dir keine Überraschung ins Projekt holen, die du nie ausgesucht hast. Umgekehrt ist ein Baum kein Grund zur Panik: Die drei Pakete von ramsey/uuid sind alle von derselben Sorte Leuten gepflegt.

Die Frage, die man vor jedem require stellen sollte, lautet deshalb nicht „hat es Abhängigkeiten?”, sondern:

  • Brauche ich das wirklich, oder sind es zehn Zeilen eigener Code?
  • Wird das Paket gepflegt? Auf Packagist steht, wann die letzte Version erschien.
  • Wie viel kommt mit? Und kenne ich, was mitkommt, wenigstens dem Namen nach?

Bei einer Kurzform für die Adresszeile aus Lektion 9.1 lautet die Antwort „Paket”. Bei einer Funktion, die zwei Zahlen addiert, lautet sie „selbst schreiben”, auch wenn es dafür ein Paket gibt.

Der Router, den du gleich brauchst

Wie ein Router einen Pfad einordnet
<?php

require __DIR__ . "/vendor/autoload.php";

$dispatcher = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {
    $r->addRoute("GET", "/preise", "preisliste");
    $r->addRoute("GET", "/bestellung/{nr:\d+}", "bestellseite");
});

foreach (["/preise", "/bestellung/77", "/bestellung/abc"] as $pfad) {
    echo $pfad, ":\n";
    var_dump($dispatcher->dispatch("GET", $pfad));
    echo "\n";
}

nikic/fast-route beantwortet eine einzige Frage: Zu welchem Stück Code gehört dieser Pfad? Genau das braucht man ab Abschnitt 16, und dort baust du es selbst. Hier siehst du zuerst, wie es aussieht, wenn es jemand fertig gebaut hat.

Zwei Dinge stehen im Beispiel:

  • simpleDispatcher() bekommt eine anonyme Funktion aus Lektion 6.6, und darin werden mit addRoute() die Regeln angemeldet. Jede Regel besteht aus Methode, Pfadmuster und einem Wert, der für dich steht: hier eine Zeichenkette, später der Name einer Funktion.
  • In geschweiften Klammern steht ein Platzhalter. {nr:\d+} heißt: Hier darf etwas stehen, es muss aus Ziffern bestehen, und es heißt nr. Ohne den Doppelpunkt und das Muster dahinter passt alles.

dispatch() gibt ein Array zurück, und das var_dump im Beispiel zeigt seinen Aufbau. An Stelle 0 steht die Auskunft: 1 heißt gefunden, 0 heißt nicht gefunden, 2 heißt „den Pfad gibt es, aber nicht mit dieser Methode”. Wer die Zahlen nicht auswendig lernen will, vergleicht mit den Konstanten FastRoute\Dispatcher::FOUND und FastRoute\Dispatcher::NOT_FOUND.

An Stelle 1 steht bei einem Treffer dein Wert, an Stelle 2 die Platzhalter als Zuordnung, also ["nr" => "77"]. Sieh dir dort den Typ an: Es ist eine Zeichenkette, keine Zahl, obwohl das Muster nur Ziffern erlaubt hat. Alles, was aus einer Adresse kommt, ist Text, und das bleibt in Abschnitt 10 und 11 so.

Beim dritten Pfad im Beispiel ist das Array dagegen nur einen Eintrag lang. Das ist kein Sonderfall, den du abfangen musst, sondern die Folge daraus, dass es nichts weiter zu sagen gibt: kein Treffer, kein Wert, keine Platzhalter. Wer nach dem Vergleich mit FOUND aussteigt, greift auf die Stellen 1 und 2 ohnehin nicht mehr zu.

Zum Mitnehmen

Du installierst ein Paket und bekommst drei. Das ist kein Fehler und kein Trick, sondern die Rechnung, die man vor jedem require aufmachen sollte: Wie viel fremder Code kommt für diese eine Zeile ins Projekt?

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.