mitmario.dev

Verzeichnisse

PHP Sandbox 4 Min Lesezeit 5 BeispieleLektion 5 von 7

Bis hierhin hast du Dateien beim Namen genannt. Sobald ein Ordner Uploads enthält, Protokolle oder Bilder, geht das nicht mehr: Du weißt vorher nicht, was drinliegt, und musst fragen.

Eine Zeile in den Beispielen ist neu, und sie gehört nicht zum Thema: require "vorbereiten.php" ganz oben. require holt eine andere PHP-Datei herein und führt sie aus, als stünde ihr Inhalt an genau dieser Stelle. In vorbereiten.php legt der Kurs den Ordner daten/ samt Inhalt an, damit es überhaupt etwas aufzulisten gibt; ohne die Zeile stünde derselbe Code in jedem Beispiel noch einmal. Ausführlich kommt require in Lektion 10.6 dran, zusammen mit seinem leiseren Bruder include.

scandir gibt dir alles, wirklich alles

Was scandir wirklich zurückgibt
<?php

require "vorbereiten.php";

var_dump(scandir("daten"));

echo "Eintraege insgesamt: ", count(scandir("daten")), "\n";

scandir() liefert eine Liste der Namen in einem Verzeichnis, alphabetisch sortiert. Nur Namen, keine Pfade.

Und die ersten beiden Einträge sind immer . und ... Das sind keine Dateien, sondern das Verzeichnis selbst und das darüber. Jedes Betriebssystem führt sie, und scandir verschweigt sie nicht. Wer sie nicht aussortiert, versucht früher oder später, sie als Datei zu lesen. Die Shell kennt die beiden auch: ls -a daten im Terminal zeigt sie an erster Stelle, ls daten lässt sie weg, und genau diese Auswahl triffst du in PHP selbst.

Zwei Wege, sie loszuwerden. Der eine ist ein continue ganz oben in der Schleife, wie in Lektion 4.6. Der andere ist array_diff($liste, [".", ".."]): Die Funktion gibt dir die erste Liste ohne alles zurück, was in der zweiten steht. Sie behält dabei die Schlüssel, deshalb steht in den Beispielen array_values() daneben.

glob nimmt ein Muster

glob nimmt ein Muster
<?php

require "vorbereiten.php";

echo "Nur die Textdateien:\n";
var_dump(glob("daten/*.txt"));

echo "\nAlles, was drinliegt:\n";
var_dump(glob("daten/*"));

glob() beantwortet eine andere Frage: nicht „was liegt da”, sondern „was passt auf dieses Muster”. Der Stern steht für beliebig viele Zeichen, das Fragezeichen für genau eines.

Drei Unterschiede zu scandir, und alle drei zählen im Alltag:

  • glob gibt Pfade zurück, nicht bloße Namen. daten/liste.txt statt liste.txt. Das ist meist praktischer, denn damit kannst du sofort weiterarbeiten.
  • . und .. sind nicht dabei. Das Muster passt nicht auf sie.
  • Ein Muster wie *.txt sortiert Verzeichnisse gleich mit aus, jedenfalls solange keines auf .txt endet. Ohne Muster, also mit daten/*, sind sie wieder dabei.

Das Muster ist übrigens dasselbe wie in der Shell: ls daten/*.txt im Terminal zeigt dieselben drei Pfade, nur löst dort die Shell den Stern auf, bevor ls überhaupt läuft.

Ordner aussieben

Zählen, was im Ordner liegt
<?php

require "vorbereiten.php";

$dateien = 0;
$ordner = 0;

foreach (scandir("daten") as $eintrag) {
    if ($eintrag === "." || $eintrag === "..") {
        continue;
    }

    if (is_dir("daten/" . $eintrag)) {
        $ordner++;
        echo "Ordner: ", $eintrag, "\n";
        continue;
    }

    $dateien++;
    echo "Datei:  ", $eintrag, "\n";
}

echo "\n", $dateien, " Dateien und ", $ordner, " Ordner\n";

Ob ein Eintrag ein Verzeichnis ist, beantwortet is_dir(). Wichtig dabei: Die Funktion braucht den vollen Pfad, nicht den Namen aus scandir. is_dir("archiv") sucht im Arbeitsverzeichnis und findet nichts, is_dir("daten/archiv") findet den Ordner.

Das ist der häufigste Fehler in dieser Lektion, und er ist besonders unangenehm, weil is_dir einfach false sagt. Kein Fehler, keine Warning. Dein Programm behandelt den Ordner dann als Datei.

Wenn du beim Durchgehen mitzählst, hast du am Ende eine Zuordnung von Endung auf Anzahl, und die willst du meistens nach der Anzahl sortiert ausgeben. Dafür gibt es arsort(), das Gegenstück zu asort() aus Lektion 5.5: nach Wert sortiert, absteigend, und die Schlüssel bleiben dran. Genau das brauchst du in der Challenge.

Zusammengesetzt wird ein Pfad mit dem normalen Punkt aus Lektion 3.2: $ordner . "/" . $eintrag. Der Schrägstrich funktioniert auch unter Windows, ein Backslash umgekehrt nicht überall.

Auf welche Reihenfolge du dich verlassen kannst

Alphabetisch ist nicht dasselbe wie numerisch
<?php

@mkdir("bilder");
foreach (["bild1.png", "bild2.png", "bild10.png", "bild20.png"] as $name) {
    file_put_contents("bilder/" . $name, "x");
}

$namen = array_values(array_diff(scandir("bilder"), [".", ".."]));

echo "So sortiert scandir:\n";
echo implode(", ", $namen), "\n";

echo "\nRueckwaerts geht auch:\n";
$rueckwaerts = array_diff(scandir("bilder", SCANDIR_SORT_DESCENDING), [".", ".."]);
echo implode(", ", $rueckwaerts), "\n";

echo "\nUnd so, wie ein Mensch es erwartet:\n";
natsort($namen);
echo implode(", ", $namen), "\n";

scandir sortiert von sich aus alphabetisch, und mit SCANDIR_SORT_DESCENDING als zweitem Argument andersherum. glob sortiert ebenfalls. Insofern ist die Reihenfolge verlässlich.

Nur ist alphabetisch nicht das, was ein Mensch erwartet: bild10.png steht vor bild2.png, weil die 1 vor der 2 kommt und danach nicht mehr weitergezählt wird. Sobald Zahlen in Dateinamen stecken, brauchst du natsort(), das eine Ziffernfolge als Zahl liest. Die Shell hat dieselben zwei Sortierungen: ls bilder im Terminal ordnet wie scandir, ls -v bilder wie natsort.

Namen zerlegen

Namen zerlegen
<?php

$pfad = "daten/archiv/alt.txt";

echo "basename:      ", basename($pfad), "\n";
echo "dirname:       ", dirname($pfad), "\n";
echo "ohne Endung:   ", basename($pfad, ".txt"), "\n\n";

var_dump(pathinfo($pfad));

echo "\nNur die Endung: ", pathinfo("archiv.tar.gz", PATHINFO_EXTENSION), "\n";

echo "Und bei einer Datei ganz ohne Punkt:\n";
var_dump(pathinfo("liesmich", PATHINFO_EXTENSION));

Drei Funktionen, die dir das Zerschneiden von Pfaden abnehmen:

  • basename() gibt den Dateinamen ohne Verzeichnis. Mit einem zweiten Argument schneidet es zusätzlich die Endung ab.
  • dirname() gibt das Verzeichnis ohne Dateinamen. Genau das benutzt vorbereiten.php in den Beispielen, um den Ordner anzulegen.
  • pathinfo() gibt alles auf einmal als Zuordnung: dirname, basename, extension, filename. Mit einer Konstante als zweitem Argument bekommst du nur ein Feld.

Zwei Dinge zur Endung, die man einmal gesehen haben muss: Bei archiv.tar.gz ist die Endung gz und nicht tar.gz, denn pathinfo schneidet am letzten Punkt. Und bei einer Datei ganz ohne Punkt bekommst du eine leere Zeichenkette zurück, keinen Fehler.

Und noch etwas Wichtiges: Eine Endung ist nur ein Teil des Namens. Sie sagt nichts darüber aus, was wirklich in der Datei steht. Wer sich beim Hochladen darauf verlässt, baut sich eine Lücke, und genau darum geht es in Abschnitt 15.

Zum Mitnehmen

Bisher kanntest du den Namen jeder Datei, die du gelesen hast. Ab jetzt fragst du das Verzeichnis, was überhaupt drinliegt, und musst dabei zwei Einträge aussortieren, die immer dabei sind.

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 5 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.