Media

Medien

Wir haben versucht, so viel wie möglich von den verschiedenen Medientypen im Internet rund um Flight zu finden. Siehe unten für verschiedene Ressourcen, die Sie nutzen können, um mehr über Flight zu erfahren.

Artikel und Berichte

Videos und Tutorials

Fehlt etwas?

Fehlt etwas, das Sie geschrieben oder aufgenommen haben? Lassen Sie es uns mit einem Issue oder Pull Request wissen!

Examples

Brauchen Sie einen schnellen Start?

Sie haben zwei Möglichkeiten, um mit einem neuen Flight-Projekt zu beginnen:

Von der Community beigesteuerte Beispiele:

Brauchen Sie etwas Inspiration?

Auch wenn diese nicht offiziell vom Flight-Team gesponsert werden, können sie Ihnen Ideen geben, wie Sie Ihre eigenen Projekte strukturieren können, die mit Flight erstellt wurden!

Möchten Sie Ihr eigenes Beispiel teilen?

Wenn Sie ein Projekt teilen möchten, reichen Sie bitte einen Pull-Request ein, um es zu dieser Liste hinzuzufügen!

Guides/unit_testing

Unit-Tests in Flight PHP mit PHPUnit

Diese Anleitung führt in Unit-Tests in Flight PHP mit PHPUnit ein und richtet sich an Anfänger, die verstehen möchten, warum Unit-Tests wichtig sind und wie man sie praktisch anwendet. Wir konzentrieren uns darauf, Verhalten zu testen – sicherzustellen, dass Ihre Anwendung das tut, was Sie erwarten, wie das Senden einer E-Mail oder das Speichern eines Datensatzes – und nicht auf triviale Berechnungen. Wir beginnen mit einem einfachen Route-Handler und gehen zu einem komplexeren Controller über, einschließlich Abhängigkeitsinjektion (DI) und dem Mocken von Diensten von Drittanbietern.

Warum Unit-Tests?

Unit-Tests stellen sicher, dass Ihr Code sich wie erwartet verhält, und fangen Fehler, bevor sie in die Produktion gelangen. Sie sind besonders wertvoll in Flight, wo leichtgewichtiges Routing und Flexibilität zu komplexen Interaktionen führen können. Für Einzelentwickler oder Teams dienen Unit-Tests als Sicherheitsnetz, dokumentieren erwartetes Verhalten und verhindern Regressionen, wenn Sie Code später erneut aufrufen. Sie verbessern auch das Design: Schwer zu testender Code deutet oft auf übermäßig komplexe oder eng gekoppelte Klassen hin.

Im Gegensatz zu vereinfachten Beispielen (z. B. Testen von x * y = z) konzentrieren wir uns auf reale Verhaltensweisen wie das Validieren von Eingaben, das Speichern von Daten oder das Auslösen von Aktionen wie E-Mails. Unser Ziel ist es, Tests zugänglich und aussagekräftig zu machen.

Allgemeine Leitprinzipien

  1. Verhalten testen, nicht Implementierung: Konzentrieren Sie sich auf Ergebnisse (z. B. „E-Mail gesendet“ oder „Datensatz gespeichert“) statt auf interne Details. Das macht Tests robust gegenüber Refactoring.
  2. Hören Sie auf, Flight:: zu verwenden: Die statischen Methoden von Flight sind sehr praktisch, erschweren aber das Testen. Sie sollten sich daran gewöhnen, die Variable $app aus $app = Flight::app(); zu verwenden. $app hat alle Methoden, die auch Flight:: hat. Sie können weiterhin $app->route() oder $this->app->json() in Ihrem Controller usw. verwenden. Sie sollten auch den echten Flight-Router verwenden mit $router = $app->router() und dann $router->get(), $router->post(), $router->group() usw. Siehe Routing.
  3. Halten Sie Tests schnell: Schnelle Tests fördern die häufige Ausführung. Vermeiden Sie langsame Operationen wie Datenbankaufrufe in Unit-Tests. Wenn ein Test langsam ist, ist das ein Zeichen dafür, dass Sie einen Integrationstest schreiben, keinen Unit-Test. Integrationstests sind Tests, bei denen echte Datenbanken, echte HTTP-Aufrufe, echtes E-Mail-Versenden usw. beteiligt sind. Sie haben ihre Berechtigung, aber sie sind langsam und können unbeständig sein, das heißt, sie schlagen manchmal aus unbekannten Gründen fehl.
  4. Verwenden Sie aussagekräftige Namen: Testnamen sollten das zu testende Verhalten klar beschreiben. Das verbessert Lesbarkeit und Wartbarkeit.
  5. Vermeiden Sie Globale wie die Pest: Minimieren Sie die Verwendung von $app->set() und $app->get(), da sie wie globaler Zustand wirken und in jedem Test Mocks erfordern. Bevorzugen Sie DI oder einen DI-Container (siehe Abhängigkeitsinjektions-Container). Auch die Verwendung der Methode $app->map() ist technisch gesehen ein „Global“ und sollte zugunsten von DI vermieden werden. Verwenden Sie eine Session-Bibliothek wie flightphp/session, damit Sie das Session-Objekt in Ihren Tests mocken können. Rufen Sie $_SESSION nicht direkt in Ihrem Code auf, da dies eine globale Variable in Ihren Code einbringt und das Testen erschwert.
  6. Verwenden Sie Abhängigkeitsinjektion: Injizieren Sie Abhängigkeiten (z. B. PDO, Mailer) in Controller, um Logik zu isolieren und Mocken zu vereinfachen. Wenn eine Klasse zu viele Abhängigkeiten hat, sollten Sie erwägen, sie in kleinere Klassen zu refaktorisieren, die jeweils gemäß den SOLID-Prinzipien eine einzige Verantwortung haben.
  7. Mocken Sie Dienste von Drittanbietern: Mocken Sie Datenbanken, HTTP-Clients (cURL) oder E-Mail-Dienste, um externe Aufrufe zu vermeiden. Testen Sie ein oder zwei Ebenen tief, aber lassen Sie Ihre Kernlogik laufen. Wenn Ihre App zum Beispiel eine Textnachricht sendet, möchten Sie NICHT wirklich jedes Mal eine Textnachricht senden, wenn Sie Ihre Tests ausführen, da sich diese Kosten summieren (und es langsamer wird). Stattdessen mocken Sie den Textnachrichtendienst und stellen nur sicher, dass Ihr Code den Textnachrichtendienst mit den richtigen Parametern aufgerufen hat.
  8. Zielen Sie auf hohe Abdeckung, nicht auf Perfektion: 100% Zeilenabdeckung ist gut, aber es bedeutet nicht wirklich, dass alles in Ihrem Code so getestet ist, wie es sein sollte (recherchieren Sie ruhig Zweig-/Pfadabdeckung in PHPUnit). Priorisieren Sie kritisches Verhalten (z. B. Benutzerregistrierung, API-Antworten und das Erfassen fehlgeschlagener Antworten).
  9. Verwenden Sie Controller für Routen: Verwenden Sie in Ihren Routendefinitionen Controller und keine Closures. Das flight\Engine $app wird standardmäßig über den Konstruktor in jeden Controller injiziert. Verwenden Sie in Tests $app = new Flight\Engine(), um Flight innerhalb eines Tests zu instanziieren, injizieren Sie es in Ihren Controller und rufen Sie Methoden direkt auf (z. B. $controller->register()). Siehe Flight erweitern und Routing.
  10. Wählen Sie einen Mocking-Stil und bleiben Sie dabei: PHPUnit unterstützt mehrere Mocking-Stile (z. B. Prophecy, integrierte Mocks), oder Sie können anonyme Klassen verwenden, die ihre eigenen Vorteile haben, wie Codevervollständigung, Fehler bei geänderter Methodendefinition usw. Seien Sie einfach konsistent in Ihren Tests. Siehe PHPUnit-Mock-Objekte.
  11. Verwenden Sie protected-Sichtbarkeit für Methoden/Eigenschaften, die Sie in Unterklassen testen möchten: Dies ermöglicht es Ihnen, sie in Testunterklassen zu überschreiben, ohne sie öffentlich zu machen. Das ist besonders nützlich für anonyme Klassen-Mocks.

Einrichten von PHPUnit

Richten Sie zuerst PHPUnit in Ihrem Flight-PHP-Projekt mit Composer ein, um einfaches Testen zu ermöglichen. Weitere Details finden Sie im PHPUnit-Getting-Started-Leitfaden.

  1. Führen Sie in Ihrem Projektverzeichnis aus:

    composer require --dev phpunit/phpunit

    Dies installiert die neueste PHPUnit-Version als Entwicklungsabhängigkeit.

  2. Erstellen Sie ein tests-Verzeichnis im Projektstamm für Testdateien.

  3. Fügen Sie composer.json ein Testskript hinzu, der Einfachheit halber:

    // weiterer Inhalt von composer.json
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Erstellen Sie eine phpunit.xml-Datei im Stammverzeichnis:

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

Wenn Ihre Tests erstellt sind, können Sie composer test ausführen, um die Tests auszuführen.

Testen eines einfachen Route-Handlers

Beginnen wir mit einer einfachen Route, die die E-Mail-Eingabe eines Benutzers validiert. Wir testen ihr Verhalten: Für gültige E-Mails wird eine Erfolgsmeldung zurückgegeben, für ungültige ein Fehler. Für die E-Mail-Validierung verwenden wir filter_var.

// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);

// UserController.php
class UserController {
    protected $app;

    public function __construct(flight\Engine $app) {
        $this->app = $app;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        $responseArray = [];
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $responseArray = ['status' => 'error', 'message' => 'Invalid email'];
        } else {
            $responseArray = ['status' => 'success', 'message' => 'Valid email'];
        }

        $this->app->json($responseArray);
    }
}

Um dies zu testen, erstellen Sie eine Testdatei. Siehe Unit-Tests und SOLID-Prinzipien für weitere Informationen zur Strukturierung von Tests:

// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;

class UserControllerTest extends TestCase {

    public function testValidEmailReturnsSuccess() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'test@example.com'; // Simuliere POST-Daten
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'invalid-email'; // Simuliere POST-Daten
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

Wichtige Punkte:

Führen Sie composer test aus, um zu überprüfen, ob die Route wie erwartet funktioniert. Weitere Informationen zu Requests und Responses in Flight finden Sie in der entsprechenden Dokumentation.

Verwenden von Abhängigkeitsinjektion für testbare Controller

Für komplexere Szenarien verwenden Sie Abhängigkeitsinjektion (DI), um Controller testbar zu machen. Vermeiden Sie die Globals von Flight (z. B. Flight::set(), Flight::map(), Flight::register()), da sie wie globaler Zustand wirken und für jeden Test Mocks erfordern. Verwenden Sie stattdessen den DI-Container von Flight, DICE, PHP-DI oder manuelle DI.

Verwenden wir flight\database\SimplePdo anstelle von rohem PDO. Dieser Helfer ist viel einfacher zu mocken und zu testen (und wird gegenüber dem veralteten PdoWrapper bevorzugt).

Hier ist ein Controller, der einen Benutzer in einer Datenbank speichert und eine Willkommens-E-Mail sendet:

use flight\database\SimplePdo;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            // das Hinzufügen des return hier hilft beim Unit-Testen, die Ausführung zu stoppen
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);

        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

Wichtige Punkte:

Testen des Controllers mit Mocks

Jetzt testen wir das Verhalten des UserController: Validieren von E-Mails, Speichern in der Datenbank und Senden von E-Mails. Wir mocken die Datenbank und den Mailer, um den Controller zu isolieren.

// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

        // Manchmal ist das Mischen von Mocking-Stilen notwendig
        // Hier verwenden wir den eingebauten Mock von PHPUnit für PDOStatement
        $statementMock = $this->createMock(PDOStatement::class);
        $statementMock->method('execute')->willReturn(true);
        // Verwenden einer anonymen Klasse zum Mocken von SimplePdo
        $mockDb = new class($statementMock) extends SimplePdo {
            protected $statementMock;
            public function __construct($statementMock) {
                $this->statementMock = $statementMock;
            }

            // Wenn wir es auf diese Weise mocken, führen wir keinen echten Datenbankaufruf durch.
            // Wir können dies weiter einrichten, um den PDOStatement-Mock zu ändern, um Fehler zu simulieren usw.
            public function runQuery(string $sql, array $params = []): PDOStatement {
                return $this->statementMock;
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                $this->sentEmail = $email;
                return true;    
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }

    public function testInvalidEmailSkipsSaveAndEmail() {
         $mockDb = new class() extends SimplePdo {
            // Ein leerer Konstruktor umgeht den Elternkonstruktor
            public function __construct() {}
            public function runQuery(string $sql, array $params = []): PDOStatement {
                throw new Exception('Should not be called');
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                throw new Exception('Should not be called');
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';

        // jsonHalt muss gemappt werden, um das Beenden zu vermeiden
        $app->map('jsonHalt', function($data) use ($app) {
            $app->json($data, 400);
        });
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('error', $result['status']);
        $this->assertEquals('Invalid email', $result['message']);
    }
}

Wichtige Punkte:

Zu viel mocken

Seien Sie vorsichtig, nicht zu viel von Ihrem Code zu mocken. Ich gebe Ihnen unten ein Beispiel, warum das schlecht sein kann, unter Verwendung unseres UserController. Wir ändern diese Prüfung in eine Methode namens isEmailValid (unter Verwendung von filter_var) und die anderen neuen Ergänzungen in eine separate Methode namens registerUser.

use flight\database\SimplePdo;
use flight\Engine;

// UserControllerDICV2.php
class UserControllerDICV2 {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!$this->isEmailValid($email)) {
            // das Hinzufügen des return hier hilft beim Unit-Testen, die Ausführung zu stoppen
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->registerUser($email);

        $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }

    protected function isEmailValid($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    protected function registerUser($email) {
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
    }
}

Und nun der übermäßig gemockte Unit-Test, der eigentlich nichts testet:

use PHPUnit\Framework\TestCase;

class UserControllerTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        // wir überspringen die zusätzliche Abhängigkeitsinjektion hier, weil es „einfach“ ist
        $controller = new class($app) extends UserControllerDICV2 {
            protected $app;
            // Abhängigkeiten im Konstruktor umgehen
            public function __construct($app) {
                $this->app = $app;
            }

            // Wir erzwingen einfach, dass dies gültig ist.
            protected function isEmailValid($email) {
                return true; // Immer true zurückgeben, echte Validierung umgehen
            }

            // Die tatsächlichen DB- und Mailer-Aufrufe umgehen
            protected function registerUser($email) {
                return false;
            }
        };
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
    }
}

Hurra, wir haben Unit-Tests und sie bestehen! Aber Moment, was, wenn ich tatsächlich die internen Abläufe von isEmailValid oder registerUser ändere? Meine Tests bestehen weiterhin, weil ich die gesamte Funktionalität gemockt habe. Lassen Sie mich zeigen, was ich meine.

// UserControllerDICV2.php
class UserControllerDICV2 {

    // ... andere Methoden ...

    protected function isEmailValid($email) {
        // Geänderte Logik
        $validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
        // Jetzt sollte es nur eine bestimmte Domain haben
        $validDomain = strpos($email, '@example.com') !== false; 
        return $validEmail && $validDomain;
    }
}

Wenn ich meine obigen Unit-Tests ausführe, bestehen sie immer noch! Aber weil ich nicht auf Verhalten getestet habe (also einen Teil des Codes tatsächlich habe laufen lassen), habe ich möglicherweise einen Fehler programmiert, der in der Produktion nur darauf wartet, aufzutreten. Der Test sollte geändert werden, um das neue Verhalten zu berücksichtigen, und auch das Gegenteil, wenn das Verhalten nicht unseren Erwartungen entspricht.

Vollständiges Beispiel

Ein vollständiges Beispiel eines Flight-PHP-Projekts mit Unit-Tests finden Sie auf GitHub: n0nag0n/flight-unit-tests-guide. Für ein tieferes Verständnis siehe Unit-Tests und SOLID-Prinzipien.

Häufige Fallstricke

Skalierung mit Unit-Tests

Unit-Tests glänzen in größeren Projekten oder wenn Sie Code nach Monaten erneut aufrufen. Sie dokumentieren Verhalten und fangen Regressionen ab, sodass Sie Ihre App nicht neu lernen müssen. Für Einzelentwickler: Testen Sie kritische Pfade (z. B. Benutzeranmeldung, Zahlungsabwicklung). Für Teams: Tests stellen konsistentes Verhalten über Beiträge hinweg sicher. Siehe Warum Frameworks? für weitere Vorteile von Frameworks und Tests.

Tragen Sie Ihre eigenen Testtipps zum Flight-PHP-Dokumentations-Repository bei!

Geschrieben von n0nag0n 2025

Guides/blog

Erstellen eines einfachen Blogs mit Flight PHP

In diesem Leitfaden erstellen Sie einen einfachen Blog mit dem Flight-PHP-Framework. Sie richten ein Projekt ein, definieren Routen, verwalten Beiträge mit JSON und rendern sie mit der Latte-Template-Engine – alles zeigt die Einfachheit und Flexibilität von Flight. Am Ende haben Sie einen funktionsfähigen Blog mit einer Startseite, einzelnen Beitragsseiten und einem Formular zum Erstellen.

Voraussetzungen

Schritt 1: Projekt einrichten

Beginnen Sie mit der Erstellung eines neuen Projektverzeichnisses und der Installation von Flight über Composer.

  1. Verzeichnis erstellen:

    mkdir flight-blog
    cd flight-blog
  2. Flight installieren:

    composer require flightphp/core
  3. Ein öffentliches Verzeichnis erstellen: Flight verwendet einen einzigen Einstiegspunkt (index.php). Erstellen Sie einen public/-Ordner dafür:

    mkdir public
  4. Basis-index.php: Erstellen Sie public/index.php mit einer einfachen „Hallo-Welt“-Route:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
        echo 'Hello, Flight!';
    });
    
    Flight::start();
  5. Den eingebauten Server ausführen: Testen Sie Ihre Einrichtung mit dem Entwicklungsserver von PHP:

    php -S localhost:8000 -t public/

    Besuchen Sie http://localhost:8000, um „Hello, Flight!“ zu sehen.

Schritt 2: Projektstruktur organisieren

Für eine saubere Einrichtung strukturieren Sie Ihr Projekt wie folgt:

flight-blog/
├── app/
│   ├── config/
│   └── views/
├── data/
├── public/
│   └── index.php
├── vendor/
└── composer.json

Schritt 3: Latte installieren und konfigurieren

Latte ist eine leichtgewichtige Template-Engine, die sich gut in Flight integrieren lässt.

  1. Latte installieren:

    composer require latte/latte
  2. Latte in Flight konfigurieren: Aktualisieren Sie public/index.php, um Latte als View-Engine zu registrieren:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::start();
  3. Ein Layout-Template erstellen: In app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
        <title>{$title}</title>
    </head>
    <body>
        <header>
            <h1>My Blog</h1>
            <nav>
                <a href="/">Home</a> | 
                <a href="/create">Create a Post</a>
            </nav>
        </header>
        <main>
            {block content}{/block}
        </main>
        <footer>
            <p>&copy; {date('Y')} Flight Blog</p>
        </footer>
    </body>
    </html>
  4. Eine Home-Vorlage erstellen: In app/views/home.latte:

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <ul>
         {foreach $posts as $post}
             <li><a href="/post/{$post['slug']}">{$post['title']}</a></li>
         {/foreach}
         </ul>
     {/block}

    Starten Sie den Server neu, falls Sie ihn beendet haben, und besuchen Sie http://localhost:8000, um die gerenderte Seite zu sehen.

  5. Eine Datendatei erstellen: Verwenden Sie eine JSON-Datei, um eine Datenbank der Einfachheit halber zu simulieren. In data/posts.json:

    [
        {
            "slug": "first-post",
            "title": "My First Post",
            "content": "This is my very first blog post with Flight PHP!"
        }
    ]

Schritt 4: Routen definieren

Lagern Sie Ihre Routen für eine bessere Organisation in eine Konfigurationsdatei aus.

  1. routes.php erstellen: In app/config/routes.php:

    <?php
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });
  2. index.php aktualisieren: Binden Sie die Routendatei ein:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    require '../app/config/routes.php';
    
    Flight::start();

Schritt 5: Blogbeiträge speichern und abrufen

Fügen Sie die Methoden zum Laden und Speichern von Beiträgen hinzu.

  1. Eine Posts-Methode hinzufügen: Fügen Sie in index.php eine Methode zum Laden von Beiträgen hinzu:

    Flight::map('posts', function () {
        $file = __DIR__ . '/../data/posts.json';
        return json_decode(file_get_contents($file), true);
    });
  2. Routen aktualisieren: Ändern Sie app/config/routes.php, um die Beiträge zu verwenden:

    <?php
    Flight::route('/', function () {
        $posts = Flight::posts();
        Flight::view()->render('home.latte', [
            'title' => 'My Blog',
            'posts' => $posts
        ]);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        $posts = Flight::posts();
        $post = array_filter($posts, fn($p) => $p['slug'] === $slug);
        $post = reset($post) ?: null;
        if (!$post) {
            Flight::notFound();
            return;
        }
        Flight::view()->render('post.latte', [
            'title' => $post['title'],
            'post' => $post
        ]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });

Schritt 6: Vorlagen erstellen

Aktualisieren Sie Ihre Vorlagen, um Beiträge anzuzeigen.

  1. Beitragsseite (app/views/post.latte):

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$post['title']}</h2>
         <div class="post-content">
             <p>{$post['content']}</p>
         </div>
     {/block}

Schritt 7: Beitragserstellung hinzufügen

Behandeln Sie die Formularübermittlung, um neue Beiträge hinzuzufügen.

  1. Formular erstellen (app/views/create.latte):

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <form method="POST" action="/create">
             <div class="form-group">
                 <label for="title">Title:</label>
                 <input type="text" name="title" id="title" required>
             </div>
             <div class="form-group">
                 <label for="content">Content:</label>
                 <textarea name="content" id="content" required></textarea>
             </div>
             <button type="submit">Save Post</button>
         </form>
     {/block}
  2. POST-Route hinzufügen: In app/config/routes.php:

    Flight::route('POST /create', function () {
        $request = Flight::request();
        $title = $request->data['title'];
        $content = $request->data['content'];
        $slug = strtolower(str_replace(' ', '-', $title));
    
        $posts = Flight::posts();
        $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content];
        file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT));
    
        Flight::redirect('/');
    });
  3. Testen Sie es:

    • Besuchen Sie http://localhost:8000/create.
    • Senden Sie einen neuen Beitrag (z. B. „Second Post“ mit etwas Inhalt).
    • Prüfen Sie die Startseite, um den Beitrag aufgelistet zu sehen.

Schritt 8: Mit Fehlerbehandlung verbessern

Überschreiben Sie die notFound-Methode für eine bessere 404-Erfahrung.

In index.php:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});

Erstellen Sie app/views/404.latte:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>Sorry, that page doesn't exist!</p>
{/block}

Nächste Schritte

Fazit

Sie haben einen einfachen Blog mit Flight PHP erstellt! Dieser Leitfaden zeigt Kernfunktionen wie Routing, Templating mit Latte und die Verarbeitung von Formularübermittlungen – und bleibt dabei leichtgewichtig. Entdecken Sie die Dokumentation von Flight für weitere fortgeschrittene Funktionen, um Ihren Blog noch weiter zu bringen!

Guides

Anleitungen

Flight PHP ist so konzipiert, dass es einfach und doch leistungsstark ist, und unsere Anleitungen helfen Ihnen, reale Anwendungen schrittweise zu erstellen. Diese praktischen Tutorials führen Sie durch vollständige Projekte, um zu demonstrieren, wie Flight effektiv eingesetzt werden kann.

Offizielle Anleitungen

Erstellen eines Blogs

Lernen Sie, wie Sie eine funktionale Blog-Anwendung mit Flight PHP erstellen. Diese Anleitung führt Sie durch:

Dieses Tutorial eignet sich perfekt für Anfänger, die sehen möchten, wie alle Teile in einer realen Anwendung zusammenpassen.

Einheitstests und SOLID-Prinzipien

Diese Anleitung behandelt die Grundlagen der Einheitstests in Flight PHP-Anwendungen. Dazu gehören:

Unoffizielle Anleitungen

Obwohl diese Anleitungen nicht offiziell vom Flight-Team gepflegt werden, sind sie wertvolle Ressourcen, die von der Community erstellt wurden. Sie decken verschiedene Themen und Anwendungsfälle ab und bieten zusätzliche Einblicke in die Nutzung von Flight PHP.

Erstellen einer RESTful API mit Flight Framework

Diese Anleitung führt Sie durch das Erstellen einer RESTful API unter Verwendung des Flight PHP-Frameworks. Sie behandelt die Grundlagen der API-Einrichtung, Definieren von Routen und Rückgabe von JSON-Antworten.

Erstellen eines einfachen Blogs

Diese Anleitung führt Sie durch das Erstellen eines grundlegenden Blogs unter Verwendung des Flight PHP-Frameworks. Es gibt tatsächlich zwei Teile: Einen für die Grundlagen und einen weiteren für fortgeschrittene Themen und Verbesserungen für einen produktionsreifen Blog.

Erstellen einer Pokémon API in PHP: Ein Leitfaden für Anfänger

Diese unterhaltsame Anleitung führt Sie durch das Erstellen einer einfachen Pokémon API unter Verwendung von Flight PHP. Sie behandelt die Grundlagen der API-Einrichtung, Definieren von Routen und Rückgabe von JSON-Antworten.

Beitrag leisten

Haben Sie eine Idee für eine Anleitung? Einen Fehler gefunden? Wir freuen uns über Beiträge! Unsere Anleitungen werden im FlightPHP-Dokumentationsrepository gepflegt.

Falls Sie etwas Interessantes mit Flight erstellt haben und es als Anleitung teilen möchten, reichen Sie bitte einen Pull-Request ein. Das Teilen Ihres Wissens hilft der Flight-Community zu wachsen.

Suche nach API-Dokumentation?

Falls Sie spezifische Informationen zu Flights Kernfunktionen und Methoden suchen, schauen Sie in den Learn-Bereich unserer Dokumentation.

About

Flight PHP Framework

Flight ist ein schnelles, einfaches und erweiterbares Framework für PHP—entwickelt für Entwickler, die Dinge schnell und ohne Umstände erledigen möchten. Ob Sie eine klassische Webanwendung, eine blitzschnelle API oder eine Kombination mit KI-Coding-Assistenten erstellen, Flights geringe Größe und unkompliziertes Design machen es zur perfekten Wahl. Flight ist schlank ausgelegt, kann aber auch Enterprise-Architekturanforderungen erfüllen.

Warum Flight wählen?

Video-Übersicht

Ziemlich einfach, oder?
Erfahren Sie mehr über Flight in der Dokumentation!

Schnellstart

Für eine schnelle Minimalinstallation installieren Sie es mit Composer:

composer require flightphp/core

Oder Sie können ein ZIP des Repositories hier herunterladen. Dann haben Sie eine grundlegende index.php-Datei wie die folgende:

<?php

// falls mit Composer installiert
require 'vendor/autoload.php';
// oder falls manuell per ZIP-Datei installiert
// require 'flight/Flight.php';

Flight::route('/', function() {
  echo 'hello world!';
});

Flight::route('/json', function() {
  Flight::json([
    'hello' => 'world'
  ]);
});

Flight::start();

Das war's! Sie haben eine grundlegende Flight-Anwendung. Sie können diese Datei nun mit php -S localhost:8000 ausführen und http://localhost:8000 in Ihrem Browser aufrufen, um die Ausgabe zu sehen.

Kurze Flight::-Beispiele wie dieses sind großartig zum Lernen und für Micro-Apps. Für ein vollständiges Projektlayout, das Menschen und KI-Tools teilen, verwenden Sie das Skeleton unten.

Skeleton/Boilerplate-App

Es gibt ein offizielles Starter-Template, um Ihnen den Einstieg in jedes neue Flight-Projekt zu erleichtern. Es richtet Struktur, Konfiguration, Composer-Skripte und KI-freundliche Anweisungen von Anfang an ein.

Schauen Sie sich flightphp/skeleton für ein sofort einsetzbares Projekt an oder besuchen Sie die Beispiele-Seite für Inspiration. Möchten Sie Details zum KI-Workflow? Entdecken Sie KI & Developer Experience.

Was Sie erhalten (Übersicht):

Installation der Skeleton-App

Ganz einfach!

# Erstellen Sie das neue Projekt
composer create-project flightphp/skeleton my-project/
# Wechseln Sie in das neue Projektverzeichnis
cd my-project/
# Starten Sie den lokalen Entwicklungsserver, um sofort loszulegen!
composer start

Es erstellt die Projektstruktur, kopiert config_sample.php → config.php (und .env.example → .env, falls vorhanden), und Sie können loslegen. Optionale Beispieldaten:

php runway migrate
# dann besuchen Sie /posts und /api/posts

Hohe Performance

Flight ist eines der schnellsten PHP-Frameworks. Sein schlankes Kernsystem bedeutet weniger Overhead und mehr Geschwindigkeit—perfekt für traditionelle Anwendungen und moderne, KI-unterstützte Workflows. Alle Benchmarks finden Sie auf TechEmpower

Sehen Sie den Benchmark unten mit einigen anderen beliebten PHP-Frameworks.

Framework Plaintext Reqs/sec JSON Reqs/sec
Flight 190,421 182,491
Yii 145,749 131,434
Fat-Free 139,238 133,952
Slim 89,588 87,348
Phalcon 95,911 87,675
Symfony 65,053 63,237
Lumen 40,572 39,700
Laravel 26,657 26,901
CodeIgniter 20,628 19,901

Flight und KI

Interessiert, wie Flight mit Coding-LLMs zusammenarbeitet? Entdecken Sie, wie AGENTS.md, Runway ai:*-Befehle und das Skeleton-Layout Assistenten auf Kurs halten.

Stabilität und Rückwärtskompatibilität

Wir schätzen Ihre Zeit. Wir haben alle Frameworks gesehen, die sich alle paar Jahre komplett neu erfinden und Entwickler mit kaputtem Code und teuren Migrationen zurücklassen. Flight ist anders. Flight v3 wurde als Erweiterung von v2 konzipiert, was bedeutet, dass die API, die Sie kennen und schätzen, nicht entfernt wurde. Tatsächlich werden die meisten v2-Projekte ohne Änderungen in v3 funktionieren.

Wir sind bestrebt, Flight stabil zu halten, damit Sie sich auf den Aufbau Ihrer Anwendung konzentrieren können, nicht auf die Reparatur Ihres Frameworks. Das Skeleton kann für neue Projekte meinungsstark sein; Kern-APIs bleiben für alle anderen vertraut.

Community

Wir sind auf Matrix Chat

Matrix

Und Discord

Mitwirken

Es gibt zwei Möglichkeiten, wie Sie zu Flight beitragen können:

  1. Tragen Sie zum Kern-Framework bei, indem Sie das Core-Repository besuchen.
  2. Helfen Sie mit, die Dokumentation zu verbessern! Diese Dokumentationswebsite wird auf Github gehostet. Wenn Sie einen Fehler entdecken oder etwas verbessern möchten, können Sie gerne einen Pull Request einreichen. Wir freuen uns über Updates und neue Ideen—besonders rund um KI und neue Technologien!

Systemanforderungen

Flight erfordert PHP 7.4 oder höher.

Hinweis: PHP 7.4 wird unterstützt, weil zum Zeitpunkt der Erstellung dieser Dokumentation (2024) PHP 7.4 die Standardversion für einige LTS-Linux-Distributionen ist. Ein erzwungener Wechsel zu PHP >8 würde bei diesen Nutzern viel Frust verursachen. Das Framework unterstützt auch PHP >8.

Lizenz

Flight wird unter der MIT-Lizenz veröffentlicht.

Install/install

Installationsanleitung

Es gibt einige grundlegende Voraussetzungen, bevor Sie Flight installieren können. Nämlich benötigen Sie:

  1. Installieren Sie PHP auf Ihrem System
  2. Installieren Sie Composer für die beste Entwicklererfahrung.

Grundinstallation

Wenn Sie Composer verwenden, können Sie den folgenden Befehl ausführen:

composer require flightphp/core

Dadurch werden nur die Flight-Kerndateien auf Ihrem System abgelegt. Sie müssen die Projektstruktur, Layout, Abhängigkeiten, Konfigurationen, Autoloading usw. selbst definieren. Diese Methode stellt sicher, dass außer Flight keine weiteren Abhängigkeiten installiert werden.

Sie können die Dateien auch herunterladen und direkt in Ihr Webverzeichnis entpacken.

Die Grundinstallation ist perfekt zum Lernen, für Micro-APIs und für Copy-Paste-Experimente. Für ein vollständiges App-Layout, das Menschen und KI-Codierungswerkzeuge auf dieselbe Weise nachvollziehen können, verwenden Sie das unten empfohlene Grundgerüst.

Empfohlene Installation

Es wird dringend empfohlen, für neue Projekte mit der flightphp/skeleton-App zu starten. Die Installation ist ein Kinderspiel.

composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# optionale Beispiel-DB + Posts-Demo
php runway migrate

Dieser Schritt richtet die Projektstruktur, das Composer-PSR-4-Autoloading, die Konfiguration sowie Werkzeuge wie Tracy, Tracy Extensions und Runway ein. Außerdem wird eine AGENTS.md im Root-Verzeichnis (sowie bereichsbezogene Kopien unter app/) mitgeliefert, damit KI-Assistenten ein gemeinsames Layout mit Ihnen teilen – siehe KI & Entwicklererfahrung.

Was das Grundgerüst Ihnen bietet

project-root/
├── AGENTS.md              # KI / Agenten-Quelle der Wahrheit
├── SECURITY.md            # Sicherheitserwartungen
├── .env.example           # Geheimnisse / Deploy-Overlays (kopiert nach .env)
├── public/index.php       # Nur Web-Einstiegspunkt
├── app/
│   ├── config/            # Bootstrap, Routen, Services, config_sample.php
│   ├── Controller/        # App\Controller\*  (PascalCase-Ordner!)
│   ├── Middleware/        # App\Middleware\*
│   ├── Model/             # App\Model\* (ActiveRecord)
│   ├── Utils/             # Config, Env, DatabaseFactory
│   ├── commands/          # Runway-CLI-Befehle
│   ├── views/             # Twig-Templates (*.twig)
│   ├── cache/
│   └── log/
├── migrations/            # SQL-Migrationen (.sql / .mysql.sql)
└── tests/                 # PHPUnit

Namespaces folgen der Ordner-Schreibweise. Composer mappt "App\\": "app/", also:

Pfad auf der Festplatte Namespace
app/Controller/HomeController.php App\Controller\HomeController
app/Middleware/… App\Middleware\…
app/Model/… App\Model\…
app/Utils/… App\Utils\…

Auf Linux ist app/controller/ nicht dasselbe wie app/Controller/. Das Autoloading unterscheidet zwischen Groß- und Kleinschreibung – verwenden Sie die PascalCase-Ordner des Grundgerüsts. Details: Autoloading.

Standard-Stack (neue Projekte): Twig-Views, SimplePdo + ActiveRecord, Dice mit Engine-Injektion (bevorzugen Sie kein Flight:: innerhalb von App-Klassen), optional SQLite nach php runway migrate.

create-project kopiert typischerweise app/config/config_sample.php → config.php und .env.example → .env, falls vorhanden. Routen leben in app/config/routes.php; Services und DI leben in app/config/services.php.

Dokumentation ↔ Grundgerüst: Diese Dokumentation lehrt die Flight-APIs (oft mit kurzen Flight::-Beispielen). Das Grundgerüst legt die Anwendungsstruktur fest. Wenn Sie Code unter app/ hinzufügen, folgen Sie dem Grundgerüstbaum; nutzen Sie die Dokumentation für Methodennamen, Optionen und Plugins.

Konfigurieren Sie Ihren Webserver

Integrierter PHP-Entwicklungsserver

Dies ist bei weitem der einfachste Weg, um loszulegen. Sie können den integrierten Server verwenden, um Ihre Anwendung auszuführen, und sogar SQLite als Datenbank nutzen (solange sqlite3 auf Ihrem System installiert ist), ohne viel zu benötigen! Führen Sie einfach den folgenden Befehl aus, sobald PHP installiert ist:

php -S localhost:8000
# oder mit der Grundgerüst-App
composer start

Öffnen Sie dann Ihren Browser und gehen Sie zu http://localhost:8000.

Wenn Sie das Dokumentenverzeichnis Ihres Projekts in ein anderes Verzeichnis legen möchten (z. B. Ihr Projekt ist ~/myproject, aber Ihr Dokumentenverzeichnis ist ~/myproject/public/), können Sie den folgenden Befehl ausführen, sobald Sie sich im Verzeichnis ~/myproject befinden:

php -S localhost:8000 -t public/
# bei der Grundgerüst-App ist dies bereits konfiguriert
composer start

Öffnen Sie dann Ihren Browser und gehen Sie zu http://localhost:8000.

Apache

Stellen Sie sicher, dass Apache bereits auf Ihrem System installiert ist. Wenn nicht, googeln Sie, wie Sie Apache auf Ihrem System installieren.

Bearbeiten Sie für Apache Ihre .htaccess-Datei mit folgendem Inhalt:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Hinweis: Wenn Sie Flight in einem Unterverzeichnis verwenden müssen, fügen Sie die Zeile RewriteBase /subdir/ direkt nach RewriteEngine On hinzu.

Hinweis: Wenn Sie alle Serverdateien schützen möchten, z. B. eine DB- oder Umgebungsdatei. Fügen Sie dies in Ihre .htaccess-Datei ein:

RewriteEngine On
RewriteRule ^(.*)$ index.php

Nginx

Stellen Sie sicher, dass Nginx bereits auf Ihrem System installiert ist. Wenn nicht, googeln Sie, wie Sie Nginx auf Ihrem System installieren.

Für Nginx fügen Sie Ihrer Server-Deklaration Folgendes hinzu:

server {
  location / {
    try_files $uri $uri/ /index.php;
  }
}

Erstellen Sie Ihre index.php-Datei

Wenn Sie eine Grundinstallation durchführen, benötigen Sie etwas Code, um loszulegen.

<?php

// Wenn Sie Composer verwenden, binden Sie den Autoloader ein.
require 'vendor/autoload.php';
// Wenn Sie Composer nicht verwenden, laden Sie das Framework direkt
// require 'flight/Flight.php';

// Definieren Sie dann eine Route und weisen Sie eine Funktion zur Bearbeitung der Anfrage zu.
Flight::route('/', function () {
  echo 'hello world!';
});

// Starten Sie schließlich das Framework.
Flight::start();

Bei der Grundgerüst-App bootet der öffentliche Einstiegspunkt nur die Anwendung. Routen werden in app/config/routes.php registriert (typischerweise [App\Controller\…::class, 'method'], damit Dice Abhängigkeiten injizieren kann). Services, Twig, SimplePdo und der Container werden in app/config/services.php verdrahtet. Diese Struktur ist beabsichtigt, damit KI-Tools und Menschen jedes Mal dieselben Stellen bearbeiten.

PHP installieren

Wenn auf Ihrem System bereits php installiert ist, überspringen Sie diese Anweisungen und fahren Sie mit dem Download-Abschnitt fort.

macOS

PHP mit Homebrew installieren

  1. Homebrew installieren (falls nicht bereits installiert):

    • Öffnen Sie das Terminal und führen Sie aus:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. PHP installieren:

    • Installieren Sie die neueste Version:
      brew install php
    • Um eine bestimmte Version zu installieren, z. B. PHP 8.1:
      brew tap shivammathur/php
      brew install shivammathur/php/php@8.1
  3. Zwischen PHP-Versionen wechseln:

    • Entfernen Sie die Verknüpfung der aktuellen Version und verknüpfen Sie die gewünschte Version:
      brew unlink php
      brew link --overwrite --force php@8.1
    • Überprüfen Sie die installierte Version:
      php -v

Windows 10/11

PHP manuell installieren

  1. PHP herunterladen:

    • Besuchen Sie PHP für Windows und laden Sie die neueste oder eine bestimmte Version (z. B. 7.4, 8.0) als Nicht-Thread-sichere ZIP-Datei herunter.
  2. PHP entpacken:

    • Entpacken Sie die heruntergeladene ZIP-Datei nach C:\php.
  3. PHP zum System-PATH hinzufügen:

    • Gehen Sie zu Systemeigenschaften > Umgebungsvariablen.
    • Suchen Sie unter Systemvariablen den Eintrag Path und klicken Sie auf Bearbeiten.
    • Fügen Sie den Pfad C:\php (oder den Ort, an den Sie PHP entpackt haben) hinzu.
    • Klicken Sie auf OK, um alle Fenster zu schließen.
  4. PHP konfigurieren:

    • Kopieren Sie php.ini-development nach php.ini.
    • Bearbeiten Sie php.ini, um PHP nach Bedarf zu konfigurieren (z. B. extension_dir festlegen, Erweiterungen aktivieren).
  5. PHP-Installation überprüfen:

    • Öffnen Sie die Eingabeaufforderung und führen Sie aus:
      php -v

Mehrere PHP-Versionen installieren

  1. Wiederholen Sie die obigen Schritte für jede Version und platzieren Sie jede in einem separaten Verzeichnis (z. B. C:\php7, C:\php8).

  2. Wechseln Sie zwischen den Versionen, indem Sie die System-PATH-Variable so anpassen, dass sie auf das gewünschte Versionsverzeichnis zeigt.

Ubuntu (20.04, 22.04 usw.)

PHP mit apt installieren

  1. Paketlisten aktualisieren:

    • Öffnen Sie das Terminal und führen Sie aus:
      sudo apt update
  2. PHP installieren:

    • Installieren Sie die neueste PHP-Version:
      sudo apt install php
    • Um eine bestimmte Version zu installieren, z. B. PHP 8.1:
      sudo apt install php8.1
  3. Zusätzliche Module installieren (optional):

    • Um beispielsweise MySQL-Unterstützung zu installieren:
      sudo apt install php8.1-mysql
  4. Zwischen PHP-Versionen wechseln:

    • Verwenden Sie update-alternatives:
      sudo update-alternatives --set php /usr/bin/php8.1
  5. Installierte Version überprüfen:

    • Führen Sie aus:
      php -v

Rocky Linux

PHP mit yum/dnf installieren

  1. EPEL-Repository aktivieren:

    • Öffnen Sie das Terminal und führen Sie aus:
      sudo dnf install epel-release
  2. Remi-Repository installieren:

    • Führen Sie aus:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. PHP installieren:

    • Um die Standardversion zu installieren:
      sudo dnf install php
    • Um eine bestimmte Version zu installieren, z. B. PHP 7.4:
      sudo dnf module install php:remi-7.4
  4. Zwischen PHP-Versionen wechseln:

    • Verwenden Sie den dnf-Modulbefehl:
      sudo dnf module reset php
      sudo dnf module enable php:remi-8.0
      sudo dnf install php
  5. Installierte Version überprüfen:

    • Führen Sie aus:
      php -v

Allgemeine Hinweise

Awesome-plugins/awesome_plugins

Großartige Plugins

Flight ist unglaublich erweiterbar. Es gibt eine Reihe von Plugins, mit denen du Funktionalität zu deiner Flight-Anwendung hinzufügen kannst. Einige werden offiziell vom Flight-Team unterstützt und andere sind Micro-/Lite-Bibliotheken, die dir den Einstieg erleichtern.

KI-Tools

Flight kann mit KI-gestützten Plugins noch cooler gemacht werden.

API-Dokumentation

API-Dokumentation ist entscheidend für jede API. Sie hilft Entwicklern zu verstehen, wie sie mit deiner API interagieren und was sie als Antwort erwarten können. Es gibt einige Tools, mit denen du API-Dokumentation für deine Flight-Projekte generieren kannst.

Anwendungs-Performance-Monitoring (APM)

Anwendungs-Performance-Monitoring (APM) ist entscheidend für jede Anwendung. Es hilft dir zu verstehen, wie deine Anwendung funktioniert und wo die Engpässe liegen. Es gibt eine Reihe von APM-Tools, die mit Flight verwendet werden können.

Asynchron

Flight ist bereits ein schnelles Framework, aber ihm einen Turbomotor zu verpassen, macht alles unterhaltsamer (und herausfordernder)!

Autorisierung/Berechtigungen

Autorisierung und Berechtigungen sind entscheidend für jede Anwendung, bei der kontrolliert werden muss, wer auf was zugreifen darf.

Authentifizierung

Authentifizierung ist unerlässlich für Anwendungen, die die Benutzeridentität überprüfen und API-Endpunkte absichern müssen.

Caching

Caching ist eine großartige Möglichkeit, deine Anwendung zu beschleunigen. Es gibt eine Reihe von Caching-Bibliotheken, die mit Flight verwendet werden können.

CLI

CLI-Anwendungen sind eine großartige Möglichkeit, mit deiner Anwendung zu interagieren. Du kannst sie verwenden, um Controller zu generieren, alle Routen anzuzeigen und mehr.

Cookies

Cookies sind eine großartige Möglichkeit, kleine Datenmengen auf der Clientseite zu speichern. Sie können verwendet werden, um Benutzereinstellungen, Anwendungseinstellungen und mehr zu speichern.

Debugging

Debugging ist entscheidend, wenn du in deiner lokalen Umgebung entwickelst. Es gibt einige Plugins, die dein Debugging-Erlebnis verbessern können.

Datenbanken

Datenbanken sind das Herzstück der meisten Anwendungen. Hier werden Daten gespeichert und abgerufen. Einige Datenbankbibliotheken sind einfach Wrapper zum Schreiben von Abfragen, andere sind vollwertige ORMs.

Verschlüsselung

Verschlüsselung ist entscheidend für jede Anwendung, die sensible Daten speichert. Das Verschlüsseln und Entschlüsseln der Daten ist nicht besonders schwer, aber das ordnungsgemäße Speichern des Verschlüsselungsschlüssels kann schwierig sein. Am wichtigsten ist, dass du deinen Verschlüsselungsschlüssel niemals in einem öffentlichen Verzeichnis speicherst oder ihn in deinem Code-Repository committest.

E-Mail

Das Senden von E-Mails ist ein grundlegendes Bedürfnis für die meisten Webanwendungen – Willkommensnachrichten, Passwort-Zurücksetzungen, Benachrichtigungen. Diese Bibliotheken machen es schmerzlos und halten die Zustellbarkeit stabil.

Job-Warteschlange

Job-Warteschlangen sind wirklich hilfreich, um Aufgaben asynchron zu verarbeiten. Das können das Senden von E-Mails, die Verarbeitung von Bildern oder alles sein, was nicht in Echtzeit erledigt werden muss.

Session

Sessions sind für APIs nicht wirklich nützlich, aber beim Aufbau einer Webanwendung können Sessions entscheidend sein, um den Zustand und Anmeldeinformationen beizubehalten.

Templating

Templating ist das Herzstück jeder Webanwendung mit einer UI. Es gibt eine Reihe von Templating-Engines, die mit Flight verwendet werden können.

WordPress-Integration

Möchtest du Flight in deinem WordPress-Projekt verwenden? Dafür gibt es ein praktisches Plugin!

Mitwirken

Hast du ein Plugin, das du teilen möchtest? Reiche einen Pull Request ein, um es zur Liste hinzuzufügen!

Awesome-plugins/apm

FlightPHP APM-Dokumentation

Willkommen bei FlightPHP APM – dein persönlicher Performance-Coach für deine App! Dieser Leitfaden ist deine Roadmap zum Einrichten, Verwenden und Meistern von Application Performance Monitoring (APM) mit FlightPHP. Ob du langsame Anfragen aufspürst oder dich einfach an Latenzdiagrammen erfreuen willst – wir haben dich abgedeckt. Machen wir deine App schneller, deine Nutzer glücklicher und deine Debugging-Sessions zum Kinderspiel!

Sieh dir eine Demo des Dashboards für die Flight-Dokumentationsseite an.

FlightPHP APM

Warum APM wichtig ist

Stell dir vor: Deine App ist ein gut besuchtes Restaurant. Ohne eine Möglichkeit zu verfolgen, wie lange Bestellungen dauern oder wo die Küche ins Stocken gerät, rätselst du, warum Kunden genervt gehen. APM ist dein Souschef – es beobachtet jeden Schritt, von eingehenden Anfragen bis zu Datenbankabfragen, und markiert alles, was dich ausbremst. Langsame Seiten verlieren Nutzer (Studien besagen, dass 53 % abspringen, wenn eine Website länger als 3 Sekunden zum Laden braucht!), und APM hilft dir, diese Probleme zu erkennen, bevor sie wehtun. Es ist proaktive Beruhigung – weniger „Warum ist das kaputt?“-Momente, mehr „Sieh mal, wie geschmeidig das läuft!“-Erfolge.

Installation

Beginne mit Composer:

composer require flightphp/apm

Du benötigst:

Unterstützte Datenbanken

FlightPHP APM unterstützt derzeit die folgenden Datenbanken zum Speichern von Metriken:

Du kannst deinen Datenbanktyp während des Konfigurationsschritts auswählen (siehe unten). Stelle sicher, dass deine PHP-Umgebung die erforderlichen Erweiterungen installiert hat (z. B. pdo_sqlite oder pdo_mysql).

Erste Schritte

Hier ist deine Schritt-für-Schritt-Anleitung zum APM-Erfolg:

1. APM registrieren

Füge dies in deine index.php oder eine services.php-Datei ein, um mit der Verfolgung zu beginnen:

use flight\apm\logger\LoggerFactory;
use flight\database\SimplePdo;
use flight\Apm;

$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);

// Wenn du eine Datenbankverbindung hinzufügst
// Bevorzuge SimplePdo (oder PdoQueryCapture aus den Tracy Extensions in der Entwicklung).
// Aktiviere die APM-Abfrageverfolgung über das Options-Array (5. Argument).
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
    'trackApmQueries' => true, // erforderlich, um Abfragen für das APM zu erfassen
]);
$Apm->addPdoConnection($pdo);

Was passiert hier?

Pro-Tipp: Sampling Wenn deine App stark ausgelastet ist, kann das Protokollieren jeder Anfrage die Dinge überlasten. Verwende eine Sampling-Rate (0.0 bis 1.0):

$Apm = new Apm($ApmLogger, 0.1); // Protokolliert 10 % der Anfragen

Das hält die Performance flott und liefert trotzdem solide Daten.

2. Konfiguriere es

Führe dies aus, um deine .runway-config.json zu erstellen:

php vendor/bin/runway apm:init

Was macht das?

Dieser Prozess fragt auch, ob du die Migrationen für dieses Setup ausführen möchtest. Wenn du dies zum ersten Mal einrichtest, lautet die Antwort ja.

Warum zwei Orte? Rohmetriken sammeln sich schnell an (denk an ungefilterte Logs). Der Worker verarbeitet sie in ein strukturiertes Ziel für das Dashboard. Das hält die Dinge ordentlich!

3. Metriken mit dem Worker verarbeiten

Der Worker verwandelt Rohmetriken in dashboardfertige Daten. Führe ihn einmal aus:

php vendor/bin/runway apm:worker

Was macht er?

Dauerhaft laufen lassen Für Live-Apps möchtest du eine kontinuierliche Verarbeitung. Hier sind deine Optionen:

Warum das Ganze? Ohne den Worker bleibt dein Dashboard leer. Er ist die Brücke zwischen Rohlogs und umsetzbaren Erkenntnissen.

4. Dashboard starten

Sieh dir die Vitalwerte deiner App an:

php vendor/bin/runway apm:dashboard

Was ist das?

Anpassen:

php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php

Rufe die URL in deinem Browser auf und erkunde sie!

Produktionsmodus

Für die Produktion musst du möglicherweise ein paar Techniken ausprobieren, um das Dashboard zum Laufen zu bringen, da wahrscheinlich Firewalls und andere Sicherheitsmaßnahmen vorhanden sind. Hier sind ein paar Optionen:

Ein anderes Dashboard gewünscht?

Du kannst dein eigenes Dashboard bauen, wenn du möchtest! Schau dir das Verzeichnis vendor/flightphp/apm/src/apm/presenter an, um Ideen zu erhalten, wie du die Daten für dein eigenes Dashboard präsentieren kannst!

Dashboard-Funktionen

Das Dashboard öffnet mit den Zahlen, dann den Diagrammen, dann dem Anfrageprotokoll.

Beispiel: Eine Anfrage an /users könnte Folgendes anzeigen:

Beispiel: Eine Anfrage an /users könnte Folgendes anzeigen:

Benutzerdefinierte Events hinzufügen

Verfolge alles – z. B. einen API-Aufruf oder einen Zahlungsvorgang:

use flight\apm\CustomEvent;

$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('api_call', [
    'endpoint' => 'https://api.example.com/users',
    'response_time' => 0.25,
    'status' => 200
]));

Wo erscheint es? In den Anfragedetails des Dashboards unter „Benutzerdefinierte Events“ – aufklappbar mit hübsch formatiertem JSON. Das Panel Benutzerdefinierte Events zählt außerdem jeden Typ für dich, sodass ein schnelles docs.markdown_served-Event alles ist, was du brauchst, um die Frage „Wie oft passiert das?“ zu beantworten.

Anwendungsfall:

$start = microtime(true);
$apiResponse = file_get_contents('https://api.example.com/data');
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('external_api', [
    'url' => 'https://api.example.com/data',
    'time' => microtime(true) - $start,
    'success' => $apiResponse !== false
]));

Jetzt siehst du, ob diese API deine App ausbremst!

Datenbanküberwachung

Verfolge PDO-Abfragen so:

use flight\database\SimplePdo;

$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
    'trackApmQueries' => true, // erforderlich, um Abfragen für das APM zu erfassen
]);
$Apm->addPdoConnection($pdo);

Was du bekommst:

Hinweis:

Beispielausgabe:

Worker-Optionen

Stimme den Worker nach deinen Wünschen ab:

Beispiel:

php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600

Läuft eine Stunde lang und verarbeitet jeweils 100 Metriken.

Request-ID in der App

Jede Anfrage hat eine eindeutige Request-ID zur Verfolgung. Du kannst diese ID in deiner App verwenden, um Logs und Metriken zu korrelieren. Zum Beispiel kannst du die Request-ID zu einer Fehlerseite hinzufügen:

Flight::map('error', function($message) {
    // Request-ID aus dem Response-Header X-Flight-Request-Id abrufen
    $requestId = Flight::response()->getHeader('X-Flight-Request-Id');

    // Zusätzlich könntest du sie aus der Flight-Variable abrufen
    // Diese Methode funktioniert in Swoole oder anderen asynchronen Plattformen nicht gut.
    // $requestId = Flight::get('apm.request_id');

    echo "Error: $message (Request ID: $requestId)";
});

Upgrade

Wenn du auf eine neuere Version des APM aktualisierst, kann es sein, dass Datenbankmigrationen ausgeführt werden müssen. Du kannst dies mit dem folgenden Befehl tun:

php vendor/bin/runway apm:migrate

Dadurch werden alle Migrationen ausgeführt, die benötigt werden, um das Datenbankschema auf die neueste Version zu aktualisieren.

Hinweis: Wenn deine APM-Datenbank groß ist, können diese Migrationen einige Zeit dauern. Führe sie in einer ruhigen Phase aus. Der Worker kann weiterlaufen. Indexmigrationen schreiben gesammelte Zeilen nicht neu.

Upgrade über 0.5.2 hinaus (Dashboard und Indizes)

0.5.2 hat nur die Bot-Erkennung erweitert. Die unten genannten Indexdateien werden im Release nach 0.5.2 ausgeliefert.

Sichere zuerst die Ziel-Datenbank. Kopiere die SQLite-Datei oder erstelle mit mysqldump einen Dump des MySQL/MariaDB-Schemas.

composer update flightphp/apm
php vendor/bin/runway apm:migrate

Das wendet an:

apm:migrate zeichnet den Dateinamen auf und überspringt ihn beim nächsten Mal. Es ist sicher, es zweimal auszuführen. apm:purge vergleicht request_dt mit einem UTC-Cutoff, was der Art entspricht, wie Zeilen gespeichert werden. Der erste Purge nach diesem Upgrade kann ein anderes Fenster behalten, wenn die Serverzeitzone vorher nicht UTC war.

X-Flight-Request-Id stimmt mit dem request_token überein, das für diese Anfrage gespeichert wurde.

Upgrade von 0.4.3 -> 0.5.0

Wenn du von 0.4.3 auf 0.5.0 aktualisierst, musst du den folgenden Befehl ausführen:

php vendor/bin/runway apm:config-migrate

Dadurch wird deine Konfiguration vom alten Format mit der Datei .runway-config.json auf das neue Format migriert, das die Schlüssel/Werte in der Datei config.php speichert.

Alte Daten bereinigen

Um deine Datenbank ordentlich zu halten, kannst du alte Daten bereinigen. Dies ist besonders nützlich, wenn du eine stark ausgelastete App betreibst und die Datenbankgröße überschaubar halten möchtest. Du kannst dies mit dem folgenden Befehl tun:

php vendor/bin/runway apm:purge

Dadurch werden alle Daten entfernt, die älter als 30 Tage sind, aus der Datenbank. Du kannst die Anzahl der Tage anpassen, indem du einen anderen Wert an die Option --days übergibst:

php vendor/bin/runway apm:purge --days 7

Dadurch werden alle Daten entfernt, die älter als 7 Tage sind, aus der Datenbank.

Fehlerbehebung

Hängst du fest? Probiere Folgendes:

Awesome-plugins/php_cookie

Cookies

overclokk/cookie ist eine einfache Bibliothek zum Verwalten von Cookies in Ihrer App.

Installation

Die Installation ist mit Composer einfach.

composer require overclokk/cookie

Verwendung

Die Verwendung ist so einfach wie das Registrieren einer neuen Methode in der Flight-Klasse.


use Overclokk\Cookie\Cookie;

/*
 * Setzen Sie dies in Ihrer Bootstrap- oder public/index.php-Datei
 */

Flight::register('cookie', Cookie::class);

/**
 * ExampleController.php
 */

class ExampleController {
    public function login() {
        // Setze ein Cookie

        // Sie möchten, dass dies falsch ist, damit Sie eine neue Instanz erhalten
        // verwenden Sie den folgenden Kommentar, wenn Sie eine Autovervollständigung wünschen
        /** @var \Overclokk\Cookie\Cookie $cookie */
        $cookie = Flight::cookie(false);
        $cookie->set(
            'stay_logged_in', // Name des Cookies
            '1', // der Wert, den Sie setzen möchten
            86400, // Anzahl der Sekunden, die das Cookie dauern soll
            '/', // Pfad, auf dem das Cookie verfügbar sein wird
            'example.com', // Domain, auf der das Cookie verfügbar sein wird
            true, // das Cookie wird nur über eine sichere HTTPS-Verbindung übertragen
            true // das Cookie ist nur über das HTTP-Protokoll verfügbar
        );

        // optional, wenn Sie die Standardwerte beibehalten und eine schnelle Möglichkeit haben möchten, ein Cookie lange zu setzen
        $cookie->forever('stay_logged_in', '1');
    }

    public function home() {
        // Überprüfen Sie, ob Sie das Cookie haben
        if (Flight::cookie()->has('stay_logged_in')) {
            // bringe sie z.B. in den Dashboard-Bereich.
            Flight::redirect('/dashboard');
        }
    }
}

Awesome-plugins/comment_template

CommentTemplate

CommentTemplate ist ein leistungsstarker PHP-Template-Engine mit Asset-Kompilierung, Template-Vererbung und Variablenverarbeitung. Es bietet eine einfache, aber flexible Möglichkeit, Templates zu verwalten, mit integrierter CSS/JS-Minifizierung und Caching.

Features

Installation

Installieren Sie es mit Composer.

composer require knifelemon/comment-template

Grundlegende Konfiguration

Es gibt einige grundlegende Konfigurationsoptionen, um zu starten. Sie können mehr darüber in der CommentTemplate Repo lesen.

Methode 1: Verwendung einer Callback-Funktion

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
    // Root-Verzeichnis (wo index.php liegt) - das Dokument-Root Ihrer Web-Anwendung
    $engine->setPublicPath(__DIR__);

    // Verzeichnis für Template-Dateien - unterstützt sowohl relative als auch absolute Pfade
    $engine->setSkinPath('views');             // Relativ zum Public Path

    // Wo kompilierte Assets gespeichert werden - unterstützt sowohl relative als auch absolute Pfade
    $engine->setAssetPath('assets');           // Relativ zum Public Path

    // Dateierweiterung für Templates
    $engine->setFileExtension('.php');
});

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

Methode 2: Verwendung von Konstruktor-Parametern

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

// __construct(string $publicPath = "", string $skinPath = "", string $assetPath = "", string $fileExtension = "")
$app->register('view', Engine::class, [
    __DIR__,                // publicPath - Root-Verzeichnis (wo index.php liegt)
    'views',                // skinPath - Template-Pfad (unterstützt relativ/absolut)
    'assets',               // assetPath - Pfad für kompilierte Assets (unterstützt relativ/absolut)
    '.php'                  // fileExtension - Dateierweiterung für Templates
]);

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

Pfadkonfiguration

CommentTemplate bietet intelligente Pfadbehandlung für sowohl relative als auch absolute Pfade:

Public Path

Der Public Path ist das Root-Verzeichnis Ihrer Web-Anwendung, typischerweise wo index.php liegt. Dies ist das Dokument-Root, aus dem Webserver Dateien ausliefern.

// Beispiel: Wenn Ihre index.php bei /var/www/html/myapp/index.php liegt
$template->setPublicPath('/var/www/html/myapp');  // Root-Verzeichnis

// Windows-Beispiel: Wenn Ihre index.php bei C:\xampp\htdocs\myapp\index.php liegt
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');

Konfiguration des Templates-Pfads

Der Templates-Pfad unterstützt sowohl relative als auch absolute Pfade:

$template = new Engine();
$template->setPublicPath('/var/www/html/myapp');  // Root-Verzeichnis (wo index.php liegt)

// Relative Pfade - werden automatisch mit dem Public Path kombiniert
$template->setSkinPath('views');           // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/

// Absolute Pfade - werden so verwendet (Unix/Linux)
$template->setSkinPath('/var/www/templates');      // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/

// Windows absolute Pfade
$template->setSkinPath('C:\\www\\templates');     // → C:\www\templates\
$template->setSkinPath('D:/projects/templates');  // → D:/projects/templates/

// UNC-Pfade (Windows-Netzwerkfreigaben)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\

Konfiguration des Asset-Pfads

Der Asset-Pfad unterstützt ebenfalls sowohl relative als auch absolute Pfade:

// Relative Pfade - werden automatisch mit dem Public Path kombiniert
$template->setAssetPath('assets');        // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files');  // → /var/www/html/myapp/static/files/

// Absolute Pfade - werden so verwendet (Unix/Linux)
$template->setAssetPath('/var/www/cdn');           // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets');   // → /full/path/to/assets/

// Windows absolute Pfade
$template->setAssetPath('C:\\www\\static');       // → C:\www\static\
$template->setAssetPath('D:/projects/assets');    // → D:/projects/assets/

// UNC-Pfade (Windows-Netzwerkfreigaben)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\

Intelligente Pfaderkennung:

So funktioniert es:

Tracy Debugger Integration

CommentTemplate beinhaltet Integration mit Tracy Debugger für Entwicklungs-Logging und Debugging.

Comment Template Tracy

Installation

composer require tracy/tracy

Verwendung

<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;

// Tracy aktivieren (muss vor jeder Ausgabe aufgerufen werden)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);

// Template-Überschreibung
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
    $builder->setPublicPath($app->get('flight.views.topPath'));
    $builder->setAssetPath($app->get('flight.views.assetPath'));
    $builder->setSkinPath($app->get('flight.views.path'));
    $builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

$app->start();

Debug-Panel-Funktionen

CommentTemplate fügt Tracys Debug-Leiste ein benutzerdefiniertes Panel mit vier Tabs hinzu:

Was wird protokolliert

Hinweis: Keine Leistungseinbußen, wenn Tracy nicht installiert oder deaktiviert ist.

Siehe vollständiges funktionierendes Beispiel mit Flight PHP.

Template-Direktiven

Layout-Vererbung

Verwenden Sie Layouts, um eine gemeinsame Struktur zu erstellen:

layout/global_layout.php:

<!DOCTYPE html>
<html>
<head>
    <title>{$title}</title>
</head>
<body>
    <!--@contents-->
</body>
</html>

view/page.php:

<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>

Asset-Verwaltung

CSS-Dateien

<!--@css(/css/styles.css)-->          <!-- Minifiziert und gecacht -->
<!--@cssSingle(/css/critical.css)-->  <!-- Einzelne Datei, nicht minifiziert -->

JavaScript-Dateien

CommentTemplate unterstützt verschiedene JavaScript-Lade-Strategien:

<!--@js(/js/script.js)-->             <!-- Minifiziert, geladen am Ende -->
<!--@jsAsync(/js/analytics.js)-->     <!-- Minifiziert, geladen am Ende mit async -->
<!--@jsDefer(/js/utils.js)-->         <!-- Minifiziert, geladen am Ende mit defer -->
<!--@jsTop(/js/critical.js)-->        <!-- Minifiziert, geladen im Head -->
<!--@jsTopAsync(/js/tracking.js)-->   <!-- Minifiziert, geladen im Head mit async -->
<!--@jsTopDefer(/js/polyfill.js)-->   <!-- Minifiziert, geladen im Head mit defer -->
<!--@jsSingle(/js/widget.js)-->       <!-- Einzelne Datei, nicht minifiziert -->
<!--@jsSingleAsync(/js/ads.js)-->     <!-- Einzelne Datei, nicht minifiziert, async -->
<!--@jsSingleDefer(/js/social.js)-->  <!-- Einzelne Datei, nicht minifiziert, defer -->

Asset-Direktiven in CSS/JS-Dateien

CommentTemplate verarbeitet auch Asset-Direktiven innerhalb von CSS- und JavaScript-Dateien während der Kompilierung:

CSS-Beispiel:

/* In Ihren CSS-Dateien */
@font-face {
    font-family: 'CustomFont';
    src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}

.background-image {
    background: url('<!--@asset(images/bg.jpg)-->');
}

.inline-icon {
    background: url('<!--@base64(icons/star.svg)-->');
}

JavaScript-Beispiel:

/* In Ihren JS-Dateien */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';

Base64-Kodierung

<!--@base64(images/logo.png)-->       <!-- Inline als Data-URI -->

Beispiel:

<!-- Kleine Bilder inline als Data-URIs für schnelleres Laden -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
    Kleines Icon als Hintergrund
</div>

Asset-Kopieren

<!--@asset(images/photo.jpg)-->       <!-- Kopiert einzelnes Asset in das Public-Verzeichnis -->
<!--@assetDir(assets)-->              <!-- Kopiert gesamtes Verzeichnis in das Public-Verzeichnis -->

Beispiel:

<!-- Statische Assets kopieren und referenzieren -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>Download Brochure</a>

<!-- Gesamtes Verzeichnis kopieren (Fonts, Icons usw.) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->

Template-Einbindungen

<!--@import(components/header)-->     <!-- Andere Templates einbinden -->

Beispiel:

<!-- Wiederverwendbare Komponenten einbinden -->
<!--@import(components/header)-->

<main>
    <h1>Willkommen auf unserer Website</h1>
    <!--@import(components/sidebar)-->

    <div class="content">
        <p>Hauptinhalt hier...</p>
    </div>
</main>

<!--@import(components/footer)-->

Variablenverarbeitung

Grundlegende Variablen

<h1>{$title}</h1>
<p>{$description}</p>

Variablenfilter

{$title|upper}                       <!-- In Großbuchstaben umwandeln -->
{$content|lower}                     <!-- In Kleinbuchstaben umwandeln -->
{$html|striptag}                     <!-- HTML-Tags entfernen -->
{$text|escape}                       <!-- HTML escapen -->
{$multiline|nl2br}                   <!-- Zeilenumbrüche in <br> umwandeln -->
{$html|br2nl}                        <!-- <br>-Tags in Zeilenumbrüche umwandeln -->
{$description|trim}                  <!-- Leerzeichen kürzen -->
{$subject|title}                     <!-- In Titel-Großschreibung umwandeln -->

Variablenbefehle

{$title|default=Default Title}       <!-- Standardwert setzen -->
{$name|concat= (Admin)}              <!-- Text konkatenerieren -->

Variablenbefehle

{$content|striptag|trim|escape}      <!-- Mehrere Filter verketten -->

Kommentare

Template-Kommentare werden vollständig aus dem Output entfernt und erscheinen nicht im finalen HTML:

{* Dies ist ein einzeiliger Template-Kommentar *}

{* 
   Dies ist ein mehrzeiliger 
   Template-Kommentar 
   der mehrere Zeilen umfasst
*}

<h1>{$title}</h1>
{* Debug-Kommentar: Überprüfen, ob die Title-Variable funktioniert *}
<p>{$content}</p>

Hinweis: Template-Kommentare {* ... *} unterscheiden sich von HTML-Kommentaren <!-- ... -->. Template-Kommentare werden während der Verarbeitung entfernt und erreichen nie den Browser.

Beispiel-Projektstruktur

project/
├── source/
│   ├── layouts/
│   │   └── default.php
│   ├── components/
│   │   ├── header.php
│   │   └── footer.php
│   ├── css/
│   │   ├── bootstrap.min.css
│   │   └── custom.css
│   ├── js/
│   │   ├── app.js
│   │   └── bootstrap.min.js
│   └── homepage.php
├── public/
│   └── assets/           # Generierte Assets
│       ├── css/
│       └── js/
└── vendor/

Awesome-plugins/async

Async

Async ist ein kleines Paket für das Flight-Framework, das es Ihnen ermöglicht, Ihre Flight-Apps in asynchronen Servern und Runtimes wie Swoole, AdapterMan, ReactPHP, Amp, RoadRunner, Workerman usw. auszuführen. Out of the box enthält es Adapter für Swoole und AdapterMan.

Das Ziel: Entwickeln und Debuggen mit PHP-FPM (oder dem integrierten Server) und Wechseln zu Swoole (oder einem anderen asynchronen Treiber) für die Produktion mit minimalen Änderungen.

Requirements

Installation

Installieren Sie es über Composer:

composer require flightphp/async

Falls Sie mit Swoole ausführen möchten, installieren Sie die Erweiterung:

# using pecl
pecl install swoole
# or openswoole
pecl install openswoole

# or with a package manager (Debian/Ubuntu example)
sudo apt-get install php-swoole

Quick Swoole example

Unten ist eine minimale Einrichtung zu sehen, die zeigt, wie Sie sowohl PHP-FPM (oder den integrierten Server) als auch Swoole mit demselben Codebase unterstützen können.

Dateien, die Sie in Ihrem Projekt benötigen:

index.php

Diese Datei ist ein einfacher Schalter, der die App im PHP-Modus für die Entwicklung erzwingt.

// index.php
<?php

define('NOT_SWOOLE', true);

include 'swoole_server.php';

swoole_server.php

Diese Datei bootstrapt Ihre Flight-App und startet den Swoole-Treiber, wenn NOT_SWOOLE nicht definiert ist.

// swoole_server.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = Flight::app();

$app->route('/', function() use ($app) {
    $app->json(['hello' => 'world']);
});

if (!defined('NOT_SWOOLE')) {
    // Require the SwooleServerDriver class when running in Swoole mode.
    require_once __DIR__ . '/SwooleServerDriver.php';

    Swoole\Runtime::enableCoroutine();
    $Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
    $Swoole_Server->start();
} else {
    $app->start();
}

SwooleServerDriver.php

Ein knapper Treiber, der zeigt, wie man Swoole-Anfragen in Flight über die AsyncBridge und die Swoole-Adapter überbrückt.

// SwooleServerDriver.php
<?php

use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;

class SwooleServerDriver {
    protected $Swoole;
    protected $app;

    public function __construct(string $host, int $port, Engine $app) {
        $this->Swoole = new SwooleServer($host, $port);
        $this->app = $app;

        $this->setDefault();
        $this->bindWorkerEvents();
        $this->bindHttpEvent();
    }

    protected function setDefault() {
        $this->Swoole->set([
            'daemonize'             => false,
            'dispatch_mode'         => 1,
            'max_request'           => 8000,
            'open_tcp_nodelay'      => true,
            'reload_async'          => true,
            'max_wait_time'         => 60,
            'enable_reuse_port'     => true,
            'enable_coroutine'      => true,
            'http_compression'      => false,
            'enable_static_handler' => true,
            'document_root'         => __DIR__,
            'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
            'buffer_output_size'    => 4 * 1024 * 1024,
            'worker_num'            => 4,
        ]);

        $app = $this->app;
        $app->map('stop', function (?int $code = null) use ($app) {
            if ($code !== null) {
                $app->response()->status($code);
            }
        });
    }

    protected function bindHttpEvent() {
        $app = $this->app;
        $AsyncBridge = new AsyncBridge($app);

        $this->Swoole->on('Start', function(SwooleServer $server) {
            echo "Swoole http server is started at http://127.0.0.1:9501\n";
        });

        $this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
            $SwooleAsyncRequest = new SwooleAsyncRequest($request);
            $SwooleAsyncResponse = new SwooleAsyncResponse($response);

            $AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);

            $response->end();
            gc_collect_cycles();
        });
    }

    protected function bindWorkerEvents() {
        $createPools = function() {
            // create worker-specific connection pools here
        };
        $closePools = function() {
            // close pools / cleanup here
        };
        $this->Swoole->on('WorkerStart', $createPools);
        $this->Swoole->on('WorkerStop', $closePools);
        $this->Swoole->on('WorkerError', $closePools);
    }

    public function start() {
        $this->Swoole->start();
    }
}

Running the server

Tipp: Für die Produktion verwenden Sie einen Reverse-Proxy (Nginx) vor Swoole, um TLS, statische Dateien und Lastverteilung zu handhaben.

Configuration notes

Der Swoole-Treiber stellt mehrere Konfigurationsoptionen zur Verfügung:

Passen Sie diese an Ihre Host-Ressourcen und Traffic-Muster an.

Error handling

AsyncBridge übersetzt Flight-Fehler in korrekte HTTP-Antworten. Sie können auch fehlerbehandlung auf Routenebene hinzufügen:

$app->route('/*', function() use ($app) {
    try {
        // route logic
    } catch (Exception $e) {
        $app->response()->status(500);
        $app->json(['error' => $e->getMessage()]);
    }
});

AdapterMan and other runtimes

AdapterMan wird als alternativer Runtime-Adapter unterstützt. Das Paket ist so konzipiert, dass es anpassbar ist – das Hinzufügen oder Verwenden anderer Adapter folgt im Allgemeinen demselben Muster: Konvertieren Sie die Server-Anfrage/Antwort in die Flight-Anfrage/Antwort über die AsyncBridge und die runtime-spezifischen Adapter.

Awesome-plugins/easy_query

EasyQuery

knifelemon/easy-query ist ein leichtgewichtiger, fließender SQL-Query-Builder, der SQL und Parameter für vorbereitete Anweisungen generiert. Funktioniert mit SimplePdo.

Features

Installation

composer require knifelemon/easy-query

Quick Start

use KnifeLemon\EasyQuery\Builder;

$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC')
    ->limit(10)
    ->build();

// Verwenden mit Flights SimplePdo
$users = Flight::db()->fetchAll($q['sql'], $q['params']);

Understanding build()

Die Methode build() gibt ein Array mit sql und params zurück. Diese Trennung hält Ihre Datenbank sicher durch die Verwendung vorbereiteter Anweisungen.

$q = Builder::table('users')
    ->where(['email' => 'user@example.com'])
    ->build();

// Gibt zurück:
// [
//     'sql' => 'SELECT * FROM users WHERE email = ?',
//     'params' => ['user@example.com']
// ]

Query Types

SELECT

// Alle Spalten auswählen
$q = Builder::table('users')->build();
// SELECT * FROM users

// Spezifische Spalten auswählen
$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->build();
// SELECT id, name, email FROM users

// Mit Tabellenalias
$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name'])
    ->build();
// SELECT u.id, u.name FROM users AS u

INSERT

$q = Builder::table('users')
    ->insert([
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'status' => 'active'
    ])
    ->build();
// INSERT INTO users SET name = ?, email = ?, status = ?

Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();

UPDATE

$q = Builder::table('users')
    ->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
    ->where(['id' => 123])
    ->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

DELETE

$q = Builder::table('users')
    ->delete()
    ->where(['id' => 123])
    ->build();
// DELETE FROM users WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

COUNT

$q = Builder::table('users')
    ->count()
    ->where(['status' => 'active'])
    ->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?

$count = Flight::db()->fetchField($q['sql'], $q['params']);

WHERE Conditions

Simple Equality

$q = Builder::table('users')
    ->where(['id' => 123, 'status' => 'active'])
    ->build();
// WHERE id = ? AND status = ?

Comparison Operators

$q = Builder::table('users')
    ->where([
        'age' => ['>=', 18],
        'score' => ['<', 100],
        'name' => ['!=', 'admin']
    ])
    ->build();
// WHERE age >= ? AND score < ? AND name != ?

LIKE

$q = Builder::table('users')
    ->where(['name' => ['LIKE', '%john%']])
    ->build();
// WHERE name LIKE ?

IN / NOT IN

// IN
$q = Builder::table('users')
    ->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
    ->build();
// WHERE id IN (?, ?, ?, ?, ?)

// NOT IN
$q = Builder::table('users')
    ->where(['status' => ['NOT IN', ['banned', 'deleted']]])
    ->build();
// WHERE status NOT IN (?, ?)

BETWEEN

$q = Builder::table('products')
    ->where(['price' => ['BETWEEN', [100, 500]]])
    ->build();
// WHERE price BETWEEN ? AND ?

OR Conditions

Verwenden Sie orWhere(), um OR-gruppierte Bedingungen hinzuzufügen:

$q = Builder::table('users')
    ->where(['status' => 'active'])
    ->orWhere([
        'role' => 'admin',
        'permissions' => ['LIKE', '%manage%']
    ])
    ->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)

JOIN

INNER JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name', 'p.title'])
    ->innerJoin('posts', 'u.id = p.user_id', 'p')
    ->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_id

LEFT JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.name', 'o.total'])
    ->leftJoin('orders', 'u.id = o.user_id', 'o')
    ->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_id

Multiple JOINs

$q = Builder::table('orders')
    ->alias('o')
    ->select(['o.id', 'u.name AS customer', 'p.title AS product'])
    ->innerJoin('users', 'o.user_id = u.id', 'u')
    ->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
    ->leftJoin('products', 'oi.product_id = p.id', 'p')
    ->where(['o.status' => 'completed'])
    ->build();

Ordering, Grouping, and Limits

ORDER BY

$q = Builder::table('users')
    ->orderBy('created_at DESC')
    ->build();
// ORDER BY created_at DESC

GROUP BY

$q = Builder::table('orders')
    ->select(['user_id', 'COUNT(*) as order_count'])
    ->groupBy('user_id')
    ->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id

LIMIT and OFFSET

$q = Builder::table('users')
    ->limit(10)
    ->build();
// LIMIT 10

$q = Builder::table('users')
    ->limit(10, 20)  // limit, offset
    ->build();
// LIMIT 10 OFFSET 20

Raw SQL Expressions

Verwenden Sie raw(), wenn Sie SQL-Funktionen oder Ausdrücke benötigen, die nicht als gebundene Parameter behandelt werden sollen.

Basic Raw

$q = Builder::table('users')
    ->update([
        'login_count' => Builder::raw('login_count + 1'),
        'updated_at' => Builder::raw('NOW()')
    ])
    ->where(['id' => 123])
    ->build();
// SET login_count = login_count + 1, updated_at = NOW()

Raw with Bound Parameters

$q = Builder::table('orders')
    ->update([
        'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
    ])
    ->where(['id' => 1])
    ->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]

Raw in WHERE (Subquery)

$q = Builder::table('products')
    ->where([
        'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
    ])
    ->build();
// WHERE price > (SELECT AVG(price) FROM products)

Safe Identifiers for User Input

Wenn Spaltennamen aus Benutzereingaben stammen, verwenden Sie safeIdentifier(), um SQL-Injection zu verhindern:

$sortColumn = $_GET['sort'];  // z.B. 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);

$q = Builder::table('users')
    ->orderBy($safeColumn . ' DESC')
    ->build();

// Wenn Benutzer versucht: "name; DROP TABLE users--"
// Wirft InvalidArgumentException

rawSafe for User-Provided Column Names

$userColumn = $_GET['aggregate_column'];

$q = Builder::table('orders')
    ->select([
        Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
    ])
    ->build();
// Validiert Spaltennamen, wirft Ausnahme bei ungültig

Warnung: Konkatenieren Sie Benutzereingaben nie direkt in raw(). Verwenden Sie immer gebundene Parameter oder safeIdentifier().


Query Builder Reuse

Clear Methods

Löschen Sie spezifische Teile, um den Builder wiederzuverwenden:

$query = Builder::table('users')
    ->select(['id', 'name'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// Erste Abfrage
$q1 = $query->limit(10)->build();

// Löschen und wiederverwenden
$query->clearWhere()->clearLimit();

// Zweite Abfrage mit anderen Bedingungen
$q2 = $query
    ->where(['status' => 'pending'])
    ->limit(5)
    ->build();

Available Clear Methods

Method Description
clearWhere() WHERE-Bedingungen und Parameter löschen
clearSelect() SELECT-Spalten auf Standard '*' zurücksetzen
clearJoin() Alle JOIN-Klauseln löschen
clearGroupBy() GROUP BY-Klausel löschen
clearOrderBy() ORDER BY-Klausel löschen
clearLimit() LIMIT und OFFSET löschen
clearAll() Builder auf Anfangszustand zurücksetzen

Pagination Example

$baseQuery = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// Gesamtzahl abrufen
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);

// Paginierte Ergebnisse abrufen
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);

Dynamic Query Building

$query = Builder::table('products')->alias('p');

if (!empty($categoryId)) {
    $query->where(['p.category_id' => $categoryId]);
}

if (!empty($minPrice)) {
    $query->where(['p.price' => ['>=', $minPrice]]);
}

if (!empty($maxPrice)) {
    $query->where(['p.price' => ['<=', $maxPrice]]);
}

if (!empty($searchTerm)) {
    $query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}

$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);

Full FlightPHP Example

use KnifeLemon\EasyQuery\Builder;

// Benutzer mit Pagination auflisten
Flight::route('GET /users', function() {
    $page = (int) (Flight::request()->query['page'] ?? 1);
    $perPage = 20;

    $q = Builder::table('users')
        ->select(['id', 'name', 'email', 'created_at'])
        ->where(['status' => 'active'])
        ->orderBy('created_at DESC')
        ->limit($perPage, ($page - 1) * $perPage)
        ->build();

    $users = Flight::db()->fetchAll($q['sql'], $q['params']);
    Flight::json(['users' => $users, 'page' => $page]);
});

// Benutzer erstellen
Flight::route('POST /users', function() {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->insert([
            'name' => $data->name,
            'email' => $data->email,
            'created_at' => Builder::raw('NOW()')
        ])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['id' => Flight::db()->lastInsertId()]);
});

// Benutzer aktualisieren
Flight::route('PUT /users/@id', function($id) {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->update([
            'name' => $data->name,
            'email' => $data->email,
            'updated_at' => Builder::raw('NOW()')
        ])
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

// Benutzer löschen
Flight::route('DELETE /users/@id', function($id) {
    $q = Builder::table('users')
        ->delete()
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

API Reference

Static Methods

Method Description
Builder::table(string $table) Neue Builder-Instanz für die Tabelle erstellen
Builder::raw(string $sql, array $bindings = []) Roher SQL-Ausdruck erstellen
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) Roher Ausdruck mit sicherer Identifier-Substitution
Builder::safeIdentifier(string $identifier) Identifier validieren und sicheren Spalten-/Tabellennamen zurückgeben

Instance Methods

Method Description
alias(string $alias) Tabellenalias setzen
select(string\|array $columns) Spalten zum Auswählen setzen (Standard: '*')
where(array $conditions) WHERE-Bedingungen hinzufügen (AND)
orWhere(array $conditions) OR-WHERE-Bedingungen hinzufügen
join(string $table, string $condition, string $alias, string $type) JOIN-Klausel hinzufügen
innerJoin(string $table, string $condition, string $alias) INNER JOIN hinzufügen
leftJoin(string $table, string $condition, string $alias) LEFT JOIN hinzufügen
groupBy(string $groupBy) GROUP BY-Klausel hinzufügen
orderBy(string $orderBy) ORDER BY-Klausel hinzufügen
limit(int $limit, int $offset = 0) LIMIT und OFFSET hinzufügen
count(string $column = '*') Abfrage auf COUNT setzen
insert(array $data) Abfrage auf INSERT setzen
update(array $data) Abfrage auf UPDATE setzen
delete() Abfrage auf DELETE setzen
build() Bauen und ['sql' => ..., 'params' => ...] zurückgeben
get() Alias für build()

Tracy Debugger Integration

EasyQuery integriert sich automatisch mit Tracy Debugger, falls installiert. Keine Einrichtung erforderlich!

composer require tracy/tracy
use Tracy\Debugger;

Debugger::enable();

// Alle Abfragen werden automatisch im Tracy-Panel protokolliert
$q = Builder::table('users')->where(['status' => 'active'])->build();

Das Tracy-Panel zeigt:

Für die vollständige Dokumentation besuchen Sie das GitHub-Repository.

Awesome-plugins/permissions

FlightPHP/Permissions

Dies ist ein Berechtigungsmodul, das in Ihren Projekten verwendet werden kann, wenn Sie mehrere Rollen in Ihrer App haben und jede Rolle eine etwas andere Funktionalität hat. Dieses Modul ermöglicht es Ihnen, Berechtigungen für jede Rolle zu definieren und dann zu überprüfen, ob der aktuelle Benutzer die Berechtigung hat, auf eine bestimmte Seite zuzugreifen oder eine bestimmte Aktion auszuführen.

Klicken Sie hier für das Repository auf GitHub.

Installation

Führen Sie composer require flightphp/permissions aus und schon sind Sie auf dem Weg!

Usage

Zuerst müssen Sie Ihre Berechtigungen einrichten, dann teilen Sie Ihrer App mit, was die Berechtigungen bedeuten. Letztendlich überprüfen Sie Ihre Berechtigungen mit $Permissions->has(), ->can(), oder is(). has() und can() haben die gleiche Funktionalität, sind aber unterschiedlich benannt, um Ihren Code lesbarer zu machen.

Basic Example

Nehmen wir an, Sie haben eine Funktion in Ihrer Anwendung, die überprüft, ob ein Benutzer angemeldet ist. Sie können ein Berechtigungsobjekt wie folgt erstellen:

// index.php
require 'vendor/autoload.php';

// some code 

// then you probably have something that tells you who the current role is of the person
// likely you have something where you pull the current role
// from a session variable which defines this
// after someone logs in, otherwise they will have a 'guest' or 'public' role.
$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// You'll probably want to persist this object in Flight somewhere
Flight::set('permission', $permission);

Dann haben Sie irgendwo in einem Controller so etwas.

<?php

// some controller
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // do something
        } else {
            // do something else
        }
    }
}

Sie können dies auch verwenden, um zu verfolgen, ob sie die Berechtigung haben, etwas in Ihrer Anwendung zu tun. Wenn Sie beispielsweise eine Möglichkeit haben, dass Benutzer mit dem Posten in Ihrer Software interagieren können, können Sie überprüfen, ob sie die Berechtigung haben, bestimmte Aktionen auszuführen.

$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
    if($current_role === 'admin') {
        $permissions = ['create', 'read', 'update', 'delete'];
    } else if($current_role === 'editor') {
        $permissions = ['create', 'read', 'update'];
    } else if($current_role === 'author') {
        $permissions = ['create', 'read'];
    } else if($current_role === 'contributor') {
        $permissions = ['create'];
    } else {
        $permissions = [];
    }
    return $permissions;
});
Flight::set('permission', $permission);

Dann irgendwo in einem Controller...

class PostController {
    public function create() {
        $permission = Flight::get('permission');
        if ($permission->can('post.create')) {
            // do something
        } else {
            // do something else
        }
    }
}

Injecting dependencies

Sie können Abhängigkeiten in die Closure injizieren, die die Berechtigungen definiert. Dies ist nützlich, wenn Sie eine Art Umschalter, ID oder einen anderen Datenpunkt haben, gegen den Sie überprüfen möchten. Das Gleiche funktioniert auch für Class->Method-Aufrufe, außer dass Sie die Argumente in der Methode definieren.

Closures

$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
    // ... code
});

// in your controller file
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // do something
    } else {
        // do something else
    }
}

Classes

namespace MyApp;

class Permissions {

    public function order(string $current_role, MyDependency $MyDependency = null) {
        // ... code
    }
}

Shortcut to set permissions with classes

Sie können auch Klassen verwenden, um Ihre Berechtigungen zu definieren. Dies ist nützlich, wenn Sie viele Berechtigungen haben und Ihren Code sauber halten möchten. Sie können so etwas tun:

<?php

// bootstrap code
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');

// myapp/Permissions.php
namespace MyApp;

class Permissions {

    public function order(string $current_role, int $user_id) {
        // Assuming you set this up beforehand
        /** @var \flight\database\SimplePdo $db */
        $db = Flight::db();
        $allowed_permissions = [ 'read' ]; // everyone can view an order
        if($current_role === 'manager') {
            $allowed_permissions[] = 'create'; // managers can create orders
        }
        $some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
        if($some_special_toggle_from_db) {
            $allowed_permissions[] = 'update'; // if the user has a special toggle, they can update orders
        }
        if($current_role === 'admin') {
            $allowed_permissions[] = 'delete'; // admins can delete orders
        }
        return $allowed_permissions;
    }
}

Das Coole daran ist, dass es auch eine Abkürzung gibt, die Sie verwenden können (die auch zwischengespeichert werden kann!!!), bei der Sie der Berechtigungsklasse einfach sagen, alle Methoden in einer Klasse in Berechtigungen zu mappen. Wenn Sie also eine Methode namens order() und eine Methode namens company() haben, werden diese automatisch gemappt, sodass Sie einfach $Permissions->has('order.read') oder $Permissions->has('company.read') ausführen können und es funktioniert. Das Definieren ist sehr schwierig, also bleiben Sie bei mir hier. Sie müssen nur Folgendes tun:

Erstellen Sie die Klasse von Berechtigungen, die Sie gruppieren möchten.

class MyPermissions {
    public function order(string $current_role, int $order_id = 0): array {
        // code to determine permissions
        return $permissions_array;
    }

    public function company(string $current_role, int $company_id): array {
        // code to determine permissions
        return $permissions_array;
    }
}

Dann machen Sie die Berechtigungen mit dieser Bibliothek erkennbar.

$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);

Schließlich rufen Sie die Berechtigung in Ihrer Codebasis auf, um zu überprüfen, ob der Benutzer berechtigt ist, eine bestimmte Berechtigung auszuführen.

class SomeController {
    public function createOrder() {
        if(Flight::get('permissions')->can('order.create') === false) {
            die('You can\'t create an order. Sorry!');
        }
    }
}

Caching

Um das Caching zu aktivieren, sehen Sie sich die einfache wruczak/phpfilecache Bibliothek an. Ein Beispiel für die Aktivierung finden Sie unten.


// this $app can be part of your code, or
// you can just pass null and it will
// pull from Flight::app() in the constructor
$app = Flight::app();

// For now it accepts this as a file cache. Others can easily
// be added in the future. 
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 is how many seconds to cache this for. Leave this off to not use caching

Und schon geht's los!

Awesome-plugins/mcp

FlightPHP MCP Server

Der FlightPHP MCP Server gibt jedem MCP-kompatiblen KI-Coding-Assistenten sofortigen, strukturierten Zugriff auf die gesamte FlightPHP-Dokumentation — Routing, Middleware, Plugins, Anleitungen und mehr. Statt dass Ihre KI API-Details halluziniert oder Methodensignaturen errät, holt sie die echten Dokumente bei Bedarf ab. Keine API-Schlüssel, keine Installation für die gehostete Version erforderlich.

Besuchen Sie das Github-Repository für den vollständigen Quellcode und Details.

Schnellstart

Der Server ist öffentlich gehostet und einsatzbereit:

https://mcp.flightphp.com/mcp

Fügen Sie einfach diese URL zu Ihrer KI-Coding-Erweiterung hinzu. Keine Anmeldung, keine Zugangsdaten. Siehe den Abschnitt IDE-Konfiguration unten für Kopier-Einfügen-Konfigurationen für die beliebtesten Tools.

Was es tut

Sobald verbunden, kann Ihr KI-Assistent:

Wichtige Punkte

IDE / AI-Erweiterungskonfiguration

Der Server verwendet den Streamable HTTP-Transport. Wählen Sie Ihre Erweiterung unten aus und fügen Sie die Konfiguration ein.

Claude Code (CLI)

Führen Sie den folgenden Befehl aus, um es zu Ihrem Projekt hinzuzufügen:

claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp

Oder fügen Sie es manuell zu Ihrer Projektdatei .mcp.json hinzu:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

GitHub Copilot (VS Code)

Fügen Sie es zu .vscode/mcp.json in Ihrem Arbeitsbereich hinzu:

{
  "servers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

Kilo Code (VS Code)

Fügen Sie es zu Ihrer VS Code settings.json hinzu:

{
  "kilocode.mcpServers": {
    "flightphp-docs": {
      "url": "https://mcp.flightphp.com/mcp",
      "transport": "streamable-http"
    }
  }
}

Continue.dev (VS Code / JetBrains)

Fügen Sie es zu ~/.continue/config.json hinzu:

{
  "mcpServers": [
    {
      "name": "flightphp-docs",
      "transport": {
        "type": "http",
        "url": "https://mcp.flightphp.com/mcp"
      }
    }
  ]
}

Verfügbare Tools

Der MCP-Server stellt Ihrem KI-Assistenten die folgenden Tools zur Verfügung:

Tool Beschreibung
list_docs_pages Listet alle verfügbaren Kern-Dokumentationsthemen mit Slugs und Beschreibungen auf
get_docs_page Ruft eine Kern-Dokumentationsseite anhand des Themen-Slugs ab (z. B. routing, middleware, security)
list_guide_pages Listet alle verfügbaren Schritt-für-Schritt-Anleitungen auf
get_guide_page Ruft eine vollständige Anleitung anhand des Slugs ab (z. B. blog, unit-testing)
list_plugin_pages Listet alle verfügbaren Plugin- und Erweiterungsseiten auf
get_plugin_docs Ruft die vollständige Plugin-Dokumentation anhand des Slugs ab (z. B. active-record, session, jwt)
search_docs Sucht über alle Dokumente, Anleitungen und Plugins nach einem Schlüsselwort oder Thema
fetch_url Ruft jede Seite direkt anhand ihrer vollständigen docs.flightphp.com-URL ab

Selbst-Hosting

Bevorzugen Sie es, Ihre eigene Instanz auszuführen? Sie benötigen PHP >= 8.1 und Composer.

git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php

Der Server startet standardmäßig unter http://0.0.0.0:8890/mcp. Aktualisieren Sie Ihre IDE-Konfiguration, um auf Ihre lokale Adresse zu verweisen:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "http://localhost:8890/mcp"
    }
  }
}

Awesome-plugins/php_encryption

PHP-Verschlüsselung

defuse/php-encryption ist eine Bibliothek, die zum Verschlüsseln und Entschlüsseln von Daten verwendet werden kann. Das Einrichten und Starten ist ziemlich einfach, um mit der Verschlüsselung und Entschlüsselung von Daten zu beginnen. Sie haben ein großartiges Tutorial, das dabei hilft, die Grundlagen zur Verwendung der Bibliothek sowie wichtige Sicherheitsaspekte in Bezug auf Verschlüsselung zu erklären.

Installation

Die Installation ist einfach mit Composer.

composer require defuse/php-encryption

Einrichtung

Dann müssen Sie einen Verschlüsselungsschlüssel generieren.

vendor/bin/generate-defuse-key

Das wird einen Schlüssel ausgeben, den Sie sicher aufbewahren müssen. Sie könnten den Schlüssel in Ihrer app/config/config.php-Datei im Array am Ende der Datei aufbewahren. Auch wenn es nicht der perfekte Ort ist, ist es zumindest etwas.

Verwendung

Nun, da Sie die Bibliothek und einen Verschlüsselungsschlüssel haben, können Sie damit beginnen, Daten zu verschlüsseln und zu entschlüsseln.


use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;

/*
 * In Ihrer Bootstrap- oder public/index.php-Datei festlegen
 */

// Verschlüsselungsmethode
Flight::map('encrypt', function($rohdaten) {
    $verschlüsselungsschlüssel = /* $config['encryption_key'] oder ein file_get_contents davon, wo Sie den Schlüssel platziert haben */;
    return Crypto::encrypt($rohdaten, Key::loadFromAsciiSafeString($verschlüsselungsschlüssel));
});

// Entschlüsselungsmethode
Flight::map('decrypt', function($verschlüsselte_daten) {
    $verschlüsselungsschlüssel = /* $config['encryption_key'] oder ein file_get_contents davon, wo Sie den Schlüssel platziert haben */;
    try {
        $rohdaten = Crypto::decrypt($verschlüsselte_daten, Key::loadFromAsciiSafeString($verschlüsselungsschlüssel));
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
        // Ein Angriff! Entweder der falsche Schlüssel wurde geladen oder der Geheimtext hat sich seit seiner Erstellung geändert -- entweder in der Datenbank korrupt oder absichtlich von Eve modifiziert, um einen Angriff durchzuführen.

        // ... diesen Fall auf eine Art und Weise behandeln, die für Ihre Anwendung geeignet ist ...
    }
    return $rohdaten;
});

Flight::route('/encrypt', function() {
    $verschlüsselte_daten = Flight::encrypt('Das ist ein Geheimnis');
    echo $verschlüsselte_daten;
});

Flight::route('/decrypt', function() {
    $verschlüsselte_daten = '...'; // Verschlüsselte Daten von irgendwoher erhalten
    $entschlüsselte_daten = Flight::decrypt($verschlüsselte_daten);
    echo $entschlüsselte_daten;
});

Awesome-plugins/tracy

Tracy

Tracy ist ein erstaunlicher Fehlerhandler, der mit Flight verwendet werden kann. Er verfügt über eine Reihe von Panels, die Ihnen beim Debuggen Ihrer Anwendung helfen können. Er ist auch sehr einfach zu erweitern und eigene Panels hinzuzufügen. Das Flight Team hat einige Panels speziell für Flight-Projekte mit dem Plugin flightphp/tracy-extensions erstellt (Flight-Variablen, DB-Abfragen, Request, Session und ein optionales Twig-Panel, wenn Sie ein Profiler-Profil übergeben – siehe Tracy Extensions).

Installation

Mit Composer installieren. Und Sie möchten dies tatsächlich ohne die Dev-Version installieren, da Tracy mit einer Produktions-Fehlerbehandlungskomponente kommt.

composer require tracy/tracy

Grundkonfiguration

Es gibt einige grundlegende Konfigurationsoptionen, um zu beginnen. Sie können mehr darüber in der Tracy-Dokumentation lesen.


require 'vendor/autoload.php';

use Tracy\Debugger;

// Tracy aktivieren
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // manchmal müssen Sie explizit sein (auch Debugger::PRODUCTION)
// Debugger::enable('23.75.345.200'); // Sie können auch ein Array von IP-Adressen angeben

// Hier werden Fehler und Ausnahmen protokolliert. Stellen Sie sicher, dass dieses Verzeichnis existiert und beschreibbar ist.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // alle Fehler anzeigen
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // alle Fehler außer veralteten Hinweisen
if (Debugger::$showBar) {
    $app->set('flight.content_length', false); // wenn die Debugger-Leiste sichtbar ist, kann die Content-Length nicht von Flight gesetzt werden

    // Dies ist spezifisch für die Tracy-Erweiterung für Flight, wenn Sie diese eingebunden haben
    // andernfalls kommentieren Sie dies aus.
    new TracyExtensionLoader($app);
}

Hilfreiche Tipps

Wenn Sie Ihren Code debuggen, gibt es einige sehr hilfreiche Funktionen, um Daten für Sie auszugeben.

Awesome-plugins/flightmail

FlightMail

Drittanbieter-Plugin – gepflegt von Ryan Stubbs (ryanstubbs/flightmail, MIT-lizenziert). Nicht Teil des Flight-Kerns – bitte melde Probleme im GitHub-Repository.

ryanstubbs/flightmail ermöglicht es dir, E-Mails aus deiner Flight-App ohne Kopfschmerzen zu versenden. Es kapselt Symfony Mailer – die am härtesten getestete Mail-Bibliothek in PHP – und lässt es sich wie ein Teil von Flight anfühlen. Eine Zeile zum Installieren, eine fließende Kette zum Senden:

Flight::mail()->compose()
    ->to('someone@example.com')
    ->subject('You did it!')
    ->text('Your first email is on its way.')
    ->send();

Features

Anforderungen

Was Version
PHP 8.2 oder neuer
Flight PHP Kern ^3.15
Symfony Mailer ^7.2 oder ^8.0 (automatisch installiert)

Installation

composer require ryanstubbs/flightmail

Das wars für den Versand von Klartext- und HTML-E-Mails. Das Rendern von Vorlagen ist optional – füge nur eine Engine hinzu, wenn du sie nutzen willst:

composer require twig/twig      # für .twig-Vorlagen
composer require latte/latte    # für .latte-Vorlagen

Zwei weitere optionale Bibliotheken unterstützen die erweiterten Funktionen zum Sendezeitpunkt, die unten beschrieben werden:

composer require pelago/emogrifier         # für CSS-Inlining ("inline_css")
composer require league/html-to-markdown   # für Markdown-Textteile ("text_from_html")

Alle können nebeneinander installiert werden; FlightMail wählt anhand deiner Konfiguration die passende aus.

Deine erste E-Mail

Füge dies zu deiner Bootstrap-Datei hinzu (am selben Ort, an dem du Routen definierst):

<?php
require 'vendor/autoload.php';

use ryanstubbs\FlightMail\MailPlugin;

// Teile FlightMail mit, von welcher Adresse und über welchen Transport E-Mails gesendet werden.
MailPlugin::install([
    'dsns' => [
        'default' => 'smtp://user:pass@localhost:1025',
    ],
    'from' => 'no-reply@example.com',
]);

Flight::route('/signup', function () {
    Flight::mail()->compose()
        ->to('new-user@example.com')
        ->subject('Welcome aboard!')
        ->html('<h1>Welcome!</h1><p>We are glad you are here.</p>')
        ->send();
});

Flight::start();

Du nutzt das Flight PHP-Skelett? Registriere stattdessen in app/config/services.php im Instanz-Stil:

use ryanstubbs\FlightMail\MailPlugin;

MailPlugin::register($app, [
    'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
    'from' => 'no-reply@example.com',
]);

Beide Stile stellen denselben Mailer bereit: Flight::mail() und $app->mail() sind austauschbar.

Lokal testen? Falls dein Projekt in DDEV läuft, setze die DSN auf smtp://127.0.0.1:1025 und lies jede erfasste E-Mail in Mailpit unter http://<project>.ddev.site:8025. Nichts verlässt deinen Rechner.

E-Mails senden

Klartext-Strings (keine Vorlagen-Engine nötig)

->text() und ->html() akzeptieren rohe Strings und benötigen nichts weiter Installiertes:

Flight::mail()->compose()
    ->to('ops@example.com')
    ->subject('Backup finished')
    ->text('Nightly backup completed in 42 minutes.')
    ->send();

Flight::mail()->compose()
    ->to('billing@example.com')
    ->subject('Invoice #123')
    ->html('<h1>Invoice #123</h1><p>Total due: $42.00</p>')
    ->send();

Twig-Vorlagen

// welcome.html.twig enthält: Hallo {{ name }}, danke für deine Anmeldung!
Flight::mail()->compose()
    ->to('someone@example.com')
    ->subject('Welcome!')
    ->template('welcome.html.twig', ['name' => 'Ryan'])
    ->send();

Latte-Vorlagen

Gleiche Idee, .latte-Erweiterung:

// welcome.latte enthält: Hallo {$name}, danke für deine Anmeldung!
Flight::mail()->compose()
    ->to('someone@example.com')
    ->subject('Welcome!')
    ->template('welcome.latte', ['name' => 'Ryan'])
    ->send();

HTML + Klartext zusammen

Bewährte Praxis für die Zustellbarkeit – gib Mail-Clients beide Versionen:

Flight::mail()->compose()
    ->to('someone@example.com')
    ->subject('Welcome!')
    ->template('welcome.html.twig', ['name' => 'Ryan'])     // umfangreiche Version
    ->textTemplate('welcome.txt.twig', ['name' => 'Ryan'])  // Fallback-Version
    ->send();

Ein paar Dinge, die du über Vorlagen wissen solltest:

HTML stylen und Textteile generieren

Zwei optionale Erweiterungen zum Sendezeitpunkt, beide standardmäßig deaktiviert und beide unterstützt von Bibliotheken, die du nur installierst, wenn du sie möchtest:

Funktion Installation Konfigurationsschlüssel
CSS-Inline-Umwandlung pelago/emogrifier inline_css
Textteil aus HTML league/html-to-markdown text_from_html

CSS in deine HTML-E-Mail einfügen

Gmail und die meisten Webmail-Clients entfernen <style>-Blöcke – Inline-style=""-Attribute sind das einzige Styling, das sie zuverlässig beachten. Das von Hand zu schreiben ist mühselig; lass Emogrifier das zum Sendezeitpunkt erledigen:

composer require pelago/emogrifier
MailPlugin::install([
    'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
    'inline_css' => true,
]);

Ist das aktiviert, wird bei jedem HTML-Body das CSS direkt vor dem Senden inline eingefügt – egal ob aus einer Vorlage oder über ->html(). Eine Nachricht wie <style>p { color: red; }</style><p>Hi</p> wird als <p style="color: red;">Hi</p> versendet.

Um gemeinsame Stile (Markenfarben, Resets) in jede E-Mail einzufügen, ohne sie in jeder Vorlage zu wiederholen, übergib Regeln direkt oder verweise auf eine Stylesheet-Datei:

'inline_css' => ['css_file' => __DIR__ . '/mail-styles/base.css'],
// oder
'inline_css' => ['css' => '.button { background: #0a84ff; color: #fff; }'],

Steuerung pro Nachricht:

$message->inlineCss();          // erzwingt das Inlining für diese eine Nachricht
$message->withoutInlineCss();   // überspringt es, auch wenn es global aktiviert ist

Textteil aus deinem HTML generieren

Bewährte Praxis ist, eine HTML- und eine Klartext-Version zusammen zu senden, aber beides zu schreiben ist mühselig. FlightMail kann den Textteil automatisch aus dem endgültigen HTML ableiten – die einfache Konvertierung benötigt keine zusätzliche Abhängigkeit, da der Konverter mit Symfony Mime geliefert wird:

MailPlugin::install([
    'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
    'text_from_html' => true,       // Markdown, wenn möglich, sonst Klartext
]);

Modi:

Die Generierung läuft nach dem Rendern und dem CSS-Inlining und nur, wenn die Nachricht einen HTML-Body, aber keinen Text-Body hat – ein explizites ->text() oder ->textTemplate() hat immer Vorrang. Die Überschreibungen pro Nachricht spiegeln das Inlining:

$message->textFromHtml('plain');    // erzwingt das Entfernen von Tags für diese eine
$message->withoutTextFromHtml();    // Nur-HTML-E-Mail

Wenn du einen Modus aktivierst, dessen Bibliothek nicht installiert ist, erhältst du eine klare Fehlermeldung, die das genaue composer require nennt – niemals stilles Verschlechtern.

Einen Anbieter wählen

Anbieter werden über DSN-Strings angeschlossen. Installiere das Bridge-Paket, füge die DSN in dsns ein, fertig.

Anbieter Installation DSN-Beispiel
SMTP integriert smtp://user:pass@host:587
Sendmail integriert sendmail://default
Dev/null (E-Mails verwerfen) integriert null://null
Postmark composer require symfony/postmark-mailer postmark+api://KEY@api.postmarkapp.com
Sendgrid composer require symfony/sendgrid-mailer sendgrid+api://KEY@default
Mailgun composer require symfony/mailgun-mailer mailgun+https://KEY:DOMAIN@api.mailgun.net
Amazon SES composer require symfony/amazon-mailer ses+https://KEY:SECRET@default
Brevo composer require symfony/brevo-mailer brevo+api://KEY@default
MailerSend composer require symfony/mailersend-mailer mailersend+api://KEY@default

Die vollständige Liste findest du in der Symfony-Mailer-Dokumentation – alles, was dort dokumentiert ist, funktioniert hier unverändert.

Mehrere Anbieter gleichzeitig

Benenne jeden Transport und wähle dann pro Nachricht:

MailPlugin::install([
    'dsns' => [
        'transactional' => 'postmark+api://KEY@api.postmarkapp.com',
        'bulk'          => 'smtp://user:pass@bulk.example.com:587',
    ],
    'from' => 'no-reply@example.com',
]);
// Kein ->transport()-Aufruf = erster Schlüssel in "dsns" ("transactional" hier).
Flight::mail()->compose()->to('...')->text('receipt')->send();

// Explizit eine andere Route wählen.
Flight::mail()->compose()->to('...')->text('newsletter')->transport('bulk')->send();

Konfigurationsreferenz

Alles ist optional außer dsns.

MailPlugin::install([
    // ERFORDERLICH – Transportname => Symfony-DSN.
    // Der erste Eintrag wird verwendet, wenn eine Nachricht keinen benennt.
    'dsns' => [
        'default' => 'smtp://user:pass@localhost:1025',
    ],

    // Transport, der verwendet wird, wenn eine Nachricht kein explizites ->transport() hat
    // und du nicht den ersten Schlüssel möchtest. Muss in "dsns" existieren.
    'default_transport' => 'default',

    // Globaler Absender. String, Symfony-Adresse oder ['email' => 'Name'].
    // Wird nur angewendet, wenn eine Nachricht kein eigenes ->from() setzt.
    'from' => ['no-reply@example.com' => 'My App'],

    // Standard-Vorlagenengine: 'twig', 'latte' oder ein benutzerdefinierter Name.
    // Wird nur für Vorlagen herangezogen, deren Erweiterung kein registrierter Renderer ist.
    'renderer' => 'twig',

    // Wo Vorlagen liegen, in Reihenfolge durchsucht; plus optionales Cache-Verzeichnis.
    'templates' => [
        'paths' => [__DIR__ . '/mail-templates'],
        'cache' => __DIR__ . '/cache/mail',
    ],

    // Zusätzliche Optionen, die direkt an Twig\Environment übergeben werden.
    'twig' => ['options' => ['strict_variables' => true]],

    // Latte-Engine beim Start anpassen: fn(Latte\Engine $engine): void.
    'latte' => ['setup' => static fn (Latte\Engine $e) => $e->addExtension(new MyExtension())],

    // Body-Erweiterungen zum Sendezeitpunkt (siehe „HTML stylen und Textteile generieren").
    'inline_css' => true,           // oder ['css' => '...', 'css_file' => '...']
    'text_from_html' => true,       // oder 'plain' / 'markdown'

    // Benutzerdefinierte DSN-Schemata, benutzerdefinierte Renderer, Hooks vor dem Senden (siehe unten).
    'transport_factories' => [],
    'renderers' => [],
    'hooks' => [],

    // Optionale Verdrahtung, die jedem Transport übergeben wird.
    'event_dispatcher' => $dispatcher,  // Symfony MessageEvents
    'logger' => $psr3Logger,
]);

Weiterführendes

Alles unten ist optional. Die Standardeinstellungen decken die meisten Apps ab.

Ein benutzerdefiniertes DSN-Schema hinzufügen

Implementiere Symfonys TransportFactoryInterface und registriere sie – dann funktioniert dein eigenes Schema genau wie ein eingebautes:

use ryanstubbs\FlightMail\MailPlugin;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;

class MyCarrierFactory implements TransportFactoryInterface
{
    public function supports(Dsn $dsn): bool
    {
        return $dsn->getScheme() === 'mycarrier';
    }

    public function create(Dsn $dsn): TransportInterface
    {
        // ... baue einen Transport, der mit deinem Anbieter kommuniziert
    }
}

$plugin = MailPlugin::install(['dsns' => ['carrier' => 'mycarrier://key']]);
$plugin->addTransportFactory(new MyCarrierFactory());

Einen benutzerdefinierten Vorlagen-Renderer hinzufügen

Alles, was einen Vorlagennamen plus Parameter in einen String verwandelt, kommt infrage:

use ryanstubbs\FlightMail\MailPlugin;
use ryanstubbs\FlightMail\Render\RendererInterface;

$plugin = MailPlugin::install($config);

$plugin->addRenderer('markdown', fn (array $config): RendererInterface =>
    new MarkdownMailRenderer($config['templates']['paths'] ?? [])
);
// Vorlagen, die auf .markdown enden, verwenden es jetzt automatisch:
Flight::mail()->compose()->to('...')->template('welcome.markdown', ['name' => 'Ryan'])->send();

Direkt vor dem Senden etwas ausführen

Hooks empfangen die fertige Nachricht – nach dem Rendern, nach den Standardwerten, direkt vor der Übertragung:

$plugin->addHook(function (ryanstubbs\FlightMail\Message $message): void {
    $message->getHeaders()->addTextHeader('X-Mailer', 'MyApp/1.0');
});

Events und Logging

Übergib einen Symfony-Event-Dispatcher und/oder PSR-3-Logger, und jeder Transport wird sie verwenden:

$plugin->eventDispatcher($dispatcher); // erhält vor jedem Senden ein MessageEvent
$plugin->logger($logger);              // Logs auf Transportebene

API-Spickzettel

// Einrichtung
MailPlugin::install($config)             // registriert in der globalen Flight-App
MailPlugin::register($app, $config)      // an einer bestimmten Engine registrieren
$mailer = Flight::mail();                // die gemeinsame Mailer-Instanz

// Nachrichten erstellen
$mailer->compose(): Message
$message->to(...)->from(...)->subject(...)   // Standard-Symfony-Mime-Methoden
$message->text(string)                       // Klartext-String-Body
$message->html(string)                       // HTML-String-Body
$message->template($name, $params)           // HTML-Body aus einer Vorlage
$message->htmlTemplate($name, $params)       // Alias von template()
$message->textTemplate($name, $params)       // Text-Body aus einer Vorlage
$message->inlineCss() / ->withoutInlineCss() // CSS-Inlining pro Nachricht
$message->textFromHtml($mode)                // automatischer Textteil: true/'auto'/'plain'/'markdown'/false
$message->withoutTextFromHtml()              // Nur-HTML-E-Mail
$message->transport($name)                   // über eine benannte DSN leiten
$message->send(): ?SentMessage               // rendern + senden

// Am Mailer selbst
$mailer->send($message): ?SentMessage        // explizite Alternative zu $message->send()
$mailer->render($template, $params): string  // rendern ohne zu senden
$mailer->addHook(callable): static           // fn(Message $message): void
$mailer->transports(): TransportManager      // get() / has() / names()
$mailer->renderers(): RendererFactory        // create() / has() / add()

Da Message von Symfony\Component\Mime\Email erbt, funktioniert jede Symfony-Methode, die du bereits kennst – attach(), embed(), priority(), replyTo() – sofort ohne weitere Einrichtung.

Fehlerbehebung

„Keine Mail-DSNs konfiguriert“ Du hast Flight::mail() aufgerufen, bevor du das Plugin registriert hast, oder das Konfigurationsarray enthielt kein dsns. Dieser Fehler ist beabsichtigt – FlightMail weigert sich zu raten, wohin deine E-Mail gehen soll, statt sie stillschweigend zu verwerfen.

„Unbekannter Mail-Vorlagen-Renderer ...“ Du hast eine Vorlage verwendet, deren Engine nicht installiert ist. Behebe es mit composer require twig/twig oder composer require latte/latte, oder registriere einen benutzerdefinierten Renderer, der nach der Erweiterung benannt ist.

„Unbekannter Mail-Transport ...“ Ein ->transport('name') (oder default_transport) entspricht keinem Schlüssel in dsns. Überprüfe die Schreibweise – die Fehlermeldung listet die konfigurierten Namen auf.

E-Mails kommen nicht an Setze dsns auf null://null, um zu bestätigen, dass der Rest deines Codes funktioniert, und wechsle dann zurück zur echten DSN. In DDEV verwende smtp://127.0.0.1:1025 und prüfe Nachrichten in Mailpit auf Port 8025.


Für Fehlerberichte, Pull-Requests und den vollständigen Quellcode besuche das GitHub-Repository.

Awesome-plugins/simple_job_queue

Einfache Job-Warteschlange

Die einfache Job-Warteschlange ist eine Bibliothek, die verwendet werden kann, um Jobs asynchron zu verarbeiten. Sie kann mit beanstalkd, MySQL/MariaDB, SQLite und PostgreSQL verwendet werden.

Installation

composer require n0nag0n/simple-job-queue

Verwendung

Damit dies funktioniert, benötigen Sie eine Möglichkeit, Jobs zur Warteschlange hinzuzufügen, und eine Möglichkeit, die Jobs zu verarbeiten (einen Worker). Im Folgenden finden Sie Beispiele, wie man einen Job zur Warteschlange hinzufügt und wie man den Job verarbeitet.

Hinzufügen zu Flight

Das Hinzufügen dieses Codes zu Flight ist einfach und erfolgt mit der Methode register(). Unten finden Sie ein Beispiel, wie Sie dies zu Flight hinzufügen.

<?php
require 'vendor/autoload.php';

// Ändern Sie ['mysql'] in ['beanstalkd'], wenn Sie beanstalkd verwenden möchten
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
    // Wenn Sie bereits eine PDO-Verbindung zu Flight::db() haben;
    $Job_Queue->addQueueConnection(Flight::db());

    // Oder wenn Sie beanstalkd/Pheanstalk verwenden
    $pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
    $Job_Queue->addQueueConnection($pheanstalk);
});

Einen neuen Job hinzufügen

Wenn Sie einen Job hinzufügen, müssen Sie eine Pipeline (Warteschlange) angeben. Dies ist vergleichbar mit einem Kanal in RabbitMQ oder einem Tube in beanstalkd.

<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));

Einen Worker ausführen

Hier ist eine Beispieldatei, wie man einen Worker ausführt.

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO-Verbindung
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);

// Oder wenn Sie beanstalkd/Pheanstalk verwenden
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);

$Job_Queue->watchPipeline('send_important_emails');
while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    // Passen Sie es an, was Ihnen nachts besser schlafen lässt (nur für Datenbankwarteschlangen, beanstalkd benötigt diese if-Anweisung nicht)
    if(empty($job)) {
        usleep(500000);
        continue;
    }

    echo "Verarbeite {$job['id']}\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
        } else {
            // Dies entfernt es aus der bereitstehenden Warteschlange und legt es in eine andere Warteschlange, die später aufgegriffen und "getreten" werden kann.
            $Job_Queue->buryJob($job);
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
    }
}

Lange Prozesse mit Supervisord verwalten

Supervisord ist ein Prozesskontrollsystem, das sicherstellt, dass Ihre Worker-Prozesse kontinuierlich laufen. Hier ist eine umfassendere Anleitung, wie Sie es mit Ihrem einfachen Job-Queue-Worker einrichten:

Supervisord installieren

# Auf Ubuntu/Debian
sudo apt-get install supervisor

# Auf CentOS/RHEL
sudo yum install supervisor

# Auf macOS mit Homebrew
brew install supervisor

Erstellen eines Worker-Skripts

Zuerst speichern Sie Ihren Worker-Code in einer dedizierten PHP-Datei:

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// PDO-Verbindung
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);

// Setzen Sie die Pipeline, die überwacht werden soll
$Job_Queue->watchPipeline('send_important_emails');

// Protokolliere den Start des Workers
echo date('Y-m-d H:i:s') . " - Worker gestartet\n";

while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    if(empty($job)) {
        usleep(500000); // Schlafen für 0,5 Sekunden
        continue;
    }

    echo date('Y-m-d H:i:s') . " - Verarbeite Job {$job['id']}\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
            echo date('Y-m-d H:i:s') . " - Job {$job['id']} erfolgreich abgeschlossen\n";
        } else {
            $Job_Queue->buryJob($job);
            echo date('Y-m-d H:i:s') . " - Job {$job['id']} fehlgeschlagen, beerdigt\n";
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
        echo date('Y-m-d H:i:s') . " - Ausnahme bei der Verarbeitung des Jobs {$job['id']}: {$e->getMessage()}\n";
    }
}

Konfigurieren von Supervisord

Erstellen Sie eine Konfigurationsdatei für Ihren Worker:

[program:email_worker]
command=php /path/to/worker.php
directory=/path/to/project
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/simple_job_queue_err.log
stdout_logfile=/var/log/simple_job_queue.log
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d

Wichtige Konfigurationsoptionen:

Worker mit Supervisorctl verwalten

Nach dem Erstellen oder Modifizieren der Konfiguration:

# Supervisor-Konfiguration neu laden
sudo supervisorctl reread
sudo supervisorctl update

# Steuerung spezifischer Worker-Prozesse
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*

Mehrere Pipelines ausführen

Für mehrere Pipelines erstellen Sie separate Worker-Dateien und Konfigurationen:

[program:email_worker]
command=php /path/to/email_worker.php
# ... andere Konfigurationen ...

[program:notification_worker]
command=php /path/to/notification_worker.php
# ... andere Konfigurationen ...

Überwachen und Protokolle

Überprüfen Sie die Protokolle, um die Aktivität der Worker zu überwachen:

# Protokolle anzeigen
sudo tail -f /var/log/simple_job_queue.log

# Status überprüfen
sudo supervisorctl status

Dieses Setup sorgt dafür, dass Ihre Job-Worker auch nach Abstürzen, Serverneustarts oder anderen Problemen weiterlaufen, was Ihr Warteschlangensystem zuverlässig für Produktionsumgebungen macht.

Awesome-plugins/session

FlightPHP Session - Leichtgewichtiger Dateibasierter Session-Handler

Dies ist ein leichtgewichtiger, dateibasisierter Session-Handler-Plugin für das Flight PHP Framework. Es bietet eine einfache, aber leistungsstarke Lösung zur Verwaltung von Sessions, mit Funktionen wie nicht blockierendem Lesen von Sessions, optionaler Verschlüsselung, Auto-Commit-Funktionalität und einem Testmodus für die Entwicklung. Session-Daten werden in Dateien gespeichert, was es ideal für Anwendungen macht, die keine Datenbank benötigen.

Falls du eine Datenbank verwenden möchtest, schaue dir das ghostff/session Plugin an, das viele der gleichen Funktionen bietet, aber mit einer Datenbank-Backend.

Besuche das Github-Repository für den vollständigen Quellcode und Details.

Installation

Installiere das Plugin über Composer:

composer require flightphp/session

Grundlegende Nutzung

Hier ist ein einfaches Beispiel, wie du das flightphp/session-Plugin in deiner Flight-Anwendung verwendest:

require 'vendor/autoload.php';

use flight\Session;

$app = Flight::app();

// Registriere den Session-Dienst
$app->register('session', Session::class);

// Beispiel-Route mit Session-Nutzung
Flight::route('/login', function() {
    $session = Flight::session();
    $session->set('user_id', 123);
    $session->set('username', 'johndoe');
    $session->set('is_admin', false);

    echo $session->get('username'); // Gibt aus: johndoe
    echo $session->get('preferences', 'default_theme'); // Gibt aus: default_theme

    if ($session->get('user_id')) {
        Flight::json(['message' => 'User is logged in!', 'user_id' => $session->get('user_id')]);
    }
});

Flight::route('/logout', function() {
    $session = Flight::session();
    $session->clear(); // Löschung aller Session-Daten
    Flight::json(['message' => 'Logged out successfully']);
});

Flight::start();

Wichtige Punkte

Konfiguration

Du kannst den Session-Handler anpassen, indem du ein Array von Optionen beim Registrieren übergeben:

// Ja, es ist ein doppeltes Array :)
$app->register('session', Session::class, [ [
    'save_path' => '/custom/path/to/sessions',         // Verzeichnis für Session-Dateien
    'prefix' => 'myapp_',                              // Präfix für Session-Dateien
    'encryption_key' => 'a-secure-32-byte-key-here',   // Verschlüsselung aktivieren (32 Bytes empfohlen für AES-256-CBC)
    'auto_commit' => false,                            // Auto-Commit deaktivieren für manuelle Kontrolle
    'start_session' => true,                           // Session automatisch starten (Standard: true)
    'test_mode' => false,                              // Testmodus für die Entwicklung aktivieren
    'serialization' => 'json',                         // Serialisierungs-Methode: 'json' (Standard) oder 'php' (Legacy)
] ]);

Konfigurationsoptionen

Option Beschreibung Standardwert
save_path Verzeichnis, in dem Session-Dateien gespeichert werden sys_get_temp_dir() . '/flight_sessions'
prefix Präfix für die gespeicherte Session-Datei sess_
encryption_key Schlüssel für AES-256-CBC-Verschlüsselung (optional) null (keine Verschlüsselung)
auto_commit Automatische Speicherung von Session-Daten beim Herunterfahren true
start_session Session automatisch starten true
test_mode Im Testmodus ausführen, ohne PHP-Sessions zu beeinflussen false
test_session_id Benutzerdefinierte Session-ID für den Testmodus (optional) Zufällig generiert, wenn nicht gesetzt
serialization Serialisierungs-Methode: 'json' (Standard, sicher) oder 'php' (Legacy, erlaubt Objekte) 'json'

Serialisierungsmodi

Standardmäßig verwendet diese Bibliothek JSON-Serialisierung für Session-Daten, was sicher ist und PHP-Objekt-Injektions-Schwachstellen verhindert. Wenn du PHP-Objekte in der Session speichern musst (nicht empfohlen für die meisten Apps), kannst du auf die Legacy-PHP-Serialisierung umschalten:

Hinweis: Wenn du JSON-Serialisierung verwendest, wirft das Versuch, ein Objekt zu speichern, eine Ausnahme.

Erweiterte Nutzung

Manuelles Commit

Wenn du Auto-Commit deaktivierst, musst du Änderungen manuell speichern:

$app->register('session', Session::class, ['auto_commit' => false]);

Flight::route('/update', function() {
    $session = Flight::session();
    $session->set('key', 'value');
    $session->commit(); // Änderungen explizit speichern
});

Session-Sicherheit mit Verschlüsselung

Aktiviere Verschlüsselung für sensible Daten:

$app->register('session', Session::class, [
    'encryption_key' => 'your-32-byte-secret-key-here'
]);

Flight::route('/secure', function() {
    $session = Flight::session();
    $session->set('credit_card', '4111-1111-1111-1111'); // Wird automatisch verschlüsselt
    echo $session->get('credit_card'); // Wird beim Abruf entschlüsselt
});

Session-Regeneration

Regeneriere die Session-ID für Sicherheit (z. B. nach dem Login):

Flight::route('/post-login', function() {
    $session = Flight::session();
    $session->regenerate(); // Neue ID, Daten behalten
    // ODER
    $session->regenerate(true); // Neue ID, alte Daten löschen
});

Middleware-Beispiel

Schütze Routen mit sessionbasierter Authentifizierung:

Flight::route('/admin', function() {
    Flight::json(['message' => 'Welcome to the admin panel']);
})->addMiddleware(function() {
    $session = Flight::session();
    if (!$session->get('is_admin')) {
        Flight::halt(403, 'Access denied');
    }
});

Dies ist nur ein einfaches Beispiel für die Nutzung in Middleware. Für ein detaillierteres Beispiel, siehe die Middleware-Dokumentation.

Methoden

Die Session-Klasse bietet diese Methoden:

Alle Methoden außer get() und id() geben die Session-Instanz für Kettenaufrufe zurück.

Warum dieses Plugin verwenden?

Technische Details

Beitrag

Beiträge sind willkommen! Forke das Repository, mache deine Änderungen und reiche einen Pull-Request ein. Melde Fehler oder schlage Features über den Github-Issue-Tracker vor.

Lizenz

Dieses Plugin ist unter der MIT-Lizenz lizenziert. Siehe das Github-Repository für Details.

Awesome-plugins/runway

Runway

Runway ist eine CLI-Anwendung, die Ihnen hilft, Ihre Flight-Anwendungen zu verwalten. Sie kann Controller generieren, alle Routen anzeigen, KI-Setup-Helfer ausführen, Migrationen (im Skeleton) und mehr. Sie basiert auf der ausgezeichneten adhocore/php-cli Bibliothek.

Klicken Sie hier, um den Code anzuzeigen.

Scaffolding-Befehle sind absichtlich mit dem offiziellen Skeleton abgestimmt, damit KI-Coding-Tools und Menschen jedes Mal die gleichen Pfade, Namespaces und Constructor-Injection-Stile erhalten.

Installation

Mit Composer installieren.

composer require flightphp/runway

Das Skeleton hängt bereits von Runway ab; verwenden Sie php runway aus dem Projekt-Root.

Grundlegende Konfiguration

Beim ersten Ausführen von Runway wird versucht, eine runway-Konfiguration in app/config/config.php über den Schlüssel 'runway' zu finden.

<?php
// app/config/config.php
return [
    'runway' => [
        'app_root' => 'app/',
        'public_root' => 'public/',
        // optional; das Skeleton verwendet auch index_root für den öffentlichen Einstieg
        'index_root' => 'public/index.php',
    ],
];

HINWEIS - Ab v1.2.0 ist .runway-config.json zugunsten von app/config/config.php veraltet. Migrieren Sie mit php runway config:migrate beim Upgrade älterer Projekte. Das Skeleton kann beim Erstellen eines Projekts weiterhin eine kleine .runway-config.json schreiben, um die Kompatibilität zu gewährleisten; bevorzugen Sie den runway-Schlüssel in config.php für die Zukunft.

Projektrouterkennung

Runway ist intelligent genug, um das Root-Verzeichnis Ihres Projekts zu erkennen, auch wenn Sie es aus einem Unterverzeichnis ausführen. Es sucht nach Indikatoren wie composer.json, .git oder app/config/config.php, um zu bestimmen, wo sich das Projekt-Root befindet. Das bedeutet, dass Sie Runway-Befehle von überall in Ihrem Projekt ausführen können!

Verwendung

Runway verfügt über eine Reihe von Befehlen, die Sie zur Verwaltung Ihrer Flight-Anwendung verwenden können. Es gibt zwei einfache Möglichkeiten, Runway zu verwenden.

  1. Wenn Sie das Skeleton-Projekt verwenden, können Sie php runway [Befehl] aus dem Root Ihres Projekts ausführen.
  2. Wenn Sie Runway als über Composer installiertes Paket verwenden, können Sie vendor/bin/runway [Befehl] aus dem Root Ihres Projekts ausführen.

Befehlsliste

Sie können eine Liste aller verfügbaren Befehle anzeigen, indem Sie den Befehl php runway ausführen.

php runway

Verlassen Sie sich nur auf Befehle, die tatsächlich in dieser Liste für Ihre Installation erscheinen (Kern-Runway-Befehle vs. projektspezifische Befehle wie migrate des Skeletons).

Befehlshilfe

Für jeden Befehl können Sie das Flag --help übergeben, um weitere Informationen zur Verwendung des Befehls zu erhalten.

php runway routes --help
php runway make:controller --help

Hier sind einige Beispiele:

Controller generieren

make:controller erstellt ein Scaffold für einen Controller, der mit dem offiziellen Skeleton-Layout übereinstimmt:

Pfad app/Controller/{Name}.php
Namespace App\Controller
Stil Constructor-Injection von flight\Engine (kein Flight:: im Klassenrumpf)
php runway make:controller MyController
# → app/Controller/MyController.php
#   namespace App\Controller;

Beispiel der erwarteten Form (vereinfacht):

<?php

declare(strict_types=1);

namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        // z.B. $this->app->render('…', […]);
    }
}

Registrieren Sie es mit einem Klassen-Callable, damit Dice den Controller erstellen kann:

// app/config/routes.php
use App\Controller\MyController;

$router->get('/mine', [MyController::class, 'index']);

Warum dieses Layout? Die Ordnergroß-/Kleinschreibung muss mit dem Namespace übereinstimmen (Controller nicht controllers) für Composer PSR-4 unter Linux—siehe Autoloading. Der gleiche Pfad ist es, den Root- und Scoped-AGENTS.md-Dateien KI-Tools mitteilen, sie zu verwenden, damit generierte und handgeschriebene Controller identisch bleiben.

Ältere Dokumentationen und Community-Projekte verwendeten manchmal app/controllers/ und app\controllers. Das bleibt gültig, wenn Ihr Baum weiterhin Kleinbuchstaben für Ordner verwendet. Neue Skeleton-Projekte und aktuelle make:controller-Ausgaben verwenden app/Controller/ + App\Controller.

Active Record Modell generieren

Stellen Sie zuerst sicher, dass Sie das Active Record Plugin installiert haben.

php runway make:record users

Im offiziellen Skeleton leben Modelle unter app/Model/ mit dem Namespace App\Model, und die DB-Verbindung ist SimplePdo (injizieren Sie es oder übergeben Sie es an den ActiveRecord-Konstruktor). Generierte Dateinamen/Namespaces folgen den aktuellen Standardeinstellungen von Runway und Ihrer runway-Konfiguration—bevorzugen Sie die Ausrichtung neuer Modelle an App\Model, damit sie mit Autoloading und AGENTS.md übereinstimmen.

Beispiel eines Modells, das mit der Skeleton-Posts-Demo übereinstimmt:

<?php

declare(strict_types=1);

namespace App\Model;

use flight\ActiveRecord;

/**
 * @property int $id
 * @property string $title
 * // …
 */
class Post extends ActiveRecord
{
    protected array $relations = [];

    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'posts');
    }
}

Wenn ein älterer Generator immer noch app/records / app\records ausgibt, können Sie diese Konvention in Legacy-Apps beibehalten oder Dateien in app/Model/ verschieben und den Namespace an die Ordner-Groß-/Kleinschreibung anpassen.

Migrationen (Skeleton)

Das offizielle Skeleton enthält einen Projektbefehl (entdeckt aus app/commands/), wie zum Beispiel:

php runway migrate

Migrationen sind SQL-Dateien unter migrations/ (zum Beispiel YYYYMMDDHHMMSS_description.sql für SQLite und …_description.mysql.sql für MySQL), ausgewählt aus Ihrer Datenbanktreiber-Konfiguration / Umgebung. Exakte Flags und Verhalten werden von diesem Projektbefehl definiert—führen Sie php runway migrate --help in Ihrer App aus.

KI-Helfer

Runway stellt KI-orientierte Befehle bereit, die mit KI & Entwicklererfahrung verwendet werden:

php runway ai:init
php runway ai:generate-instructions

Diese speichern LLM-Anmeldedaten und generieren Projektanweisungen (hauptsächlich AGENTS.md). Beim Skeleton behandeln Sie AGENTS.md (und Scoped-Kopien unter app/) plus SECURITY.md als Quelle der Wahrheit für Agenten.

Alle Routen anzeigen

Dies zeigt alle Routen an, die derzeit bei Flight registriert sind.

php runway routes

Wenn Sie nur bestimmte Routen ansehen möchten, können Sie ein Flag übergeben, um die Routen zu filtern.

# Nur GET-Routen anzeigen
php runway routes --get

# Nur POST-Routen anzeigen
php runway routes --post

# usw.

Benutzerdefinierte Befehle zu Runway hinzufügen

Wenn Sie entweder ein Paket für Flight erstellen oder Ihre eigenen benutzerdefinierten Befehle in Ihr Projekt einfügen möchten, können Sie dies tun, indem Sie ein src/commands/, flight/commands/, app/commands/ oder commands/ Verzeichnis für Ihr Projekt/Paket erstellen. Wenn Sie weitere Anpassungen benötigen, siehe den Abschnitt unten zur Konfiguration.

Im Skeleton leben Projektbefehle in app/commands/ mit dem Namespace App\Command. Runway entdeckt sie über den Pfad; halten Sie diesen Ordner synchron mit dem Composer-Classmap/PSR-4, wie Ihr Projekt es bereits tut.

Um einen Befehl zu erstellen, erweitern Sie einfach die Klasse AbstractBaseCommand und implementieren Sie mindestens eine __construct-Methode und eine execute-Methode.

<?php

declare(strict_types=1);

namespace App\Command;

use flight\commands\AbstractBaseCommand;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * Konstruktor
     *
     * @param array<string,mixed> $config Konfiguration aus app/config/config.php
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', 'Erstellt ein Beispiel für die Dokumentation', $config);
        $this->argument('<funny-gif>', 'Der Name des lustigen Gifs');
    }

    /**
     * Führt die Funktion aus
     *
     * @return void
     */
    public function execute()
    {
        $io = $this->app()->io();

        $io->info('Erstelle Beispiel...');

        // Hier etwas tun

        $io->ok('Beispiel erstellt!');
    }
}

Siehe die adhocore/php-cli Dokumentation für weitere Informationen darüber, wie Sie Ihre eigenen benutzerdefinierten Befehle in Ihre Flight-Anwendung integrieren können!

Konfigurationsverwaltung

Da die Konfiguration ab v1.2.0 in app/config/config.php verschoben wurde, gibt es einige Hilfsbefehle zur Konfigurationsverwaltung.

Skeleton-Tipp: Halten Sie config.php als literalen PHP-Wert. Geheimnisse gehören in .env. Vermeiden Sie $_ENV[...]-Ausdrücke innerhalb von config.php—config:set schreibt diese Datei als statische Daten um und könnte Geheimnisse in die Datei einbetten. Siehe Konfiguration.

Alte Konfiguration migrieren

Wenn Sie eine alte .runway-config.json-Datei haben, können Sie diese einfach mit dem folgenden Befehl zu app/config/config.php migrieren:

php runway config:migrate

Konfigurationswert setzen

Sie können einen Konfigurationswert mit dem Befehl config:set setzen. Dies ist nützlich, wenn Sie einen Konfigurationswert aktualisieren möchten, ohne die Datei zu öffnen.

php runway config:set app_root "app/"

Konfigurationswert abrufen

Sie können einen Konfigurationswert mit dem Befehl config:get abrufen.

php runway config:get app_root

Alle Runway-Konfigurationen

Wenn Sie die Konfiguration für Runway anpassen müssen, können Sie diese Werte in app/config/config.php setzen. Hier sind einige zusätzliche Konfigurationen, die Sie setzen können:

<?php
// app/config/config.php
return [
    // ... andere Konfigurationswerte ...

    'runway' => [
        // Dies ist der Ort, an dem sich Ihr Anwendungsverzeichnis befindet
        'app_root' => 'app/',

        // Dies ist das Verzeichnis, in dem sich Ihre Root-Indexdatei befindet
        'index_root' => 'public/',

        // Dies sind die Pfade zu den Roots anderer Projekte
        'root_paths' => [
            '/home/user/different-project',
            '/var/www/another-project'
        ],

        // Basis-Pfade müssen wahrscheinlich nicht konfiguriert werden, aber es ist hier, wenn Sie es wollen
        'base_paths' => [
            '/includes/libs/vendor', // wenn Sie einen wirklich einzigartigen Pfad für Ihr Vendor-Verzeichnis oder so etwas haben
        ],

        // Finale Pfade sind Orte innerhalb eines Projekts, um nach den Befehlsdateien zu suchen
        'final_paths' => [
            'src/diff-path/commands',
            'app/module/admin/commands',
        ],

        // Wenn Sie einfach den vollständigen Pfad hinzufügen möchten, nur zu (absolut oder relativ zum Projekt-Root)
        'paths' => [
            '/home/user/different-project/src/diff-path/commands',
            '/var/www/another-project/app/module/admin/commands',
            'app/my-unique-commands'
        ]
    ]
];

Konfiguration zugreifen

Wenn Sie die Konfigurationswerte effektiv zugreifen müssen, können Sie über die __construct-Methode oder die app()-Methode darauf zugreifen. Es ist auch wichtig zu beachten, dass wenn Sie eine app/config/services.php-Datei haben, diese Dienste auch für Ihren Befehl verfügbar sein werden.

public function execute()
{
    $io = $this->app()->io();

    // Konfiguration zugreifen
    $app_root = $this->config['runway']['app_root'];

    // Dienste zugreifen wie vielleicht eine Datenbankverbindung
    $database = $this->config['database']

    // ...
}

KI-Helfer-Wrapper

Runway hat einige Helfer-Wrapper, die es KI erleichtern, Befehle zu generieren. Sie können addOption und addArgument auf eine Weise verwenden, die sich ähnlich wie Symfony Console anfühlt. Dies ist hilfreich, wenn Sie KI-Tools zur Generierung Ihrer Befehle verwenden.

public function __construct(array $config)
{
    parent::__construct('make:example', 'Erstellt ein Beispiel für die Dokumentation', $config);

    // Das mode-Argument ist nullbar und standardmäßig vollständig optional
    $this->addOption('name', 'Der Name des Beispiels', null);
}

Siehe auch

Awesome-plugins/n0nag0n_wordpress

WordPress-Integration: n0nag0n/wordpress-integration-for-flight-framework

Möchten Sie Flight PHP in Ihrer WordPress-Site verwenden? Dieses Plugin macht es zum Kinderspiel! Mit n0nag0n/wordpress-integration-for-flight-framework können Sie eine vollständige Flight-Anwendung direkt neben Ihrer WordPress-Installation ausführen – ideal zum Erstellen von benutzerdefinierten APIs, Mikroservices oder sogar vollwertigen Anwendungen, ohne WordPress zu verlassen.


Was tut es?

Installation

  1. Laden Sie den Ordner flight-integration in Ihr /wp-content/plugins/-Verzeichnis hoch.
  2. Aktivieren Sie das Plugin im WordPress-Admin-Bereich (Plugins-Menü).
  3. Gehen Sie zu Einstellungen > Flight Framework, um das Plugin zu konfigurieren.
  4. Legen Sie den Pfad zum Vendor-Ordner Ihrer Flight-Installation fest (oder verwenden Sie Composer, um Flight zu installieren).
  5. Konfigurieren Sie den Pfad zu Ihrem App-Ordner und erstellen Sie die Ordnerstruktur (das Plugin kann Ihnen dabei helfen!).
  6. Starten Sie mit der Erstellung Ihrer Flight-Anwendung!

Nutzungsbeispiele

Einfaches Route-Beispiel

In Ihrer Datei app/config/routes.php:

Flight::route('GET /api/hello', function() {
    Flight::json(['message' => 'Hello World!']);
});

Controller-Beispiel

Erstellen Sie einen Controller in app/controllers/ApiController.php:

namespace app\controllers;

use Flight;

class ApiController {
    public function getUsers() {
        // Sie können WordPress-Funktionen in Flight verwenden!
        $users = get_users();
        $result = [];
        foreach($users as $user) {
            $result[] = [
                'id' => $user->ID,
                'name' => $user->display_name,
                'email' => $user->user_email
            ];
        }
        Flight::json($result);
    }
}

Dann in Ihrer routes.php:

Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);

FAQ

F: Muss ich Flight kennen, um dieses Plugin zu verwenden?
A: Ja, dies ist für Entwickler, die Flight innerhalb von WordPress nutzen möchten. Grundkenntnisse von Flights Routing und Anfrageverarbeitung werden empfohlen.

F: Wird dies meine WordPress-Site verlangsamen?
A: Nein! Das Plugin verarbeitet nur Anfragen, die zu Ihren Flight-Routen passen. Alle anderen Anfragen werden wie üblich an WordPress weitergeleitet.

F: Kann ich WordPress-Funktionen in meiner Flight-Anwendung verwenden?
A: Absolut! Sie haben vollen Zugriff auf alle WordPress-Funktionen, Hooks und Globals innerhalb Ihrer Flight-Routen und Controller.

F: Wie erstelle ich benutzerdefinierte Routes?
A: Definieren Sie Ihre Routes in der Datei config/routes.php in Ihrem App-Ordner. Schauen Sie sich die Beispieldatei an, die vom Ordnerstruktur-Generator erstellt wird.

Changelog

1.0.0
Erstveröffentlichung.


Für mehr Infos schauen Sie sich das GitHub-Repo an.

Awesome-plugins/latte

Latte

Latte ist ein vollwertiges Templating-Engine, das sehr einfach zu bedienen ist und näher an der PHP-Syntax liegt als Twig oder Smarty. Es ist auch sehr einfach zu erweitern und eigene Filter und Funktionen hinzuzufügen.

Installation

Installieren Sie es mit Composer.

composer require latte/latte

Grundlegende Konfiguration

Es gibt einige grundlegende Konfigurationsoptionen, um zu starten. Sie können mehr darüber in der Latte-Dokumentation lesen.


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Wo Latte speziell seinen Cache speichert
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    $latte->render($finalPath, $data, $block);
});

Einfaches Layout-Beispiel

Hier ist ein einfaches Beispiel für eine Layout-Datei. Dies ist die Datei, die verwendet wird, um alle Ihre anderen Views zu umschließen.

<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
    <head>
        <title>{$title ? $title . ' - '}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- Ihre Navigations-Elemente hier -->
            </nav>
        </header>
        <div id="content">
            <!-- Hier liegt die Magie -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

Und jetzt haben wir Ihre Datei, die in diesem Content-Block gerendert wird:

<!-- app/views/home.latte -->
<!-- Dies teilt Latte mit, dass diese Datei "innerhalb" der layout.latte-Datei liegt -->
{extends layout.latte}

<!-- Dies ist der Inhalt, der innerhalb des Layouts im Content-Block gerendert wird -->
{block content}
    <h1>Startseite</h1>
    <p>Willkommen in meiner App!</p>
{/block}

Wenn Sie dies in Ihrer Funktion oder Ihrem Controller rendern, würden Sie etwas Ähnliches tun:

// Einfache Route
Flight::route('/', function () {
    Flight::render('home.latte', [
        'title' => 'Startseite'
    ]);
});

// Oder wenn Sie einen Controller verwenden
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.latte', [
            'title' => 'Startseite'
        ]);
    }
}

Sehen Sie sich die Latte-Dokumentation für weitere Informationen an, wie Sie Latte in vollem Umfang nutzen können!

Debugging mit Tracy

PHP 8.1+ ist für diesen Abschnitt erforderlich.

Sie können auch Tracy verwenden, um Ihre Latte-Template-Dateien direkt aus der Box heraus zu debuggen! Wenn Sie Tracy bereits installiert haben, müssen Sie die Latte-Erweiterung zu Tracy hinzufügen.

// services.php
use Tracy\Debugger;

$app->map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Wo Latte speziell seinen Cache speichert
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    // Dies fügt die Erweiterung nur hinzu, wenn die Tracy-Debug-Bar aktiviert ist
    if (Debugger::$showBar === true) {
        // Hier fügen Sie das Latte-Panel zu Tracy hinzu
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }
    $latte->render($finalPath, $data, $block);
});

Awesome-plugins/tracy_extensions

Tracy Flight Panel-Erweiterungen

Dies ist ein Satz von Erweiterungen, um die Arbeit mit Flight etwas reichhaltiger zu gestalten.

Dies ist besonders praktisch mit dem offiziellen Skeleton, das standardmäßig Twig verwendet: das gleiche Layout KI-Tools folgen, wird auch deutlich auf der Tracy-Leiste angezeigt.

Dies ist das Panel

Flight Bar

Und jedes Panel zeigt sehr hilfreiche Informationen über Ihre Anwendung an!

Flight Data Flight Database Flight Request

Klicken Sie hier, um den Code anzusehen.

Installation

Führen Sie composer require flightphp/tracy-extensions --dev aus und schon geht es los!

Twig ist keine harte Abhängigkeit des Pakets. Installieren Sie twig/twig nur, wenn Sie das Twig-Panel möchten (das Skeleton macht dies bereits für Views).

Konfiguration

Es gibt sehr wenig Konfiguration, die Sie vornehmen müssen, um dies zu starten. Sie müssen den Tracy-Debugger vor der Verwendung initiieren https://tracy.nette.org/en/guide:

<?php

use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;

// Bootstrap-Code
require __DIR__ . '/vendor/autoload.php';

Debugger::enable();
// Sie müssen möglicherweise Ihre Umgebung mit Debugger::enable(Debugger::DEVELOPMENT) angeben

// wenn Sie Datenbankverbindungen in Ihrer App verwenden, gibt es einen 
// erforderlichen PDO-Wrapper, der NUR IN DER ENTWICKLUNG verwendet werden sollte (nicht in der Produktion bitte!)
// Er hat die gleichen Parameter wie eine reguläre PDO-Verbindung
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// oder wenn Sie dies an das Flight-Framework anhängen
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// jetzt wird jedes Mal, wenn Sie eine Abfrage ausführen, die Zeit, die Abfrage und die Parameter erfasst

// Dies verbindet die Punkte
if(Debugger::$showBar === true) {
    // Dies muss false sein, oder Tracy kann nicht wirklich rendern :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// mehr Code

Flight::start();

Zusätzliche Konfiguration

Session-Daten

Wenn Sie einen benutzerdefinierten Session-Handler haben (wie ghostff/session), können Sie jedes Array von Session-Daten an Tracy übergeben und es wird automatisch für Sie ausgegeben. Sie übergeben es mit dem session_data-Schlüssel im zweiten Parameter des TracyExtensionLoader-Konstruktors.


use Ghostff\Session\Session;
// oder flight\Session verwenden;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

if(Debugger::$showBar === true) {
    // Dies muss false sein, oder Tracy kann nicht wirklich rendern :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// Routen und andere Dinge...

Flight::start();

Twig-Panel (optional)

Wenn Ihre App Twig verwendet (einschließlich des offiziellen Skeletons), können Sie Vorlagenmetriken auf der Tracy-Leiste anzeigen. Erstellen Sie ein Twig Profile, hängen Sie ProfilerExtension an Ihre Umgebung an, dann übergeben Sie dieses Profil an den Loader unter dem twig_profile-Schlüssel. Hängen Sie Profiling nur in der Entwicklung an.

<?php

use flight\debug\tracy\TracyExtensionLoader;
use flight\debug\tracy\TwigTracyExtension;
use Tracy\Debugger;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;

$loader = new FilesystemLoader(__DIR__ . '/views');
$twig = new Environment($loader, [
    'debug' => true,
    'cache' => false,
]);

// Optional: Tracy-Dump-Helfer in Vorlagen verfügbar machen
// {{ dump(var) }}, {{ bdump(var) }}, {{ dumpe(var) }}
$twig->addExtension(new TwigTracyExtension());

$tracyConfig = [];
if (Debugger::$showBar === true) {
    $profile = new Profile();
    $twig->addExtension(new ProfilerExtension($profile));
    $tracyConfig['twig_profile'] = $profile;
}

if (Debugger::$showBar === true) {
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), $tracyConfig);
}

// Flight::render() zu Twig abbilden (Beispiel)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }
    echo $twig->render($template, $data);
});

Was das Panel anzeigt

Der Twig-Tab ist ausgeblendet, wenn für die Anfrage keine Vorlagen gerendert wurden, oder wenn Sie twig_profile weglassen (oder Twig nicht installiert haben) - andere Flight-Panels funktionieren weiterhin.

In einer skeleton-ähnlichen services.php, bauen Sie das gleiche $profile / ProfilerExtension auf, wenn Debug eingeschaltet ist, übergeben Sie twig_profile an TracyExtensionLoader, und verwenden Sie weiterhin Ihre gemeinsame Twig-Umgebung für $app->render().

Latte

PHP 8.1+ ist für diesen Abschnitt erforderlich.

Wenn Sie Latte in Ihrem Projekt installiert haben, hat Tracy eine native Integration mit Latte zur Analyse Ihrer Vorlagen. Sie registrieren einfach die Erweiterung mit Ihrer Latte-Instanz (dies ist Latte's eigene Tracy-Bridge, nicht das Twig-Panel oben).


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function($template, $data, $block = null) {
    $latte = new Latte\Engine;

    // andere Konfigurationen...

    // die Erweiterung nur hinzufügen, wenn die Tracy Debug Bar aktiviert ist
    if(Debugger::$showBar === true) {
        // hier fügen Sie das Latte-Panel zu Tracy hinzu
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

    $latte->render($template, $data, $block);
});

Siehe auch

Awesome-plugins/php_file_cache

flightphp/cache

Leichte, einfache und eigenständige PHP-Datei-Caching-Klasse, abgeleitet von Wruczek/PHP-File-Cache

Vorteile

Diese Dokumentationsseite verwendet diese Bibliothek zum Caching jeder Seite!

Klicken Sie hier, um den Code anzusehen.

Installation

Über Composer installieren:

composer require flightphp/cache

Verwendung

Die Verwendung ist ziemlich einfach. Dies speichert eine Cache-Datei im Cache-Verzeichnis.

use flight\Cache;

$app = Flight::app();

// Sie übergeben das Verzeichnis, in dem der Cache gespeichert wird, an den Konstruktor
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {

    // Dies stellt sicher, dass der Cache nur im Produktionsmodus verwendet wird
    // ENVIRONMENT ist eine Konstante, die in Ihrer Bootstrap-Datei oder an anderer Stelle in Ihrer App gesetzt wird
    $cache->setDevMode(ENVIRONMENT === 'development');
});

Einen Cache-Wert abrufen

Sie verwenden die Methode get(), um einen gecachten Wert abzurufen. Wenn Sie eine praktische Methode möchten, die den Cache aktualisiert, wenn er abgelaufen ist, können Sie refreshIfExpired() verwenden.


// Cache-Instanz abrufen
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
    return date("H:i:s"); // Daten zurückgeben, die gecacht werden sollen
}, 10); // 10 Sekunden

// oder
$data = $cache->get('simple-cache-test');
if(empty($data)) {
    $data = date("H:i:s");
    $cache->set('simple-cache-test', $data, 10); // 10 Sekunden
}

Einen Cache-Wert speichern

Sie verwenden die Methode set(), um einen Wert im Cache zu speichern.

Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10 Sekunden

Einen Cache-Wert löschen

Sie verwenden die Methode delete(), um einen Wert im Cache zu löschen.

Flight::cache()->delete('simple-cache-test');

Überprüfen, ob ein Cache-Wert existiert

Sie verwenden die Methode exists(), um zu prüfen, ob ein Wert im Cache existiert.

if(Flight::cache()->exists('simple-cache-test')) {
    // etwas tun
}

Cache leeren

Sie verwenden die Methode flush(), um den gesamten Cache zu leeren.

Flight::cache()->flush();

Metadaten mit Cache abrufen

Wenn Sie Zeitstempel und andere Metadaten zu einem Cache-Eintrag abrufen möchten, stellen Sie sicher, dass Sie true als korrekten Parameter übergeben.

$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
    echo "Refreshing data!" . PHP_EOL;
    return date("H:i:s"); // Daten zurückgeben, die gecacht werden sollen
}, 10, true); // true = mit Metadaten zurückgeben
// oder
$data = $cache->get("simple-cache-meta-test", true); // true = mit Metadaten zurückgeben

/*
Beispiel für ein gecachtes Element, das mit Metadaten abgerufen wurde:
{
    "time":1511667506, <-- Unix-Zeitstempel speichern
    "expire":10,       <-- Ablaufzeit in Sekunden
    "data":"04:38:26", <-- deserialisierte Daten
    "permanent":false
}

Mit Metadaten können wir beispielsweise berechnen, wann das Element gespeichert wurde oder wann es abläuft
Wir können auch über den Schlüssel "data" auf die Daten selbst zugreifen
*/

$expiresin = ($data["time"] + $data["expire"]) - time(); // Unix-Zeitstempel abrufen, wann die Daten ablaufen, und aktuellen Zeitstempel davon abziehen
$cacheddate = $data["data"]; // wir greifen über den Schlüssel "data" auf die Daten selbst zu

echo "Latest cache save: $cacheddate, expires in $expiresin seconds";

Quellcode

Besuchen Sie https://github.com/flightphp/cache, um den Code anzusehen.

Awesome-plugins/twig

Twig

Twig ist eine flexible, schnelle und sichere Template-Engine für PHP. Es ist die Template-Sprache, die von Symfony und vielen anderen Projekten verwendet wird, was bedeutet, dass KI-Codierungstools und die meisten PHP-Entwickler ihre Syntax bereits gut kennen. Twig kompiliert Templates in optimiertes PHP, maskiert die Ausgabe standardmäßig automatisch (ideal für XSS-Schutz) und lässt sich leicht mit Filtern, Funktionen und Erweiterungen erweitern.

Installation

Mit Composer installieren.

composer require twig/twig

Grundkonfiguration

Es gibt einige grundlegende Konfigurationsoptionen, um zu beginnen. Sie können mehr darüber in der Twig-Dokumentation lesen.

require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Wo Twig seine kompilierten Templates speichert
        'cache' => __DIR__ . '/../cache/twig',
        // Templates neu kompilieren, wenn sich die Quelle ändert (praktisch in der Entwicklung)
        'auto_reload' => true,
    ]);

    echo $twig->render($template, $data);
});

Twig als View-Klasse registrieren

Wenn Sie eine einzelne Twig-Umgebung wiederverwenden möchten (für die Produktion empfohlen), registrieren Sie diese und verweisen Sie render darauf:

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ],
]);

$app->map('render', function(string $template, array $data): void {
    echo Flight::view()->render($template, $data);
});

Einfaches Layout-Beispiel

Hier ist ein einfaches Beispiel für eine Layout-Datei. Dies ist die Datei, die verwendet wird, um alle anderen Views einzubetten.

{# app/views/layout.twig #}
<!doctype html>
<html lang="en">
    <head>
        <title>{% if title %}{{ title }} - {% endif %}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                {# Ihre Navigationselemente hier #}
            </nav>
        </header>
        <div id="content">
            {# Hier passiert die Magie #}
            {% block content %}{% endblock %}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

Und nun haben wir Ihre Datei, die im Content-Block gerendert wird:

{# app/views/home.twig #}
{# Dies teilt Twig mit, dass diese Datei "innerhalb" der layout.twig-Datei ist #}
{% extends 'layout.twig' %}

{# Dies ist der Inhalt, der im Layout innerhalb des Content-Blocks gerendert wird #}
{% block content %}
    <h1>Startseite</h1>
    <p>Willkommen in meiner App!</p>
{% endblock %}

Wenn Sie dies dann in Ihrer Funktion oder Ihrem Controller rendern möchten, würden Sie so etwas tun:

// einfache Route
Flight::route('/', function () {
    Flight::render('home.twig', [
        'title' => 'Startseite'
    ]);
});

// oder wenn Sie einen Controller verwenden
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.twig', [
            'title' => 'Startseite'
        ]);
    }
}

Weitere Informationen zur vollen Nutzung von Twig finden Sie in der Twig-Dokumentation!

Debugging

Twig enthält eine Debug-Erweiterung, die eine dump()-Funktion hinzufügt, die Sie in Templates verwenden können. Aktivieren Sie sie nur in der Entwicklung:

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'debug' => true, // erforderlich für die dump()-Funktion
        'auto_reload' => true,
    ],
], function (\Twig\Environment $twig): void {
    $twig->addExtension(new \Twig\Extension\DebugExtension());
});

Dann in einem Template:

{{ dump(user) }}

Sie können Twig auch mit Tracy für das PHP-Level-Debugging kombinieren. Für Template-Level-Metriken (Renderzeit, Speicher, welche Templates/Blöcke ausgeführt wurden), verwenden Sie das optionale Twig-Panel in flightphp/tracy-extensions: übergeben Sie ein Twig\Profiler\Profile als twig_profile an TracyExtensionLoader. Die optionale TwigTracyExtension stellt {{ dump() }} / {{ bdump() }} / {{ dumpe() }} in Templates zur Verfügung, wenn Tracy aktiviert ist.

Sicherheitshinweis

Twig maskiert die Ausgabe standardmäßig automatisch, was vor XSS-Angriffen schützt. Verwenden Sie bevorzugt {{ variable }} für Text. Verwenden Sie den Filter |raw nur, wenn Sie dem HTML-Inhalt bewusst vertrauen (z. B. bereinigtes Markdown, das Sie bereits serverseitig verarbeitet haben).

Awesome-plugins/ghost_session

Ghostff/Session

PHP-Sitzungsmanager (nicht blockierend, Flash, Segment, Sitzungsverschlüsselung). Verwendet PHP open_ssl für optionale Verschlüsselung/Entschlüsselung von Sitzungsdaten. Unterstützt File, MySQL, Redis und Memcached.

Klicken Sie hier, um den Code anzusehen.

Installation

Installieren Sie mit Composer.

composer require ghostff/session

Basic Configuration

Sie müssen nichts übergeben, um die Standardeinstellungen für Ihre Sitzung zu verwenden. Sie können mehr über Einstellungen in der Github Readme nachlesen.

use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

// eine Sache, die man sich merken sollte, ist, dass Sie Ihre Sitzung bei jedem Seitenaufruf committen müssen
// oder Sie müssen auto_commit in Ihrer Konfiguration ausführen.

Simple Example

Hier ist ein einfaches Beispiel, wie Sie das verwenden könnten.

Flight::route('POST /login', function() {
    $session = Flight::session();

    // führen Sie hier Ihre Login-Logik aus
    // Passwort validieren usw.

    // wenn der Login erfolgreich ist
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // immer wenn Sie in die Sitzung schreiben, müssen Sie sie explizit committen.
    $session->commit();
});

// Diese Überprüfung könnte in der Logik der eingeschränkten Seite erfolgen oder mit Middleware umgeben sein.
Flight::route('/some-restricted-page', function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }

    // führen Sie hier Ihre Logik für die eingeschränkte Seite aus
});

// die Middleware-Version
Flight::route('/some-restricted-page', function() {
    // reguläre Seitenslogik
})->addMiddleware(function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }
});

More Complex Example

Hier ist ein komplexeres Beispiel, wie Sie das verwenden könnten.

use Ghostff\Session\Session;

require 'vendor/autoload.php';

$app = Flight::app();

// geben Sie als ersten Argument einen benutzerdefinierten Pfad zu Ihrer Sitzungskonfigurationsdatei an
// oder geben Sie das benutzerdefinierte Array
$app->register('session', Session::class, [ 
    [
        // wenn Sie Ihre Sitzungsdaten in einer Datenbank speichern möchten (gut für Funktionen wie "mich von allen Geräten abmelden")
        Session::CONFIG_DRIVER        => Ghostff\Session\Drivers\MySql::class,
        Session::CONFIG_ENCRYPT_DATA  => true,
        Session::CONFIG_SALT_KEY      => hash('sha256', 'my-super-S3CR3T-salt'), // bitte ändern Sie das zu etwas anderem
        Session::CONFIG_AUTO_COMMIT   => true, // tun Sie das nur, wenn es erforderlich ist und/oder es schwierig ist, commit() für Ihre Sitzung aufzurufen.
                                                // zusätzlich könnten Sie Flight::after('start', function() { Flight::session()->commit(); }); machen.
        Session::CONFIG_MYSQL_DS         => [
            'driver'    => 'mysql',             # Database driver for PDO dns eg(mysql:host=...;dbname=...)
            'host'      => '127.0.0.1',         # Database host
            'db_name'   => 'my_app_database',   # Database name
            'db_table'  => 'sessions',          # Database table
            'db_user'   => 'root',              # Database username
            'db_pass'   => '',                  # Database password
            'persistent_conn'=> false,          # Vermeiden Sie den Aufwand, eine neue Verbindung bei jedem Skriptaufruf herzustellen, was zu einer schnelleren Web-Anwendung führt. FINDEN SIE DIE NACHTEILE SELBST
        ]
    ] 
]);

Help! My Session Data is Not Persisting!

Setzen Sie Ihre Sitzungsdaten und sie persistieren nicht zwischen Anfragen? Sie haben vielleicht vergessen, Ihre Sitzungsdaten zu committen. Sie können das tun, indem Sie $session->commit() aufrufen, nachdem Sie Ihre Sitzungsdaten gesetzt haben.

Flight::route('POST /login', function() {
    $session = Flight::session();

    // führen Sie hier Ihre Login-Logik aus
    // Passwort validieren usw.

    // wenn der Login erfolgreich ist
    $session->set('is_logged_in', true);
    $session->set('user', $user);

    // immer wenn Sie in die Sitzung schreiben, müssen Sie sie explizit committen.
    $session->commit();
});

Die andere Möglichkeit, das zu umgehen, ist, wenn Sie Ihren Sitzungsdienst einrichten, auto_commit in Ihrer Konfiguration auf true setzen. Das wird Ihre Sitzungsdaten automatisch nach jeder Anfrage committen.

$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
        $session->updateConfiguration([
            Session::CONFIG_AUTO_COMMIT   => true,
        ]);
    }
);

Zusätzlich könnten Sie Flight::after('start', function() { Flight::session()->commit(); }); machen, um Ihre Sitzungsdaten nach jeder Anfrage zu committen.

Documentation

Besuchen Sie die Github Readme für die vollständige Dokumentation. Die Konfigurationsoptionen sind gut dokumentiert in der default_config.php-Datei selbst. Der Code ist einfach zu verstehen, wenn Sie dieses Paket selbst durchsehen möchten.

Awesome-plugins/active_record

Flight Active Record

Ein aktiver Datensatz (Active Record) bildet eine Datenbankentität auf ein PHP-Objekt ab. Einfach gesagt: Wenn du eine Tabelle users in deiner Datenbank hast, kannst du eine Zeile in dieser Tabelle in eine User-Klasse und ein $user-Objekt in deinem Code übersetzen. Siehe Grundlegendes Beispiel.

Klicke hier für das Repository auf GitHub.

Grundlegendes Beispiel

Angenommen, du hast die folgende Tabelle:

CREATE TABLE users (
    id INTEGER PRIMARY KEY, 
    name TEXT, 
    password TEXT 
);

Jetzt kannst du eine neue Klasse einrichten, um diese Tabelle darzustellen:

/**
 * Eine ActiveRecord-Klasse ist normalerweise im Singular.
 * 
 * Es wird dringend empfohlen, die Eigenschaften der Tabelle hier als Kommentare hinzuzufügen.
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // du kannst es so setzen
        parent::__construct($database_connection, 'users');
        // oder so
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

Jetzt sieh zu, wie die Magie passiert!

// für sqlite
$database_connection = new PDO('sqlite:test.db'); // das ist nur ein Beispiel, du würdest wahrscheinlich eine echte Datenbankverbindung verwenden

// für mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');

// oder mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// oder mysqli mit nicht objekbasierter Erstellung
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');

$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// oder $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// du kannst hier nicht $user->save() verwenden, sonst wird es als Update angesehen!

echo $user->id; // 2

Und so einfach war es, einen neuen Benutzer hinzuzufügen! Jetzt, wo es eine Benutzerzeile in der Datenbank gibt, wie holst du sie heraus?

$user->find(1); // findet id = 1 in der Datenbank und gibt sie zurück.
echo $user->name; // 'Bobby Tables'

Und was, wenn du alle Benutzer finden möchtest?

$users = $user->findAll();

Und mit einer bestimmten Bedingung?

$users = $user->like('name', '%mamma%')->findAll();

Siehst du, wie viel Spaß das macht? Lass uns es installieren und loslegen!

Installation

Einfach mit Composer installieren:

composer require flightphp/active-record 

Verwendung

Dies kann als eigenständige Bibliothek oder mit dem Flight PHP Framework verwendet werden. Ganz dir überlassen.

Eigenständig

Stelle einfach sicher, dass du eine PDO-Verbindung an den Konstruktor übergibst.

$pdo_connection = new PDO('sqlite:test.db'); // das ist nur ein Beispiel, du würdest wahrscheinlich eine echte Datenbankverbindung verwenden

$User = new User($pdo_connection);

Möchtest du nicht jedes Mal deine Datenbankverbindung im Konstruktor setzen? Siehe Datenbankverbindungsverwaltung für andere Ideen!

Als Methode in Flight registrieren

Wenn du das Flight PHP Framework verwendest, kannst du die ActiveRecord-Klasse als Dienst registrieren, musst es aber ehrlich gesagt nicht.

Flight::register('user', 'User', [ $pdo_connection ]);

// dann kannst du es so in einem Controller, einer Funktion usw. verwenden.

Flight::user()->find(1);

runway-Methoden

runway ist ein CLI-Werkzeug für Flight, das einen eigenen Befehl für diese Bibliothek hat.

# Verwendung
php runway make:record database_table_name [class_name]

# Beispiel
php runway make:record users

Dies erstellt eine neue Klasse im Verzeichnis app/records/ als UserRecord.php mit folgendem Inhalt:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * ActiveRecord-Klasse für die users-Tabelle.
 * @link https://docs.flightphp.com/en/v3/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Setzt die Beziehungen für das Modell
     *   https://docs.flightphp.com/en/v3/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

    /**
     * Konstruktor
     * @param mixed $databaseConnection Die Verbindung zur Datenbank
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

CRUD-Funktionen

find($id = null) : boolean|ActiveRecord

Findet einen Datensatz und weist ihn dem aktuellen Objekt zu. Wenn du eine $id übergibst, wird eine Suche über den Primärschlüssel mit diesem Wert durchgeführt. Wenn nichts übergeben wird, wird einfach der erste Datensatz in der Tabelle gefunden.

Zusätzlich kannst du andere Hilfsmethoden übergeben, um deine Tabelle abzufragen.

// einen Datensatz mit bestimmten Bedingungen im Vorfeld finden
$user->notNull('password')->orderBy('id DESC')->find();

// einen Datensatz über eine bestimmte ID finden
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

Findet alle Datensätze in der angegebenen Tabelle.

$user->findAll();

first(): ActiveRecord (v0.8.0)

Findet den ersten Datensatz, der deinen Bedingungen entspricht. Wenn du keine Sortierung gesetzt hast, sortiert er nach dem Primärschlüssel aufsteigend. Wenn nichts übereinstimmt, bekommst du den Datensatz unbearbeitet zurück. Prüfe also isHydrated(), wenn du dir nicht sicher bist, ob etwas zurückgekommen ist.

$user->eq('status', 'active')->first();

last(): ActiveRecord (v0.8.0)

Gleiches wie first(), sortiert aber nach dem Primärschlüssel absteigend. Praktisch für Abfragen wie „gib mir den neuesten".

$user->eq('status', 'active')->last();

count(): int (v0.8.0)

Zählt die Zeilen, die deinen aktuellen Bedingungen entsprechen. Wenn du ein groupBy() in der Abfrage hast, ignoriert count() dies absichtlich. Ein einzelner skalarer Zählwert kann nicht eine Zeile pro Gruppe darstellen.

$user->count();
$user->eq('status', 'active')->count();

exists(): bool (v0.8.0)

Gibt true zurück, wenn irgendein Datensatz deinen Bedingungen entspricht. Unter der Haube führt es ein günstiges SELECT 1 ... LIMIT 1 aus.

$user->eq('name', 'Bobby')->exists(); // true

pluck(string $column): array (v0.8.0)

Gibt ein flaches Array von Werten aus einer Spalte zurück, anstatt eine Reihe von Objekten zu hydrieren. Kombiniere es mit distinct(), um eindeutige Werte zu erhalten.

$user->pluck('name'); // [ 'Bobby', 'Joseph' ]
$user->distinct()->pluck('status'); // [ 'active', 'inactive' ]

ids(): array (v0.8.0)

Eine Abkürzung für pluck() auf dem Primärschlüssel.

$user->gt('id', 0)->ids(); // [ 1, 2, 3 ]

isHydrated(): boolean (v0.4.0)

Gibt true zurück, wenn der aktuelle Datensatz hydriert (aus der Datenbank geholt) wurde.

$user->find(1);
// wenn ein Datensatz mit Daten gefunden wurde...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

Fügt den aktuellen Datensatz in die Datenbank ein.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Textbasierte Primärschlüssel

Wenn du einen textbasierten Primärschlüssel hast (z. B. eine UUID), kannst du den Primärschlüsselwert vor dem Einfügen auf eine von zwei Arten setzen.

$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // oder $user->save();

oder du kannst den Primärschlüssel automatisch durch Events generieren lassen.

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // du kannst den Primärschlüssel auch so setzen, anstatt über das Array oben.
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // oder wie auch immer du deine eindeutigen IDs generieren musst
    }
}

Wenn du den Primärschlüssel vor dem Einfügen nicht setzt, wird er auf die rowid gesetzt und die Datenbank generiert ihn für dich, aber er wird nicht gespeichert, da dieses Feld möglicherweise nicht in deiner Tabelle existiert. Deshalb wird empfohlen, das Event zu verwenden, um dies automatisch für dich zu erledigen.

update(): boolean|ActiveRecord

Aktualisiert den aktuellen Datensatz in der Datenbank.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();

updateAttribute(string $name, mixed $value): ActiveRecord (v0.8.0)

Aktualisiert eine einzelne Spalte eines geladenen Datensatzes und speichert ihn. Es ist eine Abkürzung für $user->dirty([ 'name' => $value ])->update(). Du brauchst dafür einen geladenen Datensatz.

$user->find(1);
$user->updateAttribute('name', 'New Name');

save(): boolean|ActiveRecord

Fügt den aktuellen Datensatz in die Datenbank ein oder aktualisiert ihn. Wenn der Datensatz eine ID hat, wird aktualisiert, andernfalls eingefügt.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();

Hinweis: Wenn du Beziehungen in der Klasse definiert hast, werden diese Beziehungen auch rekursiv gespeichert, wenn sie definiert, instanziiert wurden und veränderte Daten (dirty data) zu aktualisieren haben. (v0.4.0 und höher)

delete(): boolean

Löscht den aktuellen Datensatz aus der Datenbank.

$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();

Du kannst auch mehrere Datensätze löschen, indem du vorher eine Suche ausführst.

$user->like('name', 'Bob%')->delete();

updateAll(array $attributes, bool $allowEmptyConditions = false): int (v0.8.0)

Aktualisiert alle Datensätze, die deinen Bedingungen entsprechen, in einer einzigen Anweisung. Es werden keine Datensätze hydriert und keine Events ausgelöst, genau deshalb ist es schnell. Gibt die Anzahl der betroffenen Zeilen zurück.

Es weigert sich, ohne WHERE-Bedingungen zu laufen, es sei denn, du übergibst true für das zweite Argument. Dein zukünftiges Ich dankt dir.

$user->eq('status', 'inactive')->updateAll([ 'status' => 'active' ]);

// ja, du willst wirklich jede Zeile in der Tabelle aktualisieren
$user->updateAll([ 'status' => 'active' ], true);

deleteAll(bool $allowEmptyConditions = false): int (v0.8.0)

Löscht alle Datensätze, die deinen Bedingungen entsprechen, in einer einzigen Anweisung. Gleiche Sache wie bei updateAll(): keine Hydrierung, keine Events, und es erfordert WHERE-Bedingungen, es sei denn, du übergibst true. Gibt die Anzahl der gelöschten Zeilen zurück. Mit Vorsicht verwenden!

$user->eq('status', 'deleted')->deleteAll();

dirty(array $dirty = []): ActiveRecord

„Dirty data" bezieht sich auf Daten, die in einem Datensatz geändert wurden.

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// nichts ist zu diesem Zeitpunkt "dirty".

$user->email = 'test@example.com'; // jetzt gilt "email" als "dirty", da es geändert wurde.
$user->update();
// jetzt gibt es keine "dirty" Daten, da sie aktualisiert und in der Datenbank gespeichert wurden

$user->password = password_hash()'newpassword'); // jetzt ist dies "dirty"
$user->dirty(); // wenn nichts übergeben wird, werden alle "dirty"-Einträge gelöscht.
$user->update(); // nichts wird aktualisiert, da nichts als "dirty" erfasst wurde.

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // sowohl name als auch password werden aktualisiert.

copyFrom(array $data): ActiveRecord (v0.4.0)

Dies ist ein Alias für die dirty()-Methode. Es ist etwas klarer, was du tust.

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // sowohl name als auch password werden aktualisiert.

isDirty(): boolean (v0.4.0)

Gibt true zurück, wenn der aktuelle Datensatz geändert wurde.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true

reset(bool $include_query_data = true): ActiveRecord

Setzt den aktuellen Datensatz auf seinen ursprünglichen Zustand zurück. Das ist sehr gut für Schleifenverhalten geeignet. Wenn du true übergibst, wird auch die Abfragedaten zurückgesetzt, die verwendet wurden, um das aktuelle Objekt zu finden (Standardverhalten).

$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);

foreach($users as $user) {
    $user_company->reset(); // mit einem sauberen Blatt starten
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

Nachdem du eine find()-, findAll()-, insert()-, update()- oder save()-Methode ausgeführt hast, kannst du das erstellte SQL abrufen und zu Debugging-Zwecken verwenden.

Transaktionen

Musst du ein paar Schreibvorgänge ausführen, die alle gemeinsam erfolgreich sein müssen? Wickle sie in transaction() (v0.8.0). Übergib ein Callable, und der Datensatz kommt als Argument hinein. Wenn das Callable eine Exception wirft, wird alles zurückgerollt und die Exception wird für dich erneut geworfen. Andernfalls wird committet und das zurückgegeben, was dein Callable zurückgegeben hat.

$user->transaction(function ($user) {
    $user->name = 'Bobby Tables';
    $user->password = password_hash('correct horse battery staple');
    $user->insert();

    $user->email = 'bobby@example.com';
    $user->update();
    // Der Commit passiert hier, wenn nichts geworfen wurde
});

Verschachtelte Transaktionen werden nicht unterstützt (keine Savepoints), also halte sie flach.

SQL-Abfragemethoden

select(string $field1 [, string $field2 ... ])

Du kannst nur ein paar der Spalten in einer Tabelle auswählen, wenn du möchtest (das ist bei sehr breiten Tabellen mit vielen Spalten performanter).

$user->select('id', 'name')->find();

from(string $table)

Du kannst technisch gesehen auch eine andere Tabelle wählen! Warum auch nicht?!

$user->select('id', 'name')->from('user')->find();

join(string $table_name, string $join_condition)

Du kannst sogar eine andere Tabelle in der Datenbank joinen.

$user->join('contacts', 'contacts.user_id = users.id')->find();

where(string $where_conditions)

Du kannst eigene WHERE-Argumente setzen (du kannst keine Parameter in dieser WHERE-Anweisung setzen).

$user->where('id=1 AND name="demo"')->find();

Sicherheitshinweis – Du könntest versucht sein, so etwas zu tun: $user->where("id = '{$id}' AND name = '{$name}'")->find();. BITTE TU DAS NICHT!!! Dies ist anfällig für sogenannte SQL-Injection-Angriffe. Es gibt viele Artikel online. Bitte suche bei Google nach „sql injection attacks php" und du wirst viele Artikel zu diesem Thema finden. Der richtige Weg, dies mit dieser Bibliothek zu handhaben, ist, anstelle dieser where()-Methode so etwas zu tun: $user->eq('id', $id)->eq('name', $name)->find();. Wenn du es unbedingt tun musst, hat die PDO-Bibliothek $pdo->quote($var), um es für dich zu escapen. Nur nach Verwendung von quote() kannst du es in einer where()-Anweisung verwenden.

group(string $group_by_statement)/groupBy(string $group_by_statement)

Gruppiere deine Ergebnisse nach einer bestimmten Bedingung.

$user->select('COUNT(*) as count')->groupBy('name')->findAll();

order(string $order_by_statement)/orderBy(string $order_by_statement)

Sortiere die zurückgegebene Abfrage auf eine bestimmte Weise.

$user->orderBy('name DESC')->find();

orderByColumn(string $column, string $direction = 'ASC') (v0.7.2)

order() und orderBy() akzeptieren rohe SQL-Fragmente, was in Ordnung ist, wenn du 'name DESC' fest codierst. Wenn der Spaltenname von Benutzereingaben stammt (z. B. eine sortierbare Tabellenkopfzeile), verwende stattdessen orderByColumn(). Es sind nur einfache Spaltennamen und table.column-Pfade erlaubt, und die Richtung muss ASC oder DESC sein, also gibt es nichts, was man injizieren könnte.

// $sortColumn kommt aus der Anfrage
$user->orderByColumn($sortColumn, 'DESC')->findAll();

limit(string $limit)/limit(int $offset, int $limit)

Begrenze die Anzahl der zurückgegebenen Datensätze. Wenn eine zweite Ganzzahl angegeben wird, ist es Offset, Limit, genau wie in SQL.

$user->orderby('name DESC')->limit(0, 10)->findAll();

distinct() (v0.8.0)

Fügt DISTINCT zu deiner nächsten Abfrage hinzu. Es funktioniert bei normalen Selects und bei pluck(). count() ignoriert es, da das Setzen von DISTINCT auf eine einzelne Aggregatzeile nichts bewirkt.

$user->distinct()->pluck('status'); // [ 'active', 'inactive' ]

WHERE-Bedingungen

equal(string $field, mixed $value) / eq(string $field, mixed $value)

Wo field = $value

$user->eq('id', 1)->find();

notEqual(string $field, mixed $value) / ne(string $field, mixed $value)

Wo field <> $value

$user->ne('id', 1)->find();

isNull(string $field)

Wo field IS NULL

$user->isNull('id')->find();

isNotNull(string $field) / notNull(string $field)

Wo field IS NOT NULL

$user->isNotNull('id')->find();

greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)

Wo field > $value

$user->gt('id', 1)->find();

lessThan(string $field, mixed $value) / lt(string $field, mixed $value)

Wo field < $value

$user->lt('id', 1)->find();

greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)

Wo field >= $value

$user->ge('id', 1)->find();

lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)

Wo field <= $value

$user->le('id', 1)->find();

like(string $field, mixed $value) / notLike(string $field, mixed $value)

Wo field LIKE $value oder field NOT LIKE $value

$user->like('name', 'de')->find();

in(string $field, array $values) / notIn(string $field, array $values)

Wo field IN($value) oder field NOT IN($value)

$user->in('id', [1, 2])->find();

between(string $field, array $values)

Wo field BETWEEN $value AND $value1

$user->between('id', [1, 2])->find();

ODER-Bedingungen

Es ist möglich, deine Bedingungen in eine ODER-Anweisung zu packen. Dies geschieht entweder mit den Methoden startWrap() und endWrap() oder indem du den dritten Parameter der Bedingung nach dem Feld und dem Wert ausfüllst.

// Methode 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// Dies wird ausgewertet zu `id = 1 AND (name = 'demo' OR name = 'test')`

// Methode 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Dies wird ausgewertet zu `id = 1 OR name = 'demo'`

Scopes

Scopes (v0.8.0) sind wiederverwendbare Abfrageketten, die als einfache Instanzmethoden in deiner Klasse definiert werden und $this zurückgeben. Sobald du einen geschrieben hast, verhält er sich wie jede andere Abfragemethode.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    public function active(): self
    {
        return $this->eq('status', 'active');
    }

    public function recent(int $days = 7): self
    {
        return $this->ge('created_at', date('Y-m-d', strtotime("-{$days} days")));
    }
}

// und jetzt lesen sich deine Abfragen wie Sätze
(new User($pdo_connection))->active()->recent(30)->findAll();

Du kannst einen Scope auch mit scope() namentlich aufrufen, was praktisch ist, wenn der Scopename von irgendwo anders in deinem Code kommt. Es wird eine BadMethodCallException geworfen, wenn die Methode nicht existiert.

$user->scope('active')->findAll();
$user->scope('recent', 30)->findAll();

Beziehungen

Du kannst mit dieser Bibliothek verschiedene Arten von Beziehungen festlegen. Du kannst 1:n- und 1:1-Beziehungen zwischen Tabellen festlegen. Das erfordert etwas zusätzliche Einrichtung in der Klasse im Vorfeld.

Das $relations-Array zu setzen ist nicht schwer, aber die richtige Syntax zu erraten, kann verwirrend sein.

protected array $relations = [
    // du kannst den Schlüssel beliebig benennen. Der Name des ActiveRecords ist wahrscheinlich gut. Z.B.: user, contact, client
    'user' => [
        // erforderlich
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // dies ist die Art der Beziehung

        // erforderlich
        'Some_Class', // dies ist die "andere" ActiveRecord-Klasse, auf die verwiesen wird

        // erforderlich
        // abhängig von der Art der Beziehung
        // self::HAS_ONE = der Fremdschlüssel, der auf den Join verweist
        // self::HAS_MANY = der Fremdschlüssel, der auf den Join verweist
        // self::BELONGS_TO = der lokale Schlüssel, der auf den Join verweist
        'local_or_foreign_key',
        // nur zur Info: dies verbindet auch nur mit dem Primärschlüssel des "anderen" Modells

        // optional
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // zusätzliche Bedingungen, die du beim Joinen der Beziehung möchtest
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // optional
        'back_reference_name' // das ist, wenn du diese Beziehung zurück auf sich selbst referenzieren möchtest, z.B.: $user->contact->user;
    ]
];
class User extends ActiveRecord{
    protected array $relations = [
        'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
        'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
    ];

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }
}

class Contact extends ActiveRecord{
    protected array $relations = [
        'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
        'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
    ];
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'contacts');
    }
}

Jetzt haben wir die Referenzen eingerichtet, sodass wir sie sehr einfach verwenden können!

$user = new User($pdo_connection);

// finde den neuesten Benutzer.
$user->notNull('id')->orderBy('id desc')->find();

// Kontakte über die Beziehung abrufen:
foreach($user->contacts as $contact) {
    echo $contact->id;
}

// oder wir können den umgekehrten Weg gehen.
$contact = new Contact();

// einen Kontakt finden
$contact->find();

// Benutzer über die Beziehung abrufen:
echo $contact->user->name; // das ist der Benutzername

Ziemlich cool, oder?

Eager Loading

Übersicht

Eager Loading löst das N+1-Abfrageproblem, indem Beziehungen im Voraus geladen werden. Anstatt für jede Beziehung jedes Datensatzes eine separate Abfrage auszuführen, lädt Eager Loading alle zugehörigen Daten in nur einer zusätzlichen Abfrage pro Beziehung.

Hinweis: Eager Loading ist nur für v0.7.0 und höher verfügbar.

Grundlegende Verwendung

Verwende die with()-Methode, um anzugeben, welche Beziehungen eager geladen werden sollen:

// Benutzer mit ihren Kontakten in 2 Abfragen laden statt N+1
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    foreach ($u->contacts as $contact) {
        echo $contact->email; // Keine zusätzliche Abfrage!
    }
}

Mehrere Beziehungen

Lade mehrere Beziehungen auf einmal:

$users = $user->with(['contacts', 'profile', 'settings'])->findAll();

Beziehungstypen

HAS_MANY
// Alle Kontakte für jeden Benutzer eager laden
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    // $u->contacts ist bereits als Array geladen
    foreach ($u->contacts as $contact) {
        echo $contact->email;
    }
}
HAS_ONE
// Einen Kontakt für jeden Benutzer eager laden
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
    // $u->contact ist bereits als Objekt geladen
    echo $u->contact->email;
}
BELONGS_TO
// Übergeordnete Benutzer für alle Kontakte eager laden
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
    // $c->user ist bereits geladen
    echo $c->user->name;
}
Mit find()

Eager Loading funktioniert sowohl mit findAll() als auch mit find():

$user = $user->with('contacts')->find(1);
// Benutzer und alle seine Kontakte in 2 Abfragen geladen

Leistungsvorteile

Ohne Eager Loading (N+1-Problem):

$users = $user->findAll(); // 1 Abfrage
foreach ($users as $u) {
    $contacts = $u->contacts; // N Abfragen (eine pro Benutzer!)
}
// Gesamt: 1 + N Abfragen

Mit Eager Loading:

$users = $user->with('contacts')->findAll(); // 2 Abfragen gesamt
foreach ($users as $u) {
    $contacts = $u->contacts; // 0 zusätzliche Abfragen!
}
// Gesamt: 2 Abfragen (1 für Benutzer + 1 für alle Kontakte)

Bei 10 Benutzern reduziert das die Abfragen von 11 auf 2 – eine Reduktion um 82 %!

Wichtige Hinweise

Einschränkungen

Benutzerdefinierte Daten setzen

Manchmal musst du etwas Einzigartiges an dein ActiveRecord anhängen, z. B. eine benutzerdefinierte Berechnung, die möglicherweise einfacher direkt an das Objekt angehängt werden kann, das dann z. B. an ein Template übergeben wird.

setCustomData(string $field, mixed $value)

Du hängst die benutzerdefinierten Daten mit der setCustomData()-Methode an.

$user->setCustomData('page_view_count', $page_view_count);

Und dann referenzierst du es einfach wie eine normale Objekteigenschaft.

echo $user->page_view_count;

Zeitstempel

Wenn deine Tabelle created_at- und updated_at-Spalten hat, kann die Bibliothek sie für dich ausfüllen (v0.8.0). Setze protected bool $timestamps = true; in deiner Klasse, und sie setzt created_at und updated_at beim Einfügen und updated_at beim Aktualisieren. Das Format ist Y-m-d H:i:s. Wenn du eine der Spalten selbst setzt, lässt die Bibliothek deinen Wert in Ruhe.

class User extends flight\ActiveRecord {
    protected bool $timestamps = true;

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }
}

Deine Tabelle muss diese Spalten tatsächlich enthalten, sonst schlagen Einfügungen und Aktualisierungen fehl.

Events

Ein weiteres super tolles Feature dieser Bibliothek sind Events. Events werden zu bestimmten Zeitpunkten basierend auf bestimmten aufgerufenen Methoden ausgelöst. Sie sind sehr, sehr hilfreich, um Daten automatisch für dich einzurichten.

onConstruct(ActiveRecord $ActiveRecord, array &config)

Das ist wirklich hilfreich, wenn du eine Standardverbindung oder etwas Ähnliches setzen musst.

// index.php oder bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

//
//
//

// User.php
class User extends flight\ActiveRecord {

    protected function onConstruct(self $self, array &$config) { // denke an die &-Referenz
        // du könntest dies tun, um die Verbindung automatisch zu setzen
        $config['connection'] = Flight::db();
        // oder dies
        $self->transformAndPersistConnection(Flight::db());

        // Du kannst den Tabellennamen auch so setzen.
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

Dies ist wahrscheinlich nur nützlich, wenn du jedes Mal eine Abfrage manipulieren musst.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFind(self $self) {
        // immer id >= 0 ausführen, wenn das dein Ding ist
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

Dies ist wahrscheinlich nützlicher, wenn du immer einige Logik ausführen musst, jedes Mal wenn dieser Datensatz abgerufen wird. Musst du etwas entschlüsseln? Musst du jedes Mal eine benutzerdefinierte Zählabfrage ausführen (nicht performant, aber egal)?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFind(self $self) {
        // etwas entschlüsseln
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // vielleicht etwas Benutzerdefiniertes speichern, wie eine Abfrage???
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']; 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

Dies ist wahrscheinlich nur nützlich, wenn du jedes Mal eine Abfrage manipulieren musst.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFindAll(self $self) {
        // immer id >= 0 ausführen, wenn das dein Ding ist
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

Ähnlich wie afterFind(), aber du kannst es für alle Datensätze tun!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // etwas Cooles tun, wie in afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

Wirklich hilfreich, wenn du jedes Mal Standardwerte setzen musst.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // ein paar solide Standardwerte setzen
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

        if(!$self->password) {
            $self->password = password_hash((string) microtime(true));
        }
    } 
}

afterInsert(ActiveRecord $ActiveRecord)

Vielleicht hast du einen Anwendungsfall, um Daten zu ändern, nachdem sie eingefügt wurden?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // mach dein Ding
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // oder was auch immer....
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

Wirklich hilfreich, wenn du bei einem Update jedes Mal Standardwerte setzen musst.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // ein paar solide Standardwerte setzen
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

Vielleicht hast du einen Anwendungsfall, um Daten zu ändern, nachdem sie aktualisiert wurden?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // mach dein Ding
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // oder was auch immer....
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

Das ist nützlich, wenn du möchtest, dass Events sowohl bei Einfügungen als auch bei Updates passieren. Ich erspare dir die lange Erklärung, aber ich bin sicher, du kannst dir denken, was es ist.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeSave(self $self) {
        $self->last_updated = gmdate('Y-m-d H:i:s');
    } 
}

beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)

Ich bin mir nicht sicher, was du hier tun möchtest, aber hier gibt es keine Verurteilungen! Leg los!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo 'Er war ein tapferer Soldat... :cry-face:';
    } 
}

Datenbankverbindungsverwaltung

Wenn du diese Bibliothek verwendest, kannst du die Datenbankverbindung auf verschiedene Arten setzen. Du kannst die Verbindung im Konstruktor setzen, über eine Konfigurationsvariable $config['connection'] oder über setDatabaseConnection() (v0.4.1).

$pdo_connection = new PDO('sqlite:test.db'); // zum Beispiel
$user = new User($pdo_connection);
// oder
$user = new User(null, [ 'connection' => $pdo_connection ]);
// oder
$user = new User();
$user->setDatabaseConnection($pdo_connection);

Wenn du vermeiden möchtest, jedes Mal eine $database_connection setzen zu müssen, wenn du einen Active Record aufrufst, gibt es Wege, das zu umgehen!

// index.php oder bootstrap.php
// Dies als registrierte Klasse in Flight setzen
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

// User.php
class User extends flight\ActiveRecord {

    public function __construct(array $config = [])
    {
        $database_connection = $config['connection'] ?? Flight::db();
        parent::__construct($database_connection, 'users', $config);
    }
}

// Und jetzt, keine Argumente erforderlich!
$user = new User();

Hinweis: Wenn du Unit-Tests planst, kann dieser Ansatz einige Herausforderungen für Unit-Tests mit sich bringen, aber insgesamt ist es nicht zu schlimm, da du deine Verbindung mit setDatabaseConnection() oder $config['connection'] injizieren kannst.

Wenn du die Datenbankverbindung auffrischen musst, zum Beispiel wenn du ein langlaufendes CLI-Skript ausführst und die Verbindung ab und zu auffrischen musst, kannst du die Verbindung mit $your_record->setDatabaseConnection($pdo_connection) neu setzen.

Mitwirken

Bitte tu es. :D

Einrichtung

Wenn du einen Beitrag leistest, stelle sicher, dass du composer test-coverage ausführst, um 100% Testabdeckung zu erhalten (das ist keine echte Unit-Test-Abdeckung, eher Integrationstests).

Stelle außerdem sicher, dass du composer beautify und composer phpcs ausführst, um etwaige Lint-Fehler zu beheben.

Lizenz

MIT

Awesome-plugins/migrations

Migrationen

Eine Migration für Ihr Projekt verfolgt alle Datenbankänderungen, die mit Ihrem Projekt verbunden sind. byjg/php-migration ist eine wirklich hilfreiche Kernbibliothek, um Ihnen den Einstieg zu erleichtern.

Installation

PHP-Bibliothek

Wenn Sie nur die PHP-Bibliothek in Ihrem Projekt verwenden möchten:

composer require "byjg/migration"

Befehlszeilenschnittstelle

Die Befehlszeilenschnittstelle ist eigenständig und erfordert keine Installation mit Ihrem Projekt.

Sie können global installieren und einen symbolischen Link erstellen.

composer require "byjg/migration-cli"

Bitte besuchen Sie byjg/migration-cli für weitere Informationen zur Migration CLI.

Unterstützte Datenbanken

Datenbank Treiber Verbindungszeichenfolge
Sqlite pdo_sqlite sqlite:///path/to/file
MySql/MariaDb pdo_mysql mysql://username:password@hostname:port/database
Postgres pdo_pgsql pgsql://username:password@hostname:port/database
Sql Server pdo_dblib, pdo_sysbase Linux dblib://username:password@hostname:port/database
Sql Server pdo_sqlsrv Windows sqlsrv://username:password@hostname:port/database

Wie funktioniert es?

Die Datenbankmigration verwendet reines SQL, um das Datenbankversioning zu verwalten. Um es zum Laufen zu bringen, müssen Sie:

Die SQL-Skripte

Die Skripte sind in drei Gruppen unterteilt:

Das Verzeichnis der Skripte ist :

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql

Multi-Entwicklungsumgebung

Wenn Sie mit mehreren Entwicklern und mehreren Branches arbeiten, ist es schwierig zu bestimmen, welche Nummer die nächste ist.

In diesem Fall haben Sie das Suffix "-dev" nach der Versionsnummer.

Sehen Sie sich das Szenario an:

In beiden Fällen werden die Entwickler eine Datei mit dem Namen 43-dev.sql erstellen. Beide Entwickler werden ohne Probleme hoch und runter migrieren können, und Ihre lokale Version wird 43 sein.

Aber Entwickler 1 hat seine Änderungen zusammengeführt und eine endgültige Version 43.sql erstellt (git mv 43-dev.sql 43.sql). Wenn Entwickler 2 seinen lokalen Branch aktualisiert, hat er eine Datei 43.sql (von dev 1) und seine Datei 43-dev.sql. Wenn er versucht, hoch oder runter zu migrieren, wird das Migrationsskript abgebrochen und ihn warnen, dass es zwei Versionen 43 gibt. In diesem Fall muss Entwickler 2 seine Datei in 44-dev.sql aktualisieren und weiterarbeiten, bis er die Änderungen zusammenführt und eine endgültige Version generiert.

Verwendung der PHP-API und Integration in Ihre Projekte

Die grundlegende Verwendung ist

Sehen Sie sich ein Beispiel an:

<?php
// Erstellen Sie die Verbindungs-URI
// Weitere Informationen: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');

// Registrieren Sie die Datenbank oder Datenbanken, die diese URI verarbeiten können:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Erstellen Sie die Migrationsinstanz
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// Fügen Sie eine Callback-Fortschrittsfunktion hinzu, um Informationen von der Ausführung zu erhalten
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
    echo "$action, $currentVersion, ${fileInfo['description']}\n";
});

// Stellen Sie die Datenbank mit dem "base.sql"-Skript wieder her
// und führen Sie ALLE vorhandenen Skripte aus, um die Datenbankversion auf die neueste Version zu bringen
$migration->reset();

// Führen Sie ALLE vorhandenen Skripte für hoch oder runter die Datenbankversion aus
// von der aktuellen Version bis zur $version-Nummer;
// Wenn die Versionsnummer nicht angegeben ist, migrieren Sie bis zur letzten Datenbankversion
$migration->update($version = null);

Das Migrationsobjekt steuert die Datenbankversion.

Erstellen einer Versionskontrolle in Ihrem Projekt

<?php
// Registrieren Sie die Datenbank oder Datenbanken, die diese URI verarbeiten können:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Erstellen Sie die Migrationsinstanz
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// Dieser Befehl erstellt die Versions-Tabelle in Ihrer Datenbank
$migration->createVersion();

Aktuelle Version abrufen

<?php
$migration->getCurrentVersion();

Callback zum Steuern des Fortschritts hinzufügen

<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
    echo "Befehl ausführen: $command bei Version $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});

Instanz des Db-Treibers abrufen

<?php
$migration->getDbDriver();

Um es zu verwenden, besuchen Sie bitte: https://github.com/byjg/anydataset-db

Teilweise Migration vermeiden (nicht verfügbar für MySQL)

Eine partielle Migration ist, wenn das Migrationsskript in der Mitte des Prozesses aufgrund eines Fehlers oder einer manuellen Unterbrechung unterbrochen wird.

Die Migrationstabelle hat den Status partial up oder partial down und muss manuell behoben werden, bevor sie wieder migrieren kann.

Um diese Situation zu vermeiden, können Sie angeben, dass die Migration in einem Transaktionskontext ausgeführt wird. Wenn das Migrationsskript fehlschlägt, wird die Transaktion zurückgesetzt und die Migrationstabelle wird als complete markiert und die Version wird die unmittelbar vorherige Version vor dem Skript sein, das den Fehler verursacht hat.

Um diese Funktion zu aktivieren, müssen Sie die Methode withTransactionEnabled aufrufen und true als Parameter übergeben:

<?php
$migration->withTransactionEnabled(true);

HINWEIS: Diese Funktion ist nicht für MySQL verfügbar, da es DDL-Befehle innerhalb einer Transaktion nicht unterstützt. Wenn Sie diese Methode mit MySQL verwenden, ignoriert die Migration sie stillschweigend. Weitere Informationen: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html

Tipps zum Schreiben von SQL-Migrationen für Postgres

Zur Erstellung von Triggern und SQL-Funktionen

-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Überprüfen, ob empname und salary angegeben sind
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname darf nicht null sein'; -- es ist egal, ob diese Kommentare leer sind oder nicht
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% darf kein null Gehalt haben', NEW.empname; --
        END IF; --

        -- Wer arbeitet für uns, wenn sie dafür bezahlen müssen?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% darf kein negatives Gehalt haben', NEW.empname; --
        END IF; --

        -- Merken Sie, wer die Gehaltsliste wann geändert hat
        NEW.last_date := current_timestamp; --
        NEW.last_user := current_user; --
        RETURN NEW; --
    END; --
$emp_stamp$ LANGUAGE plpgsql;


-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Überprüfen, ob empname und salary angegeben sind
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname darf nicht null sein';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% darf kein null Gehalt haben', NEW.empname;
        END IF;

        -- Wer arbeitet für uns, wenn sie dafür bezahlen müssen?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% darf kein negatives Gehalt haben', NEW.empname;
        END IF;

        -- Merken Sie, wer die Gehaltsliste wann geändert hat
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

Da die PDO-Datenbank-Abstraktionsschicht keine Batchverarbeitungen von SQL-Anweisungen ausführen kann, muss byjg/migration, wenn es eine Migrationsdatei liest, den gesamten Inhalt der SQL-Datei an den Semikolons aufteilen und die Anweisungen einzeln ausführen. Es gibt jedoch eine Art von Anweisung, die mehrere Semikolons in ihrem Körper haben kann: Funktionen.

Um Funktionen korrekt parsen zu können, begann byjg/migration 2.1.0 damit, Migrationsdateien an der Semikolon + EOL-Sequenz anstelle nur am Semikolon aufzuteilen. Auf diese Weise kann byjg/migration sie parsen, wenn Sie nach jedem inneren Semikolon einer Funktionsdefinition einen leeren Kommentar anhängen.

Leider wird die Bibliothek die CREATE FUNCTION-Anweisung in mehrere Teile aufteilen und die Migration wird fehlschlagen, wenn Sie vergessen, diese Kommentare hinzuzufügen.

Vermeidung des Doppelpunktzeichens (:)

-- DO
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
  check_in   DATE NOT NULL
);


-- DON'T
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
  check_in   DATE NOT NULL
);

Da PDO das Doppelpunkt-Zeichen verwendet, um benannte Parameter in vorbereiteten Anweisungen zu kennzeichnen, führt die Verwendung zu Problemen in anderen Kontexten.

Zum Beispiel können PostgreSQL-Anweisungen :: verwenden, um Werte zwischen Typen zu casten. Auf der anderen Seite wird PDO dies als ungültigen benannten Parameter in einem ungültigen Kontext behandeln und fehlschlagen, wenn er versucht, es auszuführen.

Die einzige Möglichkeit, diese Inkonsistenz zu beheben, besteht darin, Doppelpunkte ganz zu vermeiden (in diesem Fall hat PostgreSQL auch eine alternative Syntax: CAST(value AS type)).

Verwenden Sie einen SQL-Editor

Abschließend kann das Schreiben manueller SQL-Migrationen mühsam sein, aber es ist deutlich einfacher, wenn Sie einen Editor verwenden, der die SQL-Syntax versteht, Autovervollständigung bietet, Ihr aktuelles Datenbankschema inspiziert und/oder Ihren Code automatisch formatiert.

Umgang mit unterschiedlichen Migrationen innerhalb eines Schemas

Wenn Sie unterschiedliche Migrationsskripte und Versionen innerhalb desselben Schemas erstellen müssen, ist dies möglich, aber es ist zu riskant und ich empfehle es nicht.

Um dies zu tun, müssen Sie unterschiedliche "Migrationstabellen" erstellen, indem Sie den Parameter an den Konstruktor übergeben.

<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEUER_MIGRATIONSTABELLENNAME");

Aus Sicherheitsgründen ist diese Funktion nicht über die Befehlszeile verfügbar, aber Sie können die Umgebungsvariable MIGRATION_VERSION verwenden, um den Namen zu speichern.

Wir empfehlen dringend, diese Funktion nicht zu verwenden. Die Empfehlung ist, eine Migration für ein Schema durchzuführen.

Ausführen von Unit-Tests

Basis-Unit-Tests können ausgeführt werden mit:

vendor/bin/phpunit

Ausführen von Datenbanktests

Integrationstests erfordern, dass Sie die Datenbanken online und verfügbar haben. Wir haben ein einfaches docker-compose.yml bereitgestellt, und Sie können es verwenden, um die Datenbanken für Tests zu starten.

Ausführen der Datenbanken

docker-compose up -d postgres mysql mssql

Führen Sie die Tests aus

vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*

Optional können Sie den Host und das Passwort, das von den Unit-Tests verwendet wird, festlegen.

export MYSQL_TEST_HOST=localhost     # standardmäßig localhost
export MYSQL_PASSWORD=newpassword      # verwenden Sie '.' wenn Sie ein leeres Passwort haben möchten
export PSQL_TEST_HOST=localhost        # standardmäßig localhost
export PSQL_PASSWORD=newpassword       # verwenden Sie '.' wenn Sie ein leeres Passwort haben möchten
export MSSQL_TEST_HOST=localhost       # standardmäßig localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db   # standardmäßig /tmp/test.db

Awesome-plugins/jwt

Firebase JWT - JSON Web Token Authentifizierung

JWT (JSON Web Tokens) sind eine kompakte, URL-sichere Methode, um Ansprüche zwischen Ihrer Anwendung und einem Client darzustellen. Sie eignen sich perfekt für zustandslose API-Authentifizierung – kein Bedarf an serverseitiger Sitzungsspeicherung! Diese Anleitung zeigt Ihnen, wie Sie Firebase JWT mit Flight für sichere, tokenbasierte Authentifizierung integrieren.

Besuchen Sie das Github-Repository für die vollständige Dokumentation und Details.

Was ist JWT?

Ein JSON Web Token ist eine Zeichenkette, die aus drei Teilen besteht:

  1. Header: Metadaten über den Token (Algorithmus, Typ)
  2. Payload: Ihre Daten (Benutzer-ID, Rollen, Ablaufdatum usw.)
  3. Signature: Kryptografische Signatur zur Überprüfung der Authentizität

Beispiel-JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (sieht aus wie Kauderwelsch, ist aber strukturierte Daten!)

Warum JWT verwenden?

Installation

Installieren Sie es über Composer:

composer require firebase/php-jwt

Grundlegende Verwendung

Hier ist ein schnelles Beispiel zum Erstellen und Überprüfen eines JWT:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Ihr geheimer Schlüssel (HALTEN SIE DAS SICHER!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';

// Einen Token erstellen
$payload = [
    'user_id' => 123,
    'username' => 'johndoe',
    'role' => 'admin',
    'iat' => time(),              // Issued at
    'exp' => time() + 3600        // Läuft in 1 Stunde ab
];

$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;

// Einen Token überprüfen und dekodieren
try {
    $decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
    echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
    echo "Ungültiger Token: " . $e->getMessage();
}

JWT-Middleware für Flight (Empfohlener Ansatz)

Die gängigste und nützlichste Methode, JWT mit Flight zu verwenden, ist als Middleware, um Ihre API-Routen zu schützen. Hier ist ein vollständiges, produktionsreifes Beispiel:

Schritt 1: Erstellen einer JWT-Middleware-Klasse

// app/middleware/JwtMiddleware.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use flight\Engine;

class JwtMiddleware {

    protected Engine $app;
    protected string $secretKey;

    public function __construct(Engine $app) {
        $this->app = $app;
        // Speichern Sie Ihren geheimen Schlüssel in app/config/config.php, NICHT hartcodiert!
        $this->secretKey = $app->get('config')['jwt_secret'];
    }

    public function before(array $params) {
        $authHeader = $this->app->request()->getHeader('Authorization');

        // Überprüfen, ob der Authorization-Header existiert
        if (empty($authHeader)) {
            $this->app->jsonHalt(['error' => 'Kein Autorisierungstoken bereitgestellt'], 401);
        }

        // Token aus dem "Bearer <token>"-Format extrahieren
        if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
            $this->app->jsonHalt(['error' => 'Ungültiges Autorisierungsformat. Verwenden Sie: Bearer <token>'], 401);
        }

        $jwt = $matches[1];

        try {
            // Den Token dekodieren und überprüfen
            $decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));

            // Benutzerdaten in der Anfrage speichern, um sie in Routen-Handlern zu verwenden
            $this->app->request()->data->user = $decoded;

        } catch (ExpiredException $e) {
            $this->app->jsonHalt(['error' => 'Token ist abgelaufen'], 401);
        } catch (SignatureInvalidException $e) {
            $this->app->jsonHalt(['error' => 'Ungültige Token-Signatur'], 401);
        } catch (Exception $e) {
            $this->app->jsonHalt(['error' => 'Ungültiger Token: ' . $e->getMessage()], 401);
        }
    }
}

Schritt 2: JWT-Geheimschlüssel in Ihrer Konfiguration registrieren

// app/config/config.php
return [
    'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];

// app/config/bootstrap.php oder index.php
// Stellen Sie sicher, dass Sie diese Zeile hinzufügen, wenn Sie die Konfiguration der App zugänglich machen möchten
$app->set('config', $config);

Sicherheitshinweis: Codieren Sie Ihren geheimen Schlüssel niemals hart! Verwenden Sie Umgebungsvariablen in der Produktion.

Schritt 3: Ihre Routen mit Middleware schützen

// Eine einzelne Route schützen
Flight::route('GET /api/user/profile', function() {
    $user = Flight::request()->data->user; // Von der Middleware gesetzt
    Flight::json([
        'user_id' => $user->user_id,
        'username' => $user->username,
        'role' => $user->role
    ]);
})->addMiddleware(JwtMiddleware::class);

// Eine gesamte Gruppe von Routen schützen (häufiger!)
Flight::group('/api', function() {
    Flight::route('GET /users', function() { /* ... */ });
    Flight::route('GET /posts', function() { /* ... */ });
    Flight::route('POST /posts', function() { /* ... */ });
    Flight::route('DELETE /posts/@id', function($id) { /* ... */ });
}, [ JwtMiddleware::class ]); // Alle Routen in dieser Gruppe sind geschützt!

Für weitere Details zu Middleware siehe die Middleware-Dokumentation.

Häufige Anwendungsfälle

1. Login-Endpunkt (Token-Generierung)

Erstellen Sie eine Route, die nach erfolgreicher Authentifizierung ein JWT generiert:

Flight::route('POST /api/login', function() {
    $data = Flight::request()->data;
    $username = $data->username ?? '';
    $password = $data->password ?? '';

    // Anmeldeinformationen validieren (Beispiel – verwenden Sie Ihre eigene Logik!)
    $user = validateUserCredentials($username, $password);

    if (!$user) {
        Flight::jsonHalt(['error' => 'Ungültige Anmeldeinformationen'], 401);
    }

    // JWT generieren
    $secretKey = Flight::get('config')['jwt_secret'];
    $payload = [
        'user_id' => $user->id,
        'username' => $user->username,
        'role' => $user->role,
        'iat' => time(),
        'exp' => time() + (60 * 60) // 1 Stunde Ablauf
    ];

    $jwt = JWT::encode($payload, $secretKey, 'HS256');

    Flight::json([
        'success' => true,
        'token' => $jwt,
        'expires_in' => 3600
    ]);
});

function validateUserCredentials($username, $password) {
    // Ihre Datenbankabfrage und Passwortüberprüfung hier
    // Beispiel:
    $db = Flight::db();
    $user = $db->fetchRow("SELECT * FROM users WHERE username = ?", [$username]);

    if ($user && password_verify($password, $user['password_hash'])) {
        return (object) [
            'id' => $user['id'],
            'username' => $user['username'],
            'role' => $user['role']
        ];
    }
    return null;
}

2. Token-Refresh-Flow

Implementieren Sie ein Refresh-Token-System für langfristige Sitzungen:

Flight::route('POST /api/login', function() {
    // ... Anmeldeinformationen validieren ...

    $secretKey = Flight::get('config')['jwt_secret'];
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    // Kurzlebiges Access-Token (15 Minuten)
    $accessToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'access',
        'iat' => time(),
        'exp' => time() + (15 * 60)
    ], $secretKey, 'HS256');

    // Langlebiges Refresh-Token (7 Tage)
    $refreshToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'refresh',
        'iat' => time(),
        'exp' => time() + (7 * 24 * 60 * 60)
    ], $refreshSecret, 'HS256');

    Flight::json([
        'access_token' => $accessToken,
        'refresh_token' => $refreshToken,
        'expires_in' => 900
    ]);
});

Flight::route('POST /api/refresh', function() {
    $refreshToken = Flight::request()->data->refresh_token ?? '';
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    try {
        $decoded = JWT::decode($refreshToken, new Key($refreshSecret, 'HS256'));

        // Überprüfen, ob es sich um ein Refresh-Token handelt
        if ($decoded->type !== 'refresh') {
            Flight::jsonHalt(['error' => 'Ungültiger Token-Typ'], 401);
        }

        // Neues Access-Token generieren
        $secretKey = Flight::get('config')['jwt_secret'];
        $accessToken = JWT::encode([
            'user_id' => $decoded->user_id,
            'type' => 'access',
            'iat' => time(),
            'exp' => time() + (15 * 60)
        ], $secretKey, 'HS256');

        Flight::json([
            'access_token' => $accessToken,
            'expires_in' => 900
        ]);

    } catch (Exception $e) {
        Flight::jsonHalt(['error' => 'Ungültiges Refresh-Token'], 401);
    }
});

3. Rollenbasierte Zugriffssteuerung

Erweitern Sie Ihre Middleware, um Benutzerrollen zu überprüfen:

class JwtRoleMiddleware {

    protected Engine $app;
    protected array $allowedRoles;

    public function __construct(Engine $app, array $allowedRoles = []) {
        $this->app = $app;
        $this->allowedRoles = $allowedRoles;
    }

    public function before(array $params) {
        // Angenommen, JwtMiddleware hat bereits ausgeführt und Benutzerdaten gesetzt
        $user = $this->app->request()->data->user ?? null;

        if (!$user) {
            $this->app->jsonHalt(['error' => 'Authentifizierung erforderlich'], 401);
        }

        // Überprüfen, ob der Benutzer die erforderliche Rolle hat
        if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
            $this->app->jsonHalt(['error' => 'Unzureichende Berechtigungen'], 403);
        }
    }
}

// Verwendung: Nur-Admin-Route
Flight::route('DELETE /api/users/@id', function($id) {
    // Benutzer löschen Logik
})->addMiddleware([
    JwtMiddleware::class,
    new JwtRoleMiddleware(Flight::app(), ['admin'])
]);

4. Öffentliche API mit Rate Limiting pro Benutzer

Verwenden Sie JWT, um Benutzer ohne Sitzungen zu verfolgen und zu rate-limitieren:

class RateLimitMiddleware {

    public function before(array $params) {
        $user = Flight::request()->data->user ?? null;
        $userId = $user ? $user->user_id : Flight::request()->ip;

        $cacheKey = "rate_limit:$userId";
        // Stellen Sie sicher, dass Sie einen Cache-Dienst in app/config/services.php eingerichtet haben
        $requests = Flight::cache()->get($cacheKey, 0);

        if ($requests >= 100) { // 100 Anfragen pro Stunde
            Flight::jsonHalt(['error' => 'Rate Limit überschritten'], 429);
        }

        Flight::cache()->set($cacheKey, $requests + 1, 3600);
    }
}

Best Practices für Sicherheit

1. Starke geheime Schlüssel verwenden

// Einen sicheren geheimen Schlüssel generieren (einmal ausführen, in .env-Datei speichern)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // Speichern Sie das in Ihrer .env-Datei!

2. Geheimnisse in Umgebungsvariablen speichern

// Commiten Sie Geheimnisse niemals in die Versionskontrolle!
// Verwenden Sie eine .env-Datei und eine Bibliothek wie vlucas/phpdotenv

// .env-Datei:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here

// Sie können auch die app/config/config.php-Datei verwenden, um Ihre Geheimnisse zu speichern
// stellen Sie nur sicher, dass die Konfigurationsdatei nicht in die Versionskontrolle committed wird
// return [
//     'jwt_secret' => 'your-base64-encoded-secret-here',
//     'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];

// In Ihrer App:
$secretKey = getenv('JWT_SECRET');

3. Geeignete Ablaufzeiten festlegen

// Gute Praxis: Kurzlebige Access-Tokens
'exp' => time() + (15 * 60)  // 15 Minuten

// Für Refresh-Tokens: Längere Ablaufzeit
'exp' => time() + (7 * 24 * 60 * 60)  // 7 Tage

4. HTTPS in der Produktion verwenden

JWTs sollten immer über HTTPS übertragen werden. Senden Sie Tokens in der Produktion niemals über einfaches HTTP!

5. Token-Ansprüche validieren

Validieren Sie immer die Ansprüche, die Sie interessieren:

$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

// Überprüfung des Ablaufs wird automatisch von der Bibliothek gehandhabt
// Aber Sie können benutzerdefinierte Validierungen hinzufügen:
if ($decoded->iat > time()) {
    throw new Exception('Token vor Ausstellung verwendet');
}

if (isset($decoded->nbf) && $decoded->nbf > time()) {
    throw new Exception('Token noch nicht gültig');
}

6. Token-Blacklisting für Logout in Betracht ziehen

Für zusätzliche Sicherheit eine Blacklist ungültiger Tokens führen:

Flight::route('POST /api/logout', function() {
    $authHeader = Flight::request()->getHeader('Authorization');
    preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
    $jwt = $matches[1];

    // Ablauf des Tokens extrahieren
    $decoded = Flight::request()->data->user;
    $ttl = $decoded->exp - time();

    // In Cache/Redis bis zum Ablauf speichern
    Flight::cache()->set("blacklist:$jwt", true, $ttl);

    Flight::json(['message' => 'Erfolgreich abgemeldet']);
});

// Zu Ihrer JwtMiddleware hinzufügen:
public function before(array $params) {
    // ... JWT extrahieren ...

    // Blacklist überprüfen
    if (Flight::cache()->get("blacklist:$jwt")) {
        $this->app->jsonHalt(['error' => 'Token wurde widerrufen'], 401);
    }

    // ... Token überprüfen ...
}

Algorithmen und Schlüsseltypen

Firebase JWT unterstützt mehrere Algorithmen:

Symmetrische Algorithmen (HMAC)

$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

Asymmetrische Algorithmen (RSA/ECDSA)

// Schlüssel generieren: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key

$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');

// Mit privatem Schlüssel kodieren
$jwt = JWT::encode($payload, $privateKey, 'RS256');

// Mit öffentlichem Schlüssel dekodieren
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));

Wann RSA verwenden: Verwenden Sie RSA, wenn Sie den öffentlichen Schlüssel für die Verifizierung verteilen müssen (z. B. Microservices, Drittanbieter-Integrationen). Für eine einzelne Anwendung ist HS256 einfacher und ausreichend.

Fehlerbehebung

"Expired token" Fehler

Der exp-Anspruch Ihres Tokens liegt in der Vergangenheit. Geben Sie ein neues Token aus oder implementieren Sie Token-Refresh.

"Signature verification failed"

use Firebase\JWT\JWT;

JWT::$leeway = 60; // 60 Sekunden Uhrzeitänderung erlauben
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

Token wird in Anfragen nicht gesendet

Stellen Sie sicher, dass Ihr Client den Authorization-Header sendet:

// JavaScript-Beispiel
fetch('/api/users', {
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

Methoden

Die Firebase-JWT-Bibliothek stellt diese Kernmethoden zur Verfügung:

Warum diese Bibliothek verwenden?

Siehe auch

Lizenz

Die Firebase-JWT-Bibliothek ist unter der BSD 3-Clause License lizenziert. Details finden Sie im Github-Repository.

License

Die MIT-Lizenz (MIT)

Urheberrecht © 2024 @mikecao, @n0nag0n

Hiermit wird unentgeltlich jeder Person, die eine Kopie der Software und der zugehörigen Dokumentationsdateien (die "Software") erhält, die Erlaubnis erteilt, uneingeschränkt mit der Software zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, Veränderung, Zusammenführung, Veröffentlichung, Verbreitung, Unterlizenzierung und/oder Verkauf von Kopien der Software, und Personen, denen die Software überlassen wird, zu gestatten, dies zu tun, unter den folgenden Bedingungen:

Der obige Urheberrechtsvermerk und dieser Genehmigungsvermerk sind in allen Kopien oder wesentlichen Teilen der Software enthalten.

DIE SOFTWARE WIRD "WIE BESEHEN" BEREITGESTELLT, OHNE JEGLICHE GARANTIE, AUSDRÜCKLICH ODER IMPLIZIERT, EINSCHLIEßLICH DER GARANTIE DER MARKTFÄHIGKEIT, DER EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG. IN KEINEM FALL HAFTEN DIE AUTOREN ODER COPYRIGHT-INHABER FÜR ANSPRÜCHE, SCHÄDEN ODER ANDERE HAFTUNGEN, OB IN EINER VERTRAGS- ODER DELIKTSKLAGE, DIE AUS ODER IN VERBINDUNG MIT DER SOFTWARE ODER DER VERWENDUNG ODER ANDEREN GESCHÄFTEN MIT DER SOFTWARE ENTSTEHEN.

Learn/collections

Sammlungen

Übersicht

Die Collection-Klasse in Flight ist ein praktisches Hilfsmittel zur Verwaltung von Datensätzen. Sie ermöglicht den Zugriff auf Daten sowohl mit Array- als auch mit Objektnotation, was deinen Code sauberer und flexibler macht.

Verständnis

Eine Collection ist im Grunde ein Wrapper um ein Array, aber mit einigen zusätzlichen Funktionen. Du kannst sie wie ein Array verwenden, darüber iterieren, die Anzahl der Elemente zählen und sogar auf Elemente zugreifen, als wären es Objekteigenschaften. Das ist besonders nützlich, wenn du strukturierte Daten in deiner App weitergeben möchtest oder wenn du deinen Code etwas lesbarer machen willst.

Collections implementieren mehrere PHP-Schnittstellen:

Grundlegende Verwendung

Eine Collection erstellen

Du kannst eine Collection erstellen, indem du einfach ein Array an den Konstruktor übergibst:

use flight\util\Collection;

$data = [
  'name' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$collection = new Collection($data);

Auf Elemente zugreifen

Du kannst auf Elemente entweder mit Array- oder Objektnotation zugreifen:

// Array-Notation
echo $collection['name']; // Ausgabe: FlightPHP

// Objekt-Notation
echo $collection->version; // Ausgabe: 3

Wenn du versuchst, auf einen Schlüssel zuzugreifen, der nicht existiert, erhältst du null anstatt eines Fehlers.

Elemente setzen

Du kannst Elemente ebenfalls mit beiden Notationen setzen:

// Array-Notation
$collection['author'] = 'Mike Cao';

// Objekt-Notation
$collection->license = 'MIT';

Vorhandensein prüfen und Elemente entfernen

Prüfen, ob ein Element existiert:

if (isset($collection['name'])) {
  // Etwas tun
}

if (isset($collection->version)) {
  // Etwas tun
}

Ein Element entfernen:

unset($collection['author']);
unset($collection->license);

Über eine Collection iterieren

Collections sind iterierbar, du kannst sie also in einer foreach-Schleife verwenden:

foreach ($collection as $key => $value) {
  echo "$key: $value\n";
}

Elemente zählen

Du kannst die Anzahl der Elemente in einer Collection zählen:

echo count($collection); // Ausgabe: 4

Alle Schlüssel oder Daten abrufen

Alle Schlüssel abrufen:

$keys = $collection->keys(); // ['name', 'version', 'features', 'license']

Alle Daten als Array abrufen:

$data = $collection->getData();

Collection leeren

Alle Elemente entfernen:

$collection->clear();

JSON-Serialisierung

Collections können einfach in JSON konvertiert werden:

echo json_encode($collection);
// Ausgabe: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}

Fortgeschrittene Verwendung

Du kannst das interne Datenarray bei Bedarf vollständig ersetzen:

$collection->setData(['foo' => 'bar']);

Collections sind besonders nützlich, wenn du strukturierte Daten zwischen Komponenten weitergeben möchtest oder wenn du eine objektorientiertere Schnittstelle für Array-Daten bereitstellen willst.

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/migrating_to_v3

Migration zu v3

Die Abwärtskompatibilität wurde größtenteils beibehalten, aber es gibt einige Änderungen, die Sie beachten sollten, wenn Sie von v2 zu v3 migrieren. Es gibt einige Änderungen, die zu sehr mit Designmustern kollidiert sind, sodass Anpassungen vorgenommen werden mussten.

Verhalten des Output Buffering

v3.5.0

Output buffering ist der Prozess, bei dem die Ausgabe, die von einem PHP-Skript generiert wird, in einem Puffer (intern in PHP) gespeichert wird, bevor sie an den Client gesendet wird. Dies ermöglicht es Ihnen, die Ausgabe zu modifizieren, bevor sie an den Client gesendet wird.

In einer MVC-Anwendung ist der Controller der "Manager" und er verwaltet, was die View tut. Ausgaben, die außerhalb des Controllers generiert werden (oder im Fall von Flight manchmal eine anonyme Funktion), brechen das MVC-Muster. Diese Änderung dient dazu, mehr im Einklang mit dem MVC-Muster zu sein und das Framework vorhersehbarer und einfacher zu bedienen zu machen.

In v2 wurde das Output Buffering so gehandhabt, dass es seinen eigenen Output-Puffer nicht konsistent schloss, was Unit-Tests und Streaming schwieriger machte. Für die Mehrheit der Nutzer könnte diese Änderung Sie tatsächlich nicht beeinflussen. Wenn Sie jedoch Inhalte außerhalb von Callables und Controllern ausgeben (z. B. in einem Hook), stoßen Sie wahrscheinlich auf Probleme. Das Ausgeben von Inhalten in Hooks und vor der tatsächlichen Ausführung des Frameworks hat in der Vergangenheit möglicherweise funktioniert, wird aber künftig nicht mehr funktionieren.

Wo Sie Probleme haben könnten

// index.php
require 'vendor/autoload.php';

// nur ein Beispiel
define('START_TIME', microtime(true));

function hello() {
    echo 'Hello World';
}

Flight::map('hello', 'hello');
Flight::after('hello', function(){
    // das wird tatsächlich in Ordnung sein
    echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});

Flight::before('start', function(){
    // Dinge wie das werden einen Fehler verursachen
    echo '<html><head><title>My Page</title></head><body>';
});

Flight::route('/', function(){
    // das ist tatsächlich in Ordnung
    echo 'Hello World';

    // Das sollte auch in Ordnung sein
    Flight::hello();
});

Flight::after('start', function(){
    // das wird einen Fehler verursachen
    echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});

Aktivieren des v2-Rendering-Verhaltens

Können Sie Ihren alten Code so lassen, wie er ist, ohne eine Umstellung durchzuführen, um ihn mit v3 kompatibel zu machen? Ja, das können Sie! Sie können das v2-Rendering-Verhalten aktivieren, indem Sie die Konfigurationsoption flight.v2.output_buffering auf true setzen. Dies ermöglicht es Ihnen, das alte Rendering-Verhalten weiterhin zu verwenden, aber es wird empfohlen, es künftig zu beheben. In v4 des Frameworks wird dies entfernt werden.

// index.php
require 'vendor/autoload.php';

Flight::set('flight.v2.output_buffering', true);

Flight::before('start', function(){
    // Jetzt wird das in Ordnung sein
    echo '<html><head><title>My Page</title></head><body>';
});

// mehr Code 

Änderungen am Dispatcher

v3.7.0

Wenn Sie statische Methoden für Dispatcher direkt aufgerufen haben, wie z. B. Dispatcher::invokeMethod(), Dispatcher::execute() usw., müssen Sie Ihren Code aktualisieren, um diese Methoden nicht mehr direkt aufzurufen. Dispatcher wurde zu einem objektorientierteren Ansatz umgewandelt, damit Dependency Injection Container einfacher verwendet werden können. Wenn Sie eine Methode ähnlich wie der Dispatcher aufrufen müssen, können Sie manuell etwas wie $result = $class->$method(...$params); oder call_user_func_array() verwenden.

Änderungen an halt() stop() redirect() und error()

v3.10.0

Das Standardverhalten vor 3.10.0 war, sowohl die Header als auch den Response-Body zu löschen. Dies wurde geändert, sodass nur noch der Response-Body gelöscht wird. Wenn Sie auch die Header löschen müssen, können Sie Flight::response()->clear() verwenden.

Learn/requests

Requests

Übersicht

Flight kapselt die HTTP-Anfrage in ein einzelnes Objekt, das wie folgt zugänglich ist:

$request = Flight::request();

Verständnis

HTTP-Anfragen sind eines der Kernaspekte, die man über den HTTP-Lebenszyklus verstehen muss. Ein Benutzer führt eine Aktion in einem Webbrowser oder einem HTTP-Client aus, und sie senden eine Reihe von Headern, Body, URL usw. an Ihr Projekt. Sie können diese Header (die Sprache des Browsers, welche Art von Kompression sie handhaben können, den User-Agent usw.) erfassen und den Body sowie die URL, die an Ihre Flight-Anwendung gesendet wird, erfassen. Diese Anfragen sind essenziell, damit Ihre App versteht, was als Nächstes zu tun ist.

Grundlegende Verwendung

PHP hat mehrere Super-Globalen, einschließlich $_GET, $_POST, $_REQUEST, $_SERVER, $_FILES und $_COOKIE. Flight abstrahiert diese in handliche Collections. Sie können die Eigenschaften query, data, cookies und files als Arrays oder Objekte zugreifen.

Hinweis: Es wird STRONGLICH davon abgeraten, diese Super-Globalen in Ihrem Projekt zu verwenden, und sie sollten über das request()-Objekt referenziert werden.

Hinweis: Es gibt keine Abstraktion für $_ENV verfügbar.

$_GET

Sie können das $_GET-Array über die Eigenschaft query zugreifen:

// GET /search?keyword=something
Flight::route('/search', function(){
    $keyword = Flight::request()->query['keyword'];
    // oder
    $keyword = Flight::request()->query->keyword;
    echo "You are searching for: $keyword";
    // query a database or something else with the $keyword
});

$_POST

Sie können das $_POST-Array über die Eigenschaft data zugreifen:

Flight::route('POST /submit', function(){
    $name = Flight::request()->data['name'];
    $email = Flight::request()->data['email'];
    // oder
    $name = Flight::request()->data->name;
    $email = Flight::request()->data->email;
    echo "You submitted: $name, $email";
    // save to a database or something else with the $name and $email
});

$_COOKIE

Sie können das $_COOKIE-Array über die Eigenschaft cookies zugreifen:

Flight::route('GET /login', function(){
    $savedLogin = Flight::request()->cookies['myLoginCookie'];
    // oder
    $savedLogin = Flight::request()->cookies->myLoginCookie;
    // check if it's really saved or not and if it is auto log them in
    if($savedLogin) {
        Flight::redirect('/dashboard');
        return;
    }
});

Für Hilfe beim Setzen neuer Cookie-Werte siehe overclokk/cookie

$_SERVER

Es gibt einen Shortcut, um das $_SERVER-Array über die Methode getVar() zugreifen:


$host = Flight::request()->getVar('HTTP_HOST');

$_FILES

Sie können hochgeladene Dateien über die Eigenschaft files zugreifen:

// raw access to $_FILES property. See below for recommended approach
$uploadedFile = Flight::request()->files['myFile']; 
// oder
$uploadedFile = Flight::request()->files->myFile;

Siehe Uploaded File Handler für mehr Infos.

Verarbeiten von Datei-Uploads

v3.12.0

Sie können Datei-Uploads mit dem Framework mithilfe einiger Hilfsmethoden verarbeiten. Es kommt im Wesentlichen darauf an, die Dateidaten aus der Anfrage zu ziehen und sie an einen neuen Ort zu verschieben.

Flight::route('POST /upload', function(){
    // If you had an input field like <input type="file" name="myFile">
    $uploadedFileData = Flight::request()->getUploadedFiles();
    $uploadedFile = $uploadedFileData['myFile'];
    $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});

Wenn Sie mehrere Dateien hochgeladen haben, können Sie durch sie iterieren:

Flight::route('POST /upload', function(){
    // If you had an input field like <input type="file" name="myFiles[]">
    $uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
    foreach ($uploadedFiles as $uploadedFile) {
        $uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
    }
});

Sicherheitshinweis: Validieren und sanitieren Sie immer Benutzereingaben, insbesondere bei Datei-Uploads. Validieren Sie immer den Typ der Erweiterungen, die Sie zum Hochladen erlauben, aber Sie sollten auch die "Magic Bytes" der Datei validieren, um sicherzustellen, dass es tatsächlich der Typ der Datei ist, den der Benutzer angibt. Es gibt Artikel und Bibliotheken, die dabei helfen.

Request Body

Um den rohen HTTP-Request-Body zu erhalten, z. B. bei POST/PUT-Anfragen, können Sie Folgendes tun:

Flight::route('POST /users/xml', function(){
    $xmlBody = Flight::request()->getBody();
    // do something with the XML that was sent.
});

JSON Body

Wenn Sie eine Anfrage mit dem Content-Type application/json und den Beispieldaten {"id": 123} erhalten, ist sie über die Eigenschaft data verfügbar:

$id = Flight::request()->data->id;

Request Headers

Sie können Request-Header mit der Methode getHeader() oder getHeaders() zugreifen:


// Maybe you need Authorization header
$host = Flight::request()->getHeader('Authorization');
// oder
$host = Flight::request()->header('Authorization');

// If you need to grab all headers
$headers = Flight::request()->getHeaders();
// oder
$headers = Flight::request()->headers();

Request Method

Sie können die Request-Methode über die Eigenschaft method oder die Methode getMethod() zugreifen:

$method = Flight::request()->method; // actually populated by getMethod()
$method = Flight::request()->getMethod();

Hinweis: Die Methode getMethod() zieht zunächst die Methode aus $_SERVER['REQUEST_METHOD'], dann kann sie durch $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] überschrieben werden, falls vorhanden, oder $_REQUEST['_method'], falls vorhanden.

Eigenschaften des Request-Objekts

Das Request-Objekt stellt die folgenden Eigenschaften bereit:

Hilfsmethoden

Es gibt ein paar Hilfsmethoden, um Teile einer URL zusammenzusetzen oder mit bestimmten Headern umzugehen.

Volle URL

Sie können die volle Request-URL mit der Methode getFullUrl() zugreifen:

$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar

Basis-URL

Sie können die Basis-URL mit der Methode getBaseUrl() zugreifen:

// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// Notice, no trailing slash.

Query-Parsing

Sie können eine URL an die Methode parseQuery() übergeben, um den Query-String in ein assoziatives Array zu parsen:

$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']

Verhandeln von Content-Accept-Types

v3.17.2

Sie können die Methode negotiateContentType() verwenden, um den besten Content-Type für die Antwort basierend auf dem vom Client gesendeten Accept-Header zu bestimmen.


// Example Accept header: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// The below defines what you support.
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
    // Serve JSON response
} elseif ($typeToServe === 'application/xml') {
    // Serve XML response
} else {
    // Default to something else or throw an error
}

Hinweis: Wenn keiner der verfügbaren Typen im Accept-Header gefunden wird, gibt die Methode null zurück. Wenn kein Accept-Header definiert ist, gibt die Methode den ersten Typ im $availableTypes-Array zurück.

Siehe auch

Fehlerbehebung

Changelog

Learn/learn

Lerne Flight kennen

Flight ist ein schnelles, einfaches, erweiterbares Framework für PHP. Es ist sehr vielseitig und kann zum Erstellen jeder Art von Webanwendung verwendet werden. Es ist auf Einfachheit ausgelegt und so geschrieben, dass es leicht zu verstehen und zu verwenden ist – von Menschen und von KI-Programmierassistenten.

Hinweis: Du wirst Beispiele sehen, die Flight:: als statische Variable verwenden, und einige, die das $app-> Engine-Objekt verwenden. Beide funktionieren austauschbar. $app und $this->app in einem Controller/Middleware sind der empfohlene Ansatz des Flight-Teams (und worauf das offizielle Skelett + AGENTS.md für neue Projekte standardisieren).

Kernkomponenten

Routing

Lerne, wie du Routen für deine Webanwendung verwaltest. Dies beinhaltet auch das Gruppieren von Routen, Routenparameter und Middleware.

Middleware

Lerne, wie du Middleware verwendest, um Anfragen und Antworten in deiner Anwendung zu filtern.

Autoloading

Lerne, wie du deine eigenen Klassen automatisch lädst. Die Groß-/Kleinschreibung der Ordner muss mit deinen Namespaces übereinstimmen – das Skelett verwendet App\ und PascalCase-Ordner wie app/Controller/.

Requests

Lerne, wie du Anfragen und Antworten in deiner Anwendung behandelst.

Responses

Lerne, wie du Antworten an deine Benutzer sendest.

HTML-Templates

Lerne, wie du HTML mit Twig (Skelett-Standard), Latte oder anderen Engines renderst – nicht nur mit den eingebauten PHP-Views.

Sicherheit

Lerne, wie du deine Anwendung vor häufigen Sicherheitsbedrohungen schützt.

Konfiguration

Lerne, wie du das Framework für deine Anwendung konfigurierst.

Event Manager

Lerne, wie du das Event-System verwendest, um deiner Anwendung benutzerdefinierte Events hinzuzufügen.

Flight erweitern

Lerne, wie du das Framework erweitern kannst, indem du deine eigenen Methoden und Klassen hinzufügst.

Methoden-Hooks und Filtern

Lerne, wie du Event-Hooks zu deinen Methoden und internen Framework-Methoden hinzufügst.

Dependency-Injection-Container (DIC)

Lerne, wie du Dependency-Injection-Container (DIC) verwendest, um die Abhängigkeiten deiner Anwendung zu verwalten.

Utility-Klassen

Collections

Collections werden verwendet, um Daten zu speichern und sie zur einfacheren Nutzung als Array oder als Objekt zugänglich zu machen.

JSON-Wrapper

Diese Klasse bietet ein paar einfache Funktionen, um das Kodieren und Dekodieren deines JSON konsistent zu gestalten.

SimplePdo

PDO kann manchmal mehr Kopfschmerzen bereiten als nötig. SimplePdo ist eine moderne PDO-Hilfsklasse mit praktischen Methoden wie insert(), update(), delete() und transaction(), die Datenbankoperationen viel einfacher machen.

PdoWrapper (Veraltet)

Der ursprüngliche PDO-Wrapper ist seit v3.18.0 veraltet. Bitte verwende stattdessen SimplePdo.

Uploaded Datei-Handler

Eine einfache Klasse, die hilft, hochgeladene Dateien zu verwalten und an einen dauerhaften Ort zu verschieben.

Wichtige Konzepte

Warum ein Framework?

Hier ist ein kurzer Artikel darüber, warum du ein Framework verwenden solltest. Es ist eine gute Idee, die Vorteile der Verwendung eines Frameworks zu verstehen, bevor du eines verwendest.

Zusätzlich wurde ein exzellentes Tutorial von @lubiana erstellt. Auch wenn es nicht im Detail auf Flight eingeht, hilft dir dieser Leitfaden, einige der wichtigsten Konzepte rund um ein Framework zu verstehen und warum sie vorteilhaft sind. Du findest das Tutorial hier.

Flight im Vergleich zu anderen Frameworks

Wenn du von einem anderen Framework wie Laravel, Slim, Fat-Free oder Symfony zu Flight migrierst, hilft dir diese Seite, die Unterschiede zwischen den beiden zu verstehen.

Weitere Themen

Unit Testing

Folge dieser Anleitung, um zu lernen, wie du deinen Flight-Code Unit-testest, damit er felsenfest ist.

KI & Entwicklererlebnis

Flight ist dafür gebaut, mit Code-LLMs zusammenzuarbeiten: AGENTS.md, Runway ai:*-Befehle und ein klares Skelett-Layout, damit Agenten dem Muster folgen.

Migration v2 -> v3

Die Abwärtskompatibilität wurde größtenteils beibehalten, aber es gibt einige Änderungen, die du beachten solltest, wenn du von v2 auf v3 migrierst.

Learn/flight_vs_fat_free

Flight vs. Fat-Free

Was ist Fat-Free?

Fat-Free (liebevoll F3 genannt) ist ein leistungsstarkes und dennoch einfach zu verwendendes PHP-Mikro-Framework, das dir hilft, dynamische und robuste Webanwendungen schnell zu erstellen!

Flight lässt sich in vielerlei Hinsicht mit Fat-Free vergleichen und ist wahrscheinlich der engste Verwandte in Bezug auf Funktionen und Einfachheit. Fat-Free hat viele Funktionen, die Flight nicht hat, aber es hat auch viele Funktionen, die Flight hat. Fat-Free beginnt, sein Alter zu zeigen und ist nicht mehr so beliebt wie früher.

Updates werden seltener und die Community ist nicht mehr so aktiv wie früher. Der Code ist einfach genug, aber manchmal kann der Mangel an Syntax-Disziplin das Lesen und Verstehen erschweren. Es funktioniert mit PHP 8.3, aber der Code selbst sieht immer noch aus, als stamme er aus PHP 5.3.

Vorteile im Vergleich zu Flight

Nachteile im Vergleich zu Flight

Learn/routing

Routing

Überblick

Routing in Flight PHP ordnet URL-Muster Callback-Funktionen oder Klassenmethoden zu und ermöglicht so eine schnelle und einfache Anfrageverarbeitung. Es ist auf minimalen Overhead ausgelegt, anfängerfreundlich und ohne externe Abhängigkeiten erweiterbar.

Verständnis

Routing ist der Kernmechanismus, der HTTP-Anfragen mit deiner Anwendungslogik in Flight verbindet. Durch die Definition von Routen legst du fest, wie verschiedene URLs bestimmten Code auslösen – sei es durch Funktionen, Klassenmethoden oder Controller-Aktionen. Das Routing-System von Flight ist flexibel und unterstützt grundlegende Muster, benannte Parameter, reguläre Ausdrücke sowie erweiterte Funktionen wie Dependency Injection und ressourcenorientiertes Routing. Dieser Ansatz hält deinen Code organisiert und wartungsfreundlich, bleibt dabei schnell und einfach für Anfänger und ist für fortgeschrittene Benutzer erweiterbar.

Hinweis: Du möchtest mehr über Routing verstehen? Schau dir die Seite "Warum ein Framework?" für eine ausführlichere Erklärung an.

Grundlegende Verwendung

Eine einfache Route definieren

Grundlegendes Routing in Flight erfolgt durch das Abgleichen eines URL-Musters mit einer Callback-Funktion oder einem Array aus Klasse und Methode.

Flight::route('/', function(){
    echo 'hello world!';
});

Routen werden in der Reihenfolge abgeglichen, in der sie definiert sind. Die erste Route, die eine Anfrage matcht, wird aufgerufen.

Funktionen als Callbacks verwenden

Der Callback kann jedes aufrufbare Objekt sein. Du kannst also eine normale Funktion verwenden:

function hello() {
    echo 'hello world!';
}

Flight::route('/', 'hello');

Klassen und Methoden als Controller verwenden

Du kannst auch eine Methode (statisch oder nicht statisch) einer Klasse verwenden:

class GreetingController {
    public function hello() {
        echo 'hello world!';
    }
}

Flight::route('/', [ 'GreetingController','hello' ]);
// oder
Flight::route('/', [ GreetingController::class, 'hello' ]); // bevorzugte Methode
// oder
Flight::route('/', [ 'GreetingController::hello' ]);
// oder 
Flight::route('/', [ 'GreetingController->hello' ]);

Oder indem du zuerst ein Objekt erstellst und dann die Methode aufrufst:

use flight\Engine;

// GreetingController.php
class GreetingController
{
    protected Engine $app
    public function __construct(Engine $app) {
        $this->app = $app;
        $this->name = 'John Doe';
    }

    public function hello() {
        echo "Hello, {$this->name}!";
    }
}

// index.php
$app = Flight::app();
$greeting = new GreetingController($app);

Flight::route('/', [ $greeting, 'hello' ]);

Hinweis: Standardmäßig wird die Klasse flight\Engine immer injiziert, wenn ein Controller innerhalb des Frameworks aufgerufen wird, es sei denn, du legst dies über einen Dependency-Injection-Container fest.

Methodenspezifisches Routing

Standardmäßig werden Routenmuster gegen alle Anfragemethoden abgeglichen. Du kannst auf bestimmte Methoden reagieren, indem du einen Bezeichner vor die URL setzt.

Flight::route('GET /', function () {
  echo 'Ich habe eine GET-Anfrage erhalten.';
});

Flight::route('POST /', function () {
  echo 'Ich habe eine POST-Anfrage erhalten.';
});

// Du kannst Flight::get() nicht für Routen verwenden, da dies eine Methode
//    zum Abrufen von Variablen ist, nicht zum Erstellen einer Route.
Flight::post('/', function() { /* Code */ });
Flight::patch('/', function() { /* Code */ });
Flight::put('/', function() { /* Code */ });
Flight::delete('/', function() { /* Code */ });

Du kannst auch mehrere Methoden auf einen einzelnen Callback abbilden, indem du ein |-Trennzeichen verwendest:

Flight::route('GET|POST /', function () {
  echo 'Ich habe entweder eine GET- oder eine POST-Anfrage erhalten.';
});

Spezielle Behandlung für HEAD- und OPTIONS-Anfragen

Flight bietet eine integrierte Behandlung für HEAD- und OPTIONS-HTTP-Anfragen:

HEAD-Anfragen

Flight::route('GET /info', function() {
    echo 'Dies ist eine Information!';
});
// Eine HEAD-Anfrage an /info gibt dieselben Header zurück, aber keinen Body.

OPTIONS-Anfragen

OPTIONS-Anfragen werden von Flight automatisch für jede definierte Route behandelt.

// Für eine Route, die definiert ist als:
Flight::route('GET|POST /users', function() { /* ... */ });

// Eine OPTIONS-Anfrage an /users antwortet mit:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS

Das Router-Objekt verwenden

Zusätzlich kannst du das Router-Objekt abrufen, das einige Hilfsmethoden für dich bereithält:


$router = Flight::router();

// bildet alle Methoden ab, genau wie Flight::route()
$router->map('/', function() {
    echo 'hello world!';
});

// GET-Anfrage
$router->get('/users', function() {
    echo 'users';
});
$router->post('/users',             function() { /* Code */});
$router->put('/users/update/@id',   function() { /* Code */});
$router->delete('/users/@id',       function() { /* Code */});
$router->patch('/users/@id',        function() { /* Code */});

Reguläre Ausdrücke (Regex)

Du kannst reguläre Ausdrücke in deinen Routen verwenden:

Flight::route('/user/[0-9]+', function () {
  // Dies matcht /user/1234
});

Obwohl diese Methode verfügbar ist, wird empfohlen, benannte Parameter oder benannte Parameter mit regulären Ausdrücken zu verwenden, da sie lesbarer und leichter zu warten sind.

Benannte Parameter

Du kannst benannte Parameter in deinen Routen angeben, die an deine Callback-Funktion übergeben werden. Dies dient eher der Lesbarkeit der Route als allem anderen. Bitte beachte den wichtigen Hinweis im folgenden Abschnitt.

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "hello, $name ($id)!";
});

Du kannst auch reguläre Ausdrücke mit deinen benannten Parametern einbinden, indem du das :-Trennzeichen verwendest:

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // Dies matcht /bob/123
  // Aber nicht /bob/12345
});

Hinweis: Das Abgleichen von Regex-Gruppen () mit Positionsparametern wird nicht unterstützt. Beispiel: :'\(

Wichtiger Hinweis

Obwohl es im obigen Beispiel so aussieht, als ob @name direkt mit der Variable $name verbunden ist, ist das nicht der Fall. Die Reihenfolge der Parameter in der Callback-Funktion bestimmt, was an sie übergeben wird. Wenn du die Reihenfolge der Parameter in der Callback-Funktion vertauschst, werden auch die Variablen vertauscht. Hier ist ein Beispiel:

Flight::route('/@name/@id', function (string $id, string $name) {
  echo "hello, $name ($id)!";
});

Und wenn du die folgende URL aufrufst: /bob/123, wäre die Ausgabe hello, 123 (bob)!. Sei vorsichtig, wenn du deine Routen und Callback-Funktionen einrichtest!

Optionale Parameter

Du kannst benannte Parameter angeben, die für den Abgleich optional sind, indem du Segmente in Klammern setzt.

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // Dies matcht die folgenden URLs:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

Alle optionalen Parameter, die nicht gematcht werden, werden als NULL übergeben.

Wildcard-Routing

Der Abgleich erfolgt nur auf einzelnen URL-Segmenten. Wenn du mehrere Segmente abgleichen möchtest, kannst du den *-Platzhalter verwenden.

Flight::route('/blog/*', function () {
  // Dies matcht /blog/2000/02/01
});

Um alle Anfragen an einen einzelnen Callback weiterzuleiten, kannst du Folgendes tun:

Flight::route('*', function () {
  // Etwas tun
});

404 Not Found Handler

Standardmäßig sendet Flight eine sehr einfache und schlichte HTTP 404 Not Found-Antwort, wenn eine URL nicht gefunden werden kann. Wenn du eine individuellere 404-Antwort wünschst, kannst du deine eigene notFound-Methode mappen:

Flight::map('notFound', function() {
    $url = Flight::request()->url;

    // Du könntest auch Flight::render() mit einer eigenen Vorlage verwenden.
    $output = <<<HTML
        <h1>Mein benutzerdefinierter 404 Not Found</h1>
        <h3>Die von dir angeforderte Seite {$url} konnte nicht gefunden werden.</h3>
        HTML;

    $this->response()
        ->clearBody()
        ->status(404)
        ->write($output)
        ->send();
});

Method Not Found Handler

Standardmäßig sendet Flight eine sehr einfache und schlichte HTTP 405 Method Not Allowed-Antwort (z. B. Method Not Allowed. Zulässige Methoden sind: GET, POST), wenn eine URL gefunden wird, aber die Methode nicht erlaubt ist. Es wird auch ein Allow-Header mit den zulässigen Methoden für diese URL mitgesendet.

Wenn du eine individuellere 405-Antwort wünschst, kannst du deine eigene methodNotFound-Methode mappen:

use flight\net\Route;

Flight::map('methodNotFound', function(Route $route) {
    $url = Flight::request()->url;
    $methods = implode(', ', $route->methods);

    // Du könntest auch Flight::render() mit einer eigenen Vorlage verwenden.
    $output = <<<HTML
        <h1>Mein benutzerdefinierter 405 Method Not Allowed</h1>
        <h3>Die von dir angeforderte Methode für {$url} ist nicht erlaubt.</h3>
        <p>Zulässige Methoden sind: {$methods}</p>
        HTML;

    $this->response()
        ->clearBody()
        ->status(405)
        ->setHeader('Allow', $methods)
        ->write($output)
        ->send();
});

Fortgeschrittene Verwendung

Dependency Injection in Routen

Wenn du Dependency Injection über einen Container (PSR-11, PHP-DI, Dice, etc.) verwenden möchtest, sind die einzigen Routentypen, bei denen das verfügbar ist, entweder das direkte Erstellen des Objekts selbst und die Verwendung des Containers zum Erstellen deines Objekts, oder du kannst Strings verwenden, um die Klasse und Methode zu definieren, die aufgerufen werden sollen. Weitere Informationen findest du auf der Seite Dependency Injection.

Hier ist ein kurzes Beispiel:


use flight\database\SimplePdo;

// Greeting.php
class Greeting
{
    protected SimplePdo $db;
    public function __construct(SimplePdo $db) {
        $this->db = $db;
    }

    public function hello(int $id) {
        // etwas mit $this->db tun
        $name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hallo, Welt! Mein Name ist {$name}!";
    }
}

// index.php

// Richte den Container mit den benötigten Parametern ein
// Weitere Informationen zu PSR-11 findest du auf der Dependency-Injection-Seite
$dice = new \Dice\Dice();

// Vergiss nicht, die Variable mit '$dice = ' neu zuzuweisen!!!!!
$dice = $dice->addRule(SimplePdo::class, [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// Registriere den Container-Handler
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// Routen wie gewohnt
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// oder
Flight::route('/hello/@id', 'Greeting->hello');
// oder
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

Ausführung an die nächste Route übergeben

Veraltet Du kannst die Ausführung an die nächste passende Route übergeben, indem du true aus deiner Callback-Funktion zurückgibst.

Flight::route('/user/@name', function (string $name) {
  // Eine Bedingung prüfen
  if ($name !== "Bob") {
    // Mit der nächsten Route fortfahren
    return true;
  }
});

Flight::route('/user/*', function () {
  // Dies wird aufgerufen
});

Es wird nun empfohlen, Middleware zu verwenden, um komplexe Anwendungsfälle wie diesen zu behandeln.

Route-Aliase

Durch die Vergabe eines Alias für eine Route kannst du diesen Alias später in deiner App dynamisch aufrufen, um ihn im Code generieren zu lassen (z. B. einen Link in einem HTML-Template oder die Erstellung einer Redirect-URL).

Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// oder 
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');

// später irgendwo im Code
class UserController {
    public function update() {

        // Code zum Speichern des Benutzers...
        $id = $user['id']; // z. B. 5

        $redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // gibt '/users/5' zurück
        Flight::redirect($redirectUrl);
    }
}

Dies ist besonders hilfreich, wenn sich deine URL ändert. Im obigen Beispiel nehmen wir an, dass users zu /admin/users/@id verschoben wurde. Dank des Alias für die Route musst du nicht mehr alle alten URLs in deinem Code suchen und ändern, da der Alias nun /admin/users/5 zurückgibt, wie im obigen Beispiel.

Route-Aliase funktionieren auch in Gruppen:

Flight::group('/users', function() {
    Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
    // oder
    Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});

Routeninformationen untersuchen

Wenn du die Informationen der passenden Route untersuchen möchtest, gibt es zwei Möglichkeiten:

  1. Du kannst die Eigenschaft executedRoute auf dem Objekt Flight::router() verwenden.
  2. Du kannst darum bitten, dass das Routenobjekt an deinen Callback übergeben wird, indem du true als dritten Parameter in der Routenmethode übergibst. Das Routenobjekt ist immer der letzte Parameter, der an deine Callback-Funktion übergeben wird.

executedRoute

Flight::route('/', function() {
  $route = Flight::router()->executedRoute;
  // Etwas mit $route tun
  // Array der abgeglichenen HTTP-Methoden
  $route->methods;

  // Array der benannten Parameter
  $route->params;

  // Passender regulärer Ausdruck
  $route->regex;

  // Enthält den Inhalt aller '*' im URL-Muster
  $route->splat;

  // Zeigt den URL-Pfad....falls du ihn wirklich brauchst
  $route->pattern;

  // Zeigt, welche Middleware dieser Route zugewiesen ist
  $route->middleware;

  // Zeigt den Alias, der dieser Route zugewiesen ist
  $route->alias;
});

Hinweis: Die Eigenschaft executedRoute wird nur gesetzt, nachdem eine Route ausgeführt wurde. Wenn du versuchst, darauf vor der Ausführung einer Route zuzugreifen, ist sie NULL. Du kannst executedRoute auch in Middleware verwenden!

true an die Routendefinition übergeben

Flight::route('/', function(\flight\net\Route $route) {
  // Array der abgeglichenen HTTP-Methoden
  $route->methods;

  // Array der benannten Parameter
  $route->params;

  // Passender regulärer Ausdruck
  $route->regex;

  // Enthält den Inhalt aller '*' im URL-Muster
  $route->splat;

  // Zeigt den URL-Pfad....falls du ihn wirklich brauchst
  $route->pattern;

  // Zeigt, welche Middleware dieser Route zugewiesen ist
  $route->middleware;

  // Zeigt den Alias, der dieser Route zugewiesen ist
  $route->alias;
}, true);// <-- Dieser true-Parameter bewirkt das

Routen-Gruppierung und Middleware

Es kann Zeiten geben, in denen du zusammengehörige Routen gruppieren möchtest (z. B. /api/v1). Du kannst dies mit der group-Methode tun:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Matcht /api/v1/users
  });

  Flight::route('/posts', function () {
    // Matcht /api/v1/posts
  });
});

Du kannst sogar Gruppen von Gruppen verschachteln:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() ruft Variablen ab, es setzt keine Route! Siehe Objektkontext unten
    Flight::route('GET /users', function () {
      // Matcht GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // Matcht POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // Matcht PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() ruft Variablen ab, es setzt keine Route! Siehe Objektkontext unten
    Flight::route('GET /users', function () {
      // Matcht GET /api/v2/users
    });
  });
});

Gruppierung mit Objektkontext

Du kannst die Routengruppierung weiterhin mit dem Engine-Objekt auf folgende Weise verwenden:

$app = Flight::app();

$app->group('/api/v1', function (Router $router) {

  // verwende die Variable $router
  $router->get('/users', function () {
    // Matcht GET /api/v1/users
  });

  $router->post('/posts', function () {
    // Matcht POST /api/v1/posts
  });
});

Hinweis: Dies ist die bevorzugte Methode zum Definieren von Routen und Gruppen mit dem $router-Objekt.

Gruppierung mit Middleware

Du kannst einer Gruppe von Routen auch Middleware zuweisen:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Matcht /api/v1/users
  });
}, [ MyAuthMiddleware::class ]); // oder [ new MyAuthMiddleware() ], wenn du eine Instanz verwenden möchtest

Weitere Details findest du auf der Seite Gruppen-Middleware.

Ressourcen-Routing

Du kannst mit der resource-Methode eine Reihe von Routen für eine Ressource erstellen. Dadurch wird eine Reihe von Routen für eine Ressource erstellt, die den RESTful-Konventionen folgt.

Um eine Ressource zu erstellen, gehe wie folgt vor:

Flight::resource('/users', UsersController::class);

Im Hintergrund werden die folgenden Routen erstellt:

[
      'index' => 'GET /users',
      'create' => 'GET /users/create',
      'store' => 'POST /users',
      'show' => 'GET /users/@id',
      'edit' => 'GET /users/@id/edit',
      'update' => 'PUT /users/@id',
      'destroy' => 'DELETE /users/@id'
]

Und dein Controller verwendet die folgenden Methoden:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

Hinweis: Du kannst die neu hinzugefügten Routen mit runway anzeigen, indem du php runway routes ausführst.

Ressourcen-Routen anpassen

Es gibt einige Optionen, um die Ressourcen-Routen zu konfigurieren.

Alias-Basis

Du kannst die aliasBase konfigurieren. Standardmäßig ist der Alias der letzte Teil der angegebenen URL. Zum Beispiel würde /users/ zu einer aliasBase von users führen. Wenn diese Routen erstellt werden, sind die Aliase users.index, users.create usw. Wenn du den Alias ändern möchtest, setze die aliasBase auf den gewünschten Wert.

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only und Except

Du kannst auch festlegen, welche Routen du erstellen möchtest, indem du die Optionen only und except verwendest.

// Whitelist nur diese Methoden und Blacklist den Rest
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Blacklist nur diese Methoden und Whitelist den Rest
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

Dies sind im Grunde Whitelist- und Blacklist-Optionen, mit denen du festlegen kannst, welche Routen du erstellen möchtest.

Middleware

Du kannst auch Middleware angeben, die für jede der durch die resource-Methode erstellten Routen ausgeführt werden soll.

Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);

Streaming-Antworten

Du kannst jetzt Antworten an den Client mit stream() oder streamWithHeaders() streamen. Dies ist nützlich für das Senden großer Dateien, langlaufender Prozesse oder das Erzeugen großer Antworten. Das Streaming einer Route wird etwas anders behandelt als eine normale Route.

Hinweis: Streaming-Antworten sind nur verfügbar, wenn flight.v2.output_buffering auf false gesetzt ist.

Streamen mit manuellen Headern

Du kannst eine Antwort an den Client streamen, indem du die stream()-Methode auf einer Route verwendest. Wenn du dies tust, musst du alle Header von Hand setzen, bevor du irgendetwas an den Client ausgibst. Dies geschieht mit der PHP-Funktion header() oder der Methode Flight::response()->setRealHeader().

Flight::route('/@filename', function($filename) {

    $response = Flight::response();

    // Offensichtlich würdest du den Pfad bereinigen und so weiter.
    $fileNameSafe = basename($filename);

    // Wenn du nach der Ausführung der Route zusätzliche Header setzen möchtest,
    // musst du sie definieren, bevor irgendetwas ausgegeben wird.
    // Sie müssen alle ein roher Aufruf der Funktion header() oder
    // ein Aufruf von Flight::response()->setRealHeader() sein.
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // oder
    $response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');

    $filePath = '/some/path/to/files/'.$fileNameSafe;

    if (!is_readable($filePath)) {
        Flight::halt(404, 'Datei nicht gefunden');
    }

    // setze die Content-Length manuell, wenn du möchtest
    header('Content-Length: '.filesize($filePath));
    // oder
    $response->setRealHeader('Content-Length: '.filesize($filePath));

    // Stream die Datei an den Client, während sie gelesen wird
    readfile($filePath);

// Dies ist die magische Zeile hier
})->stream();

Streamen mit Headern

Du kannst auch die streamWithHeaders()-Methode verwenden, um die Header zu setzen, bevor du mit dem Streamen beginnst.

Flight::route('/stream-users', function() {

    // Du kannst hier alle zusätzlichen Header hinzufügen, die du möchtest
    // Du musst nur header() oder Flight::response()->setRealHeader() verwenden

    // Wie auch immer du deine Daten abrufst, nur als Beispiel...
    $users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");

    echo '{';
    $user_count = count($users);
    while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
        echo json_encode($user);
        if(--$user_count > 0) {
            echo ',';
        }

        // Dies ist erforderlich, um die Daten an den Client zu senden
        ob_flush();
    }
    echo '}';

// So setzt du die Header, bevor du mit dem Streamen beginnst.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // optionaler Statuscode, Standard ist 200
    'status' => 200
]);

Siehe auch

Fehlerbehebung

404 Not Found oder unerwartetes Routenverhalten

Wenn du einen 404 Not Found-Fehler siehst (obwohl du dir sicher bist, dass die Route wirklich existiert und es kein Tippfehler ist), könnte das tatsächlich ein Problem damit sein, dass du in deinem Routen-Endpunkt einen Wert zurückgibst, anstatt ihn nur auszugeben. Der Grund dafür ist beabsichtigt, kann aber einige Entwickler überraschen.

Flight::route('/hello', function(){
    // Dies könnte einen 404 Not Found-Fehler verursachen
    return 'Hello World';
});

// Was du wahrscheinlich willst
Flight::route('/hello', function(){
    echo 'Hello World';
});

Der Grund dafür ist ein spezieller Mechanismus im Router, der den Rückgabewert als Signal zum "Weitergehen zur nächsten Route" behandelt. Du kannst dieses Verhalten im Abschnitt Routing dokumentiert sehen.

Änderungsprotokoll

Learn/responses

Responses

Überblick

Flight hilft dabei, Teile der Response-Header für Sie zu generieren, aber Sie haben die meiste Kontrolle darüber, was Sie an den Benutzer zurücksenden. Meistens greifen Sie direkt auf das response()-Objekt zu, aber Flight bietet einige Hilfsmethoden, um einige der Response-Header für Sie zu setzen.

Verständnis

Nachdem der Benutzer seine request-Anfrage an Ihre Anwendung gesendet hat, müssen Sie eine angemessene Response für sie generieren. Sie haben Ihnen Informationen wie die bevorzugte Sprache, ob sie bestimmte Kompressionstypen handhaben können, ihren User Agent usw. gesendet, und nach der Verarbeitung von allem ist es Zeit, ihnen eine angemessene Response zurückzusenden. Dies kann das Setzen von Headern, das Ausgeben eines HTML- oder JSON-Bodys für sie oder das Weiterleiten zu einer Seite sein.

Grundlegende Verwendung

Senden eines Response-Bodys

Flight verwendet ob_start(), um die Ausgabe zu puffern. Das bedeutet, Sie können echo oder print verwenden, um eine Response an den Benutzer zu senden, und Flight wird sie erfassen und mit den entsprechenden Headern an den Benutzer zurücksenden.

// Dies sendet "Hello, World!" an den Browser des Benutzers
Flight::route('/', function() {
    echo "Hello, World!";
});

// HTTP/1.1 200 OK
// Content-Type: text/html
//
// Hello, World!

Als Alternative können Sie die write()-Methode aufrufen, um zum Body hinzuzufügen.

// Dies sendet "Hello, World!" an den Browser des Benutzers
Flight::route('/', function() {
    // ausführlich, aber erledigt den Job manchmal, wenn Sie es brauchen
    Flight::response()->write("Hello, World!");

    // wenn Sie den Body abrufen möchten, den Sie zu diesem Zeitpunkt gesetzt haben
    // können Sie das so tun
    $body = Flight::response()->getBody();
});

JSON

Flight bietet Unterstützung für das Senden von JSON- und JSONP-Responses. Um eine JSON-Response zu senden, geben Sie einige Daten weiter, die JSON-kodiert werden sollen:

Flight::route('/@companyId/users', function(int $companyId) {
    // holen Sie irgendwie Ihre Benutzer aus einer Datenbank, z.B.
    $users = Flight::db()->fetchAll("SELECT id, first_name, last_name FROM users WHERE company_id = ?", [ $companyId ]);

    Flight::json($users);
});
// [{"id":1,"first_name":"Bob","last_name":"Jones"}, /* mehr Benutzer */ ]

Hinweis: Standardmäßig sendet Flight einen Content-Type: application/json-Header mit der Response. Es verwendet auch die Flags JSON_THROW_ON_ERROR und JSON_UNESCAPED_SLASHES beim Kodieren des JSON.

JSON mit Statuscode

Sie können auch einen Statuscode als zweiten Argument übergeben:

Flight::json(['id' => 123], 201);

JSON mit Pretty Print

Sie können auch ein Argument an die letzte Position übergeben, um Pretty Printing zu aktivieren:

Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);

Ändern der JSON-Argument-Reihenfolge

Flight::json() ist eine sehr veraltete Methode, aber das Ziel von Flight ist es, die Abwärtskompatibilität für Projekte aufrechtzuerhalten. Es ist eigentlich sehr einfach, wenn Sie die Reihenfolge der Argumente neu gestalten möchten, um eine einfachere Syntax zu verwenden, können Sie die JSON-Methode einfach neu zuordnen wie jede andere Flight-Methode:

Flight::map('json', function($data, $code = 200, $options = 0) {

    // jetzt müssen Sie nicht mehr `true, 'utf-8'` verwenden, wenn Sie die json()-Methode nutzen!
    Flight::_json($data, $code, true, 'utf-8', $options);
}

// Und jetzt kann sie so verwendet werden
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);

JSON und Stoppen der Ausführung

v3.10.0

Wenn Sie eine JSON-Response senden und die Ausführung stoppen möchten, können Sie die jsonHalt()-Methode verwenden. Dies ist nützlich für Fälle, in denen Sie auf eine Art von Autorisierung prüfen und wenn der Benutzer nicht autorisiert ist, können Sie sofort eine JSON-Response senden, den bestehenden Body-Inhalt löschen und die Ausführung stoppen.

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Prüfen, ob der Benutzer autorisiert ist
    if($authorized === false) {
        Flight::jsonHalt(['error' => 'Unauthorized'], 401);
        // kein exit; hier benötigt.
    }

    // Mit dem Rest der Route fortfahren
});

Vor v3.10.0 hätten Sie etwas wie das tun müssen:

Flight::route('/users', function() {
    $authorized = someAuthorizationCheck();
    // Prüfen, ob der Benutzer autorisiert ist
    if($authorized === false) {
        Flight::halt(401, json_encode(['error' => 'Unauthorized']));
    }

    // Mit dem Rest der Route fortfahren
});

Löschen eines Response-Bodys

Wenn Sie den Response-Body löschen möchten, können Sie die clearBody-Methode verwenden:

Flight::route('/', function() {
    if($someCondition) {
        Flight::response()->write("Hello, World!");
    } else {
        Flight::response()->clearBody();
    }
});

Der obige Anwendungsfall ist wahrscheinlich nicht üblich, könnte aber häufiger vorkommen, wenn dies in einem Middleware verwendet wird.

Ausführen eines Callbacks auf dem Response-Body

Sie können einen Callback auf dem Response-Body ausführen, indem Sie die addResponseBodyCallback-Methode verwenden:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);
});

// Dies wird alle Responses für jede Route gzippen
Flight::response()->addResponseBodyCallback(function($body) {
    return gzencode($body, 9);
});

Sie können mehrere Callbacks hinzufügen, und sie werden in der Reihenfolge ausgeführt, in der sie hinzugefügt wurden. Da dies jede callable akzeptieren kann, kann es ein Klassen-Array [ $class, 'method' ], eine Closure $strReplace = function($body) { str_replace('hi', 'there', $body); }; oder einen Funktionsnamen 'minify' akzeptieren, wenn Sie z.B. eine Funktion haben, um Ihren HTML-Code zu minimieren.

Hinweis: Route-Callbacks funktionieren nicht, wenn Sie die Konfigurationsoption flight.v2.output_buffering verwenden.

Spezifischer Route-Callback

Wenn Sie möchten, dass dies nur auf eine spezifische Route angewendet wird, können Sie den Callback direkt in der Route hinzufügen:

Flight::route('/users', function() {
    $db = Flight::db();
    $users = $db->fetchAll("SELECT * FROM users");
    Flight::render('users_table', ['users' => $users]);

    // Dies wird nur die Response für diese Route gzippen
    Flight::response()->addResponseBodyCallback(function($body) {
        return gzencode($body, 9);
    });
});

Middleware-Option

Sie können auch Middleware verwenden, um den Callback auf alle Routes über Middleware anzuwenden:

// MinifyMiddleware.php
class MinifyMiddleware {
    public function before() {
        // Wenden Sie den Callback hier auf das response()-Objekt an.
        Flight::response()->addResponseBodyCallback(function($body) {
            return $this->minify($body);
        });
    }

    protected function minify(string $body): string {
        // minimieren Sie den Body irgendwie
        return $body;
    }
}

// index.php
Flight::group('/users', function() {
    Flight::route('', function() { /* ... */ });
    Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);

Statuscodes

Sie können den Statuscode der Response mit der status-Methode setzen:

Flight::route('/@id', function($id) {
    if($id == 123) {
        Flight::response()->status(200);
        echo "Hello, World!";
    } else {
        Flight::response()->status(403);
        echo "Forbidden";
    }
});

Wenn Sie den aktuellen Statuscode abrufen möchten, können Sie die status-Methode ohne Argumente verwenden:

Flight::response()->status(); // 200

Setzen eines Response-Headers

Sie können einen Header wie den Content-Type der Response mit der header-Methode setzen:

// Dies sendet "Hello, World!" an den Browser des Benutzers als reinen Text
Flight::route('/', function() {
    Flight::response()->header('Content-Type', 'text/plain');
    // oder
    Flight::response()->setHeader('Content-Type', 'text/plain');
    echo "Hello, World!";
});

Weiterleitung

Sie können die aktuelle Anfrage weiterleiten, indem Sie die redirect()-Methode verwenden und eine neue URL übergeben:

Flight::route('/login', function() {
    $username = Flight::request()->data->username;
    $password = Flight::request()->data->password;
    $passwordConfirm = Flight::request()->data->password_confirm;

    if($password !== $passwordConfirm) {
        Flight::redirect('/new/location');
        return; // dies ist notwendig, damit die Funktionalität unten nicht ausgeführt wird
    }

    // fügen Sie den neuen Benutzer hinzu...
    Flight::db()->runQuery("INSERT INTO users ....");
    Flight::redirect('/admin/dashboard');
});

Hinweis: Standardmäßig sendet Flight einen HTTP 303 ("See Other")-Statuscode. Sie können optional einen benutzerdefinierten Code setzen:

Flight::redirect('/new/location', 301); // permanent

Stoppen der Route-Ausführung

Sie können das Framework stoppen und sofort beenden, indem Sie die halt-Methode aufrufen:

Flight::halt();

Sie können auch einen optionalen HTTP-Statuscode und eine Nachricht angeben:

Flight::halt(200, 'Be right back...');

Das Aufrufen von halt verwirft alle Response-Inhalte bis zu diesem Punkt und stoppt die gesamte Ausführung. Wenn Sie das Framework stoppen und die aktuelle Response ausgeben möchten, verwenden Sie die stop-Methode:

Flight::stop($httpStatusCode = null);

Hinweis: Flight::stop() hat einiges seltsames Verhalten, wie z.B. dass es die Response ausgibt, aber die Ausführung Ihres Skripts fortsetzt, was möglicherweise nicht das ist, was Sie wollen. Sie können exit oder return nach dem Aufruf von Flight::stop() verwenden, um weitere Ausführung zu verhindern, aber es wird im Allgemeinen empfohlen, Flight::halt() zu verwenden.

Dies speichert den Header-Schlüssel und -Wert im Response-Objekt. Am Ende des Request-Lebenszyklus wird es die Header aufbauen und eine Response senden.

Erweiterte Verwendung

Sofortiges Senden eines Headers

Es kann Fälle geben, in denen Sie etwas Benutzerdefiniertes mit dem Header tun müssen und den Header in genau dieser Code-Zeile senden müssen, an der Sie arbeiten. Wenn Sie eine streamed route setzen, ist das, was Sie brauchen. Das ist durch response()->setRealHeader() erreichbar.

Flight::route('/', function() {
    Flight::response()->setRealHeader('Content-Type: text/plain');
    echo 'Streaming response...';
    sleep(5);
    echo 'Done!';
})->stream();

JSONP

Für JSONP-Anfragen können Sie optional den Query-Parameter-Namen übergeben, den Sie verwenden, um Ihre Callback-Funktion zu definieren:

Flight::jsonp(['id' => 123], 'q');

Also, wenn Sie eine GET-Anfrage mit ?q=my_func stellen, sollten Sie die Ausgabe erhalten:

my_func({"id":123});

Wenn Sie keinen Query-Parameter-Namen übergeben, wird standardmäßig jsonp verwendet.

Hinweis: Wenn Sie 2025 und später immer noch JSONP-Anfragen verwenden, springen Sie in den Chat und erzählen Sie uns warum! Wir lieben es, gute Kampf-/Horror-Geschichten zu hören!

Löschen von Response-Daten

Sie können den Response-Body und Header löschen, indem Sie die clear()-Methode verwenden. Dies löscht alle der Response zugewiesenen Header, löscht den Response-Body und setzt den Statuscode auf 200.

Flight::response()->clear();

Nur Response-Body löschen

Wenn Sie nur den Response-Body löschen möchten, können Sie die clearBody()-Methode verwenden:

// Dies behält immer noch alle auf dem response()-Objekt gesetzten Header.
Flight::response()->clearBody();

HTTP-Caching

Flight bietet integrierte Unterstützung für HTTP-Level-Caching. Wenn die Caching-Bedingung erfüllt ist, wird Flight eine HTTP 304 Not Modified-Response zurückgeben. Beim nächsten Mal, wenn der Client dieselbe Ressource anfordert, wird er aufgefordert, seine lokal gecachte Version zu verwenden.

Route-Level-Caching

Wenn Sie Ihre gesamte Response cachen möchten, können Sie die cache()-Methode verwenden und eine Cache-Zeit übergeben.


// Dies cached die Response für 5 Minuten
Flight::route('/news', function () {
  Flight::response()->cache(time() + 300);
  echo 'This content will be cached.';
});

// Alternativ können Sie einen String verwenden, den Sie an die strtotime()-Methode übergeben würden
Flight::route('/news', function () {
  Flight::response()->cache('+5 minutes');
  echo 'This content will be cached.';
});

Last-Modified

Sie können die lastModified-Methode verwenden und einen UNIX-Timestamp übergeben, um das Datum und die Zeit zu setzen, zu der eine Seite zuletzt geändert wurde. Der Client wird sein Cache weiterhin verwenden, bis der Last-Modified-Wert geändert wird.

Flight::route('/news', function () {
  Flight::lastModified(1234567890);
  echo 'This content will be cached.';
});

ETag

ETag-Caching ist ähnlich wie Last-Modified, außer dass Sie jede ID für die Ressource angeben können, die Sie möchten:

Flight::route('/news', function () {
  Flight::etag('my-unique-id');
  echo 'This content will be cached.';
});

Beachten Sie, dass das Aufrufen von entweder lastModified oder etag beide den Cache-Wert setzt und prüft. Wenn der Cache-Wert zwischen den Anfragen gleich ist, wird Flight sofort eine HTTP 304-Response senden und die Verarbeitung stoppen.

Herunterladen einer Datei

v3.12.0

Es gibt eine Hilfsmethode, um eine Datei an den Endbenutzer zu streamen. Sie können die download-Methode verwenden und den Pfad übergeben.

Flight::route('/download', function () {
  Flight::download('/path/to/file.txt');
  // Ab v3.17.1 können Sie einen benutzerdefinierten Dateinamen für das Download angeben
  Flight::download('/path/to/file.txt', 'custom_name.txt');
});

Siehe auch

Fehlerbehebung

Changelog

Learn/events

Event Manager

ab v3.15.0

Überblick

Events ermöglichen es Ihnen, benutzerdefiniertes Verhalten in Ihrer Anwendung zu registrieren und auszulösen. Mit der Ergänzung von Flight::onEvent() und Flight::triggerEvent() können Sie nun in Schlüssel-Momente des Lebenszyklus Ihrer App eingreifen oder eigene Events definieren (wie Benachrichtigungen und E-Mails), um Ihren Code modularer und erweiterbarer zu machen. Diese Methoden sind Teil der mappbaren Methoden von Flight, was bedeutet, dass Sie ihr Verhalten nach Bedarf überschreiben können.

Verständnis

Events erlauben es Ihnen, verschiedene Teile Ihrer Anwendung zu trennen, damit sie nicht zu stark voneinander abhängen. Diese Trennung – oft als Entkopplung bezeichnet – macht Ihren Code einfacher zu aktualisieren, zu erweitern oder zu debuggen. Anstatt alles in einem großen Block zu schreiben, können Sie Ihre Logik in kleinere, unabhängige Teile aufteilen, die auf spezifische Aktionen (Events) reagieren.

Stellen Sie sich vor, Sie bauen eine Blog-App:

Ohne Events würden Sie all das in eine Funktion packen. Mit Events können Sie es aufteilen: Ein Teil speichert den Kommentar, ein anderer löst ein Event wie 'comment.posted' aus, und separate Listener handhaben die E-Mail und das Protokollieren. Das hält Ihren Code sauberer und ermöglicht es Ihnen, Funktionen (wie Benachrichtigungen) hinzuzufügen oder zu entfernen, ohne die Kernlogik zu berühren.

Häufige Anwendungsfälle

In den meisten Fällen eignen sich Events für Dinge, die optional sind, aber nicht zwingend ein absoluter Kernteil Ihres Systems. Zum Beispiel sind die Folgenden gut zu haben, aber wenn sie aus irgendeinem Grund fehlschlagen, sollte Ihre Anwendung immer noch funktionieren:

Angenommen jedoch, Sie haben eine „Passwort vergessen“-Funktion. Diese sollte Teil Ihrer Kernfunktionalität sein und kein Event, da wenn diese E-Mail nicht versendet wird, der Benutzer sein Passwort nicht zurücksetzen und Ihre Anwendung nicht nutzen kann.

Grundlegende Verwendung

Das Event-System von Flight basiert auf zwei Hauptmethoden: Flight::onEvent() zum Registrieren von Event-Listenern und Flight::triggerEvent() zum Auslösen von Events. Hier ist, wie Sie sie verwenden können:

Registrieren von Event-Listenern

Um auf ein Event zu hören, verwenden Sie Flight::onEvent(). Diese Methode ermöglicht es Ihnen, zu definieren, was passieren soll, wenn ein Event auftritt.

Flight::onEvent(string $event, callable $callback): void

Sie „abonnieren“ ein Event, indem Sie Flight mitteilen, was es tun soll, wenn es passiert. Der Callback kann Argumente akzeptieren, die vom Event-Auslöser übergeben werden.

Das Event-System von Flight ist synchron, was bedeutet, dass jeder Event-Listener nacheinander ausgeführt wird. Wenn Sie ein Event auslösen, werden alle registrierten Listener für dieses Event vollständig ausgeführt, bevor Ihr Code fortfährt. Dies ist wichtig zu verstehen, da es sich von asynchronen Event-Systemen unterscheidet, bei denen Listener parallel oder zu einem späteren Zeitpunkt ausgeführt werden könnten.

Einfaches Beispiel

Flight::onEvent('user.login', function ($username) {
    echo "Willkommen zurück, $username!";

    // Sie können eine E-Mail senden, wenn der Login von einem neuen Standort kommt
});

Hier, wenn das 'user.login'-Event ausgelöst wird, begrüßt es den Benutzer namentlich und könnte auch Logik enthalten, um eine E-Mail zu senden, falls nötig.

Hinweis: Der Callback kann eine Funktion, eine anonyme Funktion oder eine Methode aus einer Klasse sein.

Auslösen von Events

Um ein Event auszulösen, verwenden Sie Flight::triggerEvent(). Dies weist Flight an, alle für dieses Event registrierten Listener auszuführen und dabei alle von Ihnen bereitgestellten Daten weiterzuleiten.

Flight::triggerEvent(string $event, ...$args): void

Einfaches Beispiel

$username = 'alice';
Flight::triggerEvent('user.login', $username);

Dies löst das 'user.login'-Event aus und sendet 'alice' an den Listener, den wir zuvor definiert haben, was ausgibt: Willkommen zurück, alice!.

Stoppen von Events

Wenn ein Listener false zurückgibt, werden keine weiteren Listener für dieses Event ausgeführt. Dies ermöglicht es Ihnen, die Event-Kette basierend auf spezifischen Bedingungen zu stoppen. Denken Sie daran, dass die Reihenfolge der Listener wichtig ist, da der erste, der false zurückgibt, den Rest stoppt.

Beispiel:

Flight::onEvent('user.login', function ($username) {
    if (isBanned($username)) {
        logoutUser($username);
        return false; // Stoppt nachfolgende Listener
    }
});
Flight::onEvent('user.login', function ($username) {
    sendWelcomeEmail($username); // Dies wird nie gesendet
});

Überschreiben von Event-Methoden

Flight::onEvent() und Flight::triggerEvent() können erweitert werden, was bedeutet, dass Sie definieren können, wie sie funktionieren. Das ist großartig für fortgeschrittene Benutzer, die das Event-System anpassen möchten, z. B. durch Hinzufügen von Protokollierung oder Änderung der Event-Verteilung.

Beispiel: Anpassen von onEvent

Flight::map('onEvent', function (string $event, callable $callback) {
    // Protokolliere jede Event-Registrierung
    error_log("Neuer Event-Listener hinzugefügt für: $event");
    // Rufe das Standardverhalten auf (angenommen ein internes Event-System)
    Flight::_onEvent($event, $callback);
});

Jetzt wird jedes Mal, wenn Sie ein Event registrieren, protokolliert, bevor es fortgesetzt wird.

Warum überschreiben?

Wo Events platzieren

Wenn Sie neu in den Event-Konzepten in Ihrem Projekt sind, fragen Sie sich vielleicht: Wo registriere ich all diese Events in meiner App? Die Einfachheit von Flight bedeutet, dass es keine strenge Regel gibt – Sie können sie überall platzieren, wo es für Ihr Projekt Sinn macht. Allerdings hilft es, sie organisiert zu halten, um Ihren Code zu pflegen, wenn Ihre App wächst. Hier sind einige praktische Optionen und Best Practices, angepasst an die leichte Natur von Flight:

Option 1: In Ihrer Haupt-index.php

Für kleine Apps oder schnelle Prototypen können Sie Events direkt in Ihrer index.php-Datei neben Ihren Routen registrieren. Das hält alles an einem Ort, was in Ordnung ist, wenn Einfachheit Ihre Priorität ist.

require 'vendor/autoload.php';

// Events registrieren
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in at " . date('Y-m-d H:i:s'));
});

// Routen definieren
Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
});

Flight::start();

Option 2: Eine separate events.php-Datei

Für eine etwas größere App ziehen Sie in Erwägung, Event-Registrierungen in eine dedizierte Datei wie app/config/events.php zu verschieben. Schließen Sie diese Datei in Ihrer index.php vor Ihren Routen ein. Das ahmt nach, wie Routen oft in app/config/routes.php in Flight-Projekten organisiert werden.

// app/config/events.php
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in at " . date('Y-m-d H:i:s'));
});

Flight::onEvent('user.registered', function ($email, $name) {
    echo "Email sent to $email: Welcome, $name!";
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';

Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
});

Flight::start();

Option 3: Nahe dem Auslöseort

Ein anderer Ansatz ist, Events nahe dem Ort zu registrieren, an dem sie ausgelöst werden, z. B. in einem Controller oder Routen-Definition. Das funktioniert gut, wenn ein Event spezifisch für einen Teil Ihrer App ist.

Flight::route('/signup', function () {
    // Event hier registrieren
    Flight::onEvent('user.registered', function ($email) {
        echo "Welcome email sent to $email!";
    });

    $email = 'jane@example.com';
    Flight::triggerEvent('user.registered', $email);
    echo "Signed up!";
});

Best Practice für Flight

Tipp: Nach Zweck gruppieren

In events.php gruppieren Sie verwandte Events (z. B. alle benutzerbezogenen Events zusammen) mit Kommentaren für Klarheit:

// app/config/events.php
// User Events
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in");
});
Flight::onEvent('user.registered', function ($email) {
    echo "Welcome to $email!";
});

// Page Events
Flight::onEvent('page.updated', function ($pageId) {
    Flight::cache()->delete("page_$pageId");
});

Diese Struktur skaliert gut und bleibt anfängerfreundlich.

Beispiele aus der Praxis

Lassen Sie uns einige reale Szenarien durchgehen, um zu zeigen, wie Events funktionieren und warum sie hilfreich sind.

Beispiel 1: Protokollieren eines Benutzer-Logins

// Schritt 1: Einen Listener registrieren
Flight::onEvent('user.login', function ($username) {
    $time = date('Y-m-d H:i:s');
    error_log("$username logged in at $time");
});

// Schritt 2: Es in Ihrer App auslösen
Flight::route('/login', function () {
    $username = 'bob'; // Stellen Sie sich vor, das kommt aus einem Formular
    Flight::triggerEvent('user.login', $username);
    echo "Hi, $username!";
});

Warum nützlich: Der Login-Code muss nichts über Protokollierung wissen – er löst nur das Event aus. Sie können später mehr Listener hinzufügen (z. B. eine Willkommens-E-Mail senden), ohne die Route zu ändern.

Beispiel 2: Benachrichtigen über neue Benutzer

// Listener für neue Registrierungen
Flight::onEvent('user.registered', function ($email, $name) {
    // Simuliere das Senden einer E-Mail
    echo "Email sent to $email: Welcome, $name!";
});

// Auslösen, wenn jemand registriert
Flight::route('/signup', function () {
    $email = 'jane@example.com';
    $name = 'Jane';
    Flight::triggerEvent('user.registered', $email, $name);
    echo "Thanks for signing up!";
});

Warum nützlich: Die Registrierungslogik konzentriert sich auf die Erstellung des Benutzers, während das Event Benachrichtigungen handhabt. Sie könnten später mehr Listener hinzufügen (z. B. die Registrierung protokollieren).

Beispiel 3: Cache leeren

// Listener zum Leeren eines Caches
Flight::onEvent('page.updated', function ($pageId) {
    // Wenn Sie das flightphp/cache-Plugin verwenden
    Flight::cache()->delete("page_$pageId");
    echo "Cache cleared for page $pageId.";
});

// Auslösen, wenn eine Seite bearbeitet wird
Flight::route('/edit-page/(@id)', function ($pageId) {
    // Stellen Sie sich vor, wir haben die Seite aktualisiert
    Flight::triggerEvent('page.updated', $pageId);
    echo "Page $pageId updated.";
});

Warum nützlich: Der Bearbeitungscode kümmert sich nicht um Caching – er signalisiert nur die Aktualisierung. Andere Teile der App können entsprechend reagieren.

Best Practices

Das Event-System in Flight PHP mit Flight::onEvent() und Flight::triggerEvent() bietet Ihnen eine einfache, aber leistungsstarke Möglichkeit, flexible Anwendungen zu bauen. Indem verschiedene Teile Ihrer App durch Events miteinander kommunizieren, können Sie Ihren Code organisiert, wiederverwendbar und einfach erweiterbar halten. Ob Sie Aktionen protokollieren, Benachrichtigungen senden oder Updates verwalten – Events helfen Ihnen dabei, ohne Ihre Logik zu verknüpfen. Und mit der Möglichkeit, diese Methoden zu überschreiben, haben Sie die Freiheit, das System an Ihre Bedürfnisse anzupassen. Starten Sie klein mit einem einzelnen Event und beobachten Sie, wie es die Struktur Ihrer App verändert!

Eingebauten Events

Flight PHP kommt mit einigen eingebauten Events, die Sie verwenden können, um in den Lebenszyklus des Frameworks einzugreifen. Diese Events werden an spezifischen Punkten im Request/Response-Zyklus ausgelöst und ermöglichen es Ihnen, benutzerdefinierte Logik auszuführen, wenn bestimmte Aktionen auftreten.

Liste der eingebauten Events

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/dependency_injection_container

Dependency-Injection-Container

Übersicht

Der Dependency-Injection-Container (DIC) ist eine leistungsstarke Erweiterung, mit der Sie die Abhängigkeiten Ihrer Anwendung verwalten können. Er ist auch einer der Hauptgründe, warum Flight gut mit KI-Programmierwerkzeugen und Unit-Tests funktioniert: Controller nehmen im Konstruktor, was sie benötigen, anstatt auf globale Variablen zuzugreifen.

Verständnis

Dependency Injection (DI) ist ein zentrales Konzept in modernen PHP-Frameworks und wird verwendet, um die Instanziierung und Konfiguration von Objekten zu verwalten. Einige Beispiele für DIC-Bibliotheken sind: flightphp/container, Dice, Pimple, PHP-DI und league/container.

Ein DIC ist eine ausgefallene Möglichkeit, Ihre Klassen an einem zentralen Ort zu erstellen und zu verwalten. Das ist nützlich, wenn Sie dasselbe Objekt an mehrere Klassen (Controller, Middleware, Befehle usw.) übergeben müssen.

Das offizielle flightphp/skeleton bindet Dice in app/config/services.php ein, ersetzt die gemeinsame flight\Engine-Instanz und löst Routenziele wie [App\Controller\HomeController::class, 'index'] auf. Bevorzugen Sie dieses Muster für neue Projekte, damit Menschen und Agenten dieselben Stellen bearbeiten.

Grundlegende Verwendung

Die alte Vorgehensweise könnte so aussehen:


require 'vendor/autoload.php';

// Klasse zur Verwaltung von Benutzern aus der Datenbank
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// in Ihrer routes.php-Datei

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// weitere UserController-Routen...

Flight::start();

Sie können am obigen Code sehen, dass wir ein neues PDO-Objekt erstellen und es an unsere UserController-Klasse übergeben. Das ist für eine kleine Anwendung in Ordnung, aber wenn Ihre Anwendung wächst, werden Sie feststellen, dass Sie dasselbe PDO-Objekt an mehreren Stellen erstellen oder weiterreichen. Hier kommt ein DIC ins Spiel.

Hier ist dasselbe Beispiel mit einem DIC (unter Verwendung von Dice):


require 'vendor/autoload.php';

// dieselbe Klasse wie oben. Nichts geändert
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// einen neuen Container erstellen
$container = new \Dice\Dice;

// eine Regel hinzufügen, die dem Container mitteilt, wie ein PDO-Objekt erstellt wird
// nicht vergessen, es wie unten gezeigt sich selbst zuzuweisen!
$container = $container->addRule('PDO', [
    // shared bedeutet, dass jedes Mal dasselbe Objekt zurückgegeben wird
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// Dies registriert den Container-Handler, damit Flight weiß, dass er ihn verwenden soll.
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// jetzt können wir den Container verwenden, um unseren UserController zu erstellen
Flight::route('/user/@id', [ UserController::class, 'view' ]);

Flight::start();

Ich wette, Sie denken vielleicht, dass dem Beispiel eine Menge zusätzlicher Code hinzugefügt wurde. Die Magie zeigt sich, wenn Sie einen weiteren Controller haben, der das PDO-Objekt benötigt.


// Wenn alle Ihre Controller einen Konstruktor haben, der ein PDO-Objekt benötigt,
// wird es automatisch in jede der folgenden Routen injiziert!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);

Ein zusätzlicher Vorteil der Nutzung eines DIC ist, dass Unit-Tests viel einfacher werden. Sie können ein Mock-Objekt erstellen und es an Ihre Klasse übergeben. Das ist ein großer Vorteil, wenn Sie Tests für Ihre Anwendung schreiben – und wenn ein KI-Assistent einen Controller generiert, gibt die Konstruktor-Injektion ihm ein klares, konsistentes Muster, dem er folgen kann (Unit-Testing-Leitfaden).

Einen zentralen DIC-Handler erstellen

Sie können einen zentralen DIC-Handler in Ihrer Services-Datei erstellen, indem Sie Ihre App erweitern. Hier ist ein Beispiel:

// services.php

// einen neuen Container erstellen
$container = new \Dice\Dice;
// nicht vergessen, es wie unten gezeigt sich selbst zuzuweisen!
$container = $container->addRule('PDO', [
    // shared bedeutet, dass jedes Mal dasselbe Objekt zurückgegeben wird
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// jetzt können wir eine mappbare Methode erstellen, um jedes Objekt zu erstellen.
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// Dies registriert den Container-Handler, damit Flight weiß, dass er ihn für Controller/Middleware verwenden soll
Flight::registerContainerHandler(function($class, $params) {
    return Flight::make($class, $params);
});


// nehmen wir an, wir haben die folgende Beispielklasse, die ein PDO-Objekt im Konstruktor erhält
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // Code, der eine E-Mail sendet
    }
}

// Und schließlich können Sie Objekte mithilfe von Dependency Injection erstellen
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

flightphp/container

Flight hat ein Plugin, das einen einfachen PSR-11-konformen Container bereitstellt, den Sie für Ihre Dependency Injection verwenden können. Hier ist ein kurzes Beispiel zur Verwendung:


// index.php zum Beispiel
require 'vendor/autoload.php';

use flight\Container;

$container = new Container;

$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));

Flight::registerContainerHandler([$container, 'get']);

class TestController {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function index() {
    var_dump($this->pdo);
    // wird dies korrekt ausgeben!
  }
}

Flight::route('GET /', [TestController::class, 'index']);

Flight::start();

Erweiterte Verwendung von flightphp/container

Sie können Abhängigkeiten auch rekursiv auflösen. Hier ist ein Beispiel:

<?php

require 'vendor/autoload.php';

use flight\Container;

class User {}

interface UserRepository {
  function find(int $id): ?User;
}

class PdoUserRepository implements UserRepository {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function find(int $id): ?User {
    // Implementierung ...
    return null;
  }
}

$container = new Container;

$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);

$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);

/*
object(PdoUserRepository)#4 (1) {
  ["pdo":"PdoUserRepository":private]=>
  object(PDO)#3 (0) {
  }
}
 */

DICE

Sie können auch Ihren eigenen DIC-Handler erstellen. Das ist nützlich, wenn Sie einen benutzerdefinierten Container verwenden möchten, der nicht PSR-11-konform ist (Dice). Siehe den Abschnitt grundlegende Verwendung für Details.

Zusätzlich gibt es einige hilfreiche Standardeinstellungen, die Ihnen die Arbeit mit Flight erleichtern.

Engine-Instanz (erforderlich für die $app-Injektion)

Wenn Sie flight\Engine in Controllern oder Middleware als Typ-Hinweis verwenden, darf Dice keine neue Engine erstellen. Ersetzen Sie sie durch dieselbe Instanz aus dem Bootstrap. Das macht das offizielle Skeleton, und es ist das Muster, das AGENTS.md für KI-generierte Controller erwartet:

// Irgendwo in Ihrem Bootstrap / services.php
use flight\Engine;
use flight\database\SimplePdo;

$app = Flight::app(); // oder $engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Kritisch: die gebootstrappte Engine wiederverwenden – Dice nicht `new Engine()` ausführen lassen
        Engine::class => $app,
        // SimplePdo für neuen Code bevorzugen
        // SimplePdo::class => $db,
        // Config::class => $config,
        // \Twig\Environment::class => $twig,
    ]
]);

$app->registerContainerHandler(function ($class, $params) use ($container) {
    return $container->create($class, $params);
});

// Optionaler Helfer für Nicht-Routen-Code
$app->map('make', function ($class, $params = []) use ($container) {
    return $container->create($class, $params);
});
// app/Controller/MyController.php  (Skeleton-Layout – Ordner-Schreibweise entspricht dem Namespace)
namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        // Keine Flight::-Fassade in der App-Ebene – einfacher zu testen und klarer für KI-Tools
        $this->app->render('welcome', ['message' => 'Hello']);
    }
}
// app/config/routes.php
use App\Controller\MyController;

$router->get('/', [MyController::class, 'index']);

Wenn Sie die Engine-Substitution überspringen, könnte Dice eine zweite Engine erstellen und Ihr Controller wird keine Routen, Konfiguration oder das gemappte Twig-render aus dem Bootstrap teilen.

Weitere gemeinsame Services hinzufügen (SimplePdo, Config, Twig)

use flight\database\SimplePdo;
use flight\Engine;

// Nachdem Sie $db, $config, $twig in services.php erstellt haben:
$substitutions = [
    Engine::class => $app,
    SimplePdo::class => $db,
    // App\Utils\Config::class => $config,
    // \Twig\Environment::class => $twig,
];

$container = $container->addRule('*', [
    'substitutions' => $substitutions,
]);

Dann können Controller SimplePdo $db (oder Ihren Konfigurationstyp) im Konstruktor entgegennehmen und müssen nie Flight::db() aufrufen. Das entspricht dem Unit-Testing-Leitfaden und dem Hausstil des Skeletons.

Weitere Klassen hinzufügen

Wenn Sie weitere Klassen zum Container hinzufügen möchten, ist das mit Dice einfach, da sie automatisch vom Container aufgelöst werden. Hier ist ein Beispiel:


$container = new \Dice\Dice;
// Wenn Sie keine Abhängigkeiten in Ihre Klassen injizieren müssen,
// müssen Sie nichts definieren!
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

class MyCustomClass {
    public function parseThing() {
        return 'thing';
    }
}

class UserController {

    protected MyCustomClass $MyCustomClass;

    public function __construct(MyCustomClass $MyCustomClass) {
        $this->MyCustomClass = $MyCustomClass;
    }

    public function index() {
        echo $this->MyCustomClass->parseThing();
    }
}

Flight::route('/user', 'UserController->index');

PSR-11

Flight kann auch jeden PSR-11-konformen Container verwenden. Das bedeutet, dass Sie jeden Container verwenden können, der das PSR-11-Interface implementiert. Hier ist ein Beispiel mit dem PSR-11-Container von League:


require 'vendor/autoload.php';

use flight\database\SimplePdo;

// dieselbe UserController-Idee wie oben, mit SimplePdo als Typ-Hinweis statt rohem PDO

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
    ->addArgument('mysql:host=localhost;dbname=test')
    ->addArgument('user')
    ->addArgument('pass');
Flight::registerContainerHandler($container);

Flight::route('/user', [ 'UserController', 'view' ]);

Flight::start();

Das kann etwas ausführlicher sein als das vorherige Dice-Beispiel, erfüllt aber denselben Zweck mit denselben Vorteilen!

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/unit_testing

Unit-Tests

Übersicht

Unit-Tests in Flight helfen Ihnen, sicherzustellen, dass Ihre Anwendung wie erwartet funktioniert, Fehler früh zu erkennen und Ihre Codebasis einfacher zu warten. Flight ist so konzipiert, dass es reibungslos mit PHPUnit funktioniert, dem beliebtesten PHP-Testframework.

Verständnis

Unit-Tests prüfen das Verhalten kleiner Teile Ihrer Anwendung (wie Controller oder Services) isoliert. In Flight bedeutet dies, zu testen, wie Ihre Routen, Controller und Logik auf verschiedene Eingaben reagieren – ohne sich auf globalen Zustand oder echte externe Dienste zu verlassen.

Wichtige Grundsätze:

Grundlegende Verwendung

PHPUnit einrichten

  1. PHPUnit mit Composer installieren:
    composer require --dev phpunit/phpunit
  2. Erstellen Sie ein tests-Verzeichnis im Stammverzeichnis Ihres Projekts.
  3. Fügen Sie ein Test-Skript zu Ihrer composer.json hinzu:
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Erstellen Sie eine phpunit.xml-Datei:
    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

Jetzt können Sie Ihre Tests mit composer test ausführen.

Testen eines einfachen Routen-Handlers

Angenommen, Sie haben eine Route, die eine E-Mail validiert:

// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);

// UserController.php
class UserController {
    protected $app;
    public function __construct(flight\Engine $app) {
        $this->app = $app;
    }
    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
        }
        return $this->app->json(['status' => 'success', 'message' => 'Valid email']);
    }
}

Ein einfacher Test für diesen Controller:

use PHPUnit\Framework\TestCase;
use flight\Engine;

class UserControllerTest extends TestCase {
    public function testValidEmailReturnsSuccess() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserController($app);
        $controller->register();
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';
        $controller = new UserController($app);
        $controller->register();
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

Tipps:

Verwenden von Dependency Injection für testbare Controller

Injizieren Sie Abhängigkeiten (wie Datenbank oder Mailer) in Ihre Controller, um sie in Tests einfach mocken zu können:

use flight\database\SimplePdo;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;
    public function __construct($app, $db, $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }
    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
        }
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

Und ein Test mit Mocks:

use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $mockDb = $this->createMock(flight\database\SimplePdo::class);
        $mockDb->method('runQuery')->willReturn(true);
        $mockMailer = new class {
            public $sentEmail = null;
            public function sendWelcome($email) { $this->sentEmail = $email; return true; }
        };
        $app = new flight\Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserController($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }
}

Erweiterte Verwendung

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/simple_pdo

SimplePdo PDO-Hilfsklasse

Überblick

Die SimplePdo-Klasse in Flight ist eine moderne, funktionsreiche Hilfsklasse für die Arbeit mit Datenbanken unter Verwendung von PDO. Sie erweitert PdoWrapper und fügt bequeme Hilfsmethoden für gängige Datenbankoperationen wie insert(), update(), delete() und Transaktionen hinzu. Sie vereinfacht Datenbankaufgaben, gibt Ergebnisse als Collections zurück für einfachen Zugriff und unterstützt Abfrageprotokollierung und Anwendungsleistungsüberwachung (APM) für fortgeschrittene Anwendungsfälle.

Verständnis

Die SimplePdo-Klasse ist so konzipiert, dass die Arbeit mit Datenbanken in PHP viel einfacher wird. Statt mit vorbereiteten Anweisungen, Abrufmodi und ausführlichen SQL-Operationen zu jonglieren, erhalten Sie saubere, einfache Methoden für gängige Aufgaben. Jede Zeile wird als Collection zurückgegeben, sodass Sie sowohl Array-Notation ($row['name']) als auch Objekt-Notation ($row->name) verwenden können.

Diese Klasse ist eine Übersetzung von PdoWrapper, was bedeutet, dass sie alle Funktionen von PdoWrapper plus zusätzliche Hilfsmethoden enthält, die Ihren Code sauberer und wartbarer machen. Wenn Sie derzeit PdoWrapper verwenden, ist das Upgrade auf SimplePdo unkompliziert, da es PdoWrapper erweitert.

Sie können SimplePdo als geteilten Dienst in Flight registrieren und es dann überall in Ihrer App über Flight::db() verwenden.

Grundlegende Verwendung

Registrierung von SimplePdo

Registrieren Sie zuerst die SimplePdo-Klasse bei Flight:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

HINWEIS

Wenn Sie PDO::ATTR_DEFAULT_FETCH_MODE nicht angeben, setzt SimplePdo es automatisch auf PDO::FETCH_ASSOC für Sie.

Nun können Sie Flight::db() überall verwenden, um Ihre Datenbankverbindung zu erhalten.

Ausführen von Abfragen

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

Verwenden Sie dies für INSERTs, UPDATEs oder wenn Sie Ergebnisse manuell abrufen möchten:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row is an array
}

Sie können es auch für Schreibvorgänge verwenden:

$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);

fetchField()

function fetchField(string $sql, array $params = []): mixed

Einen einzelnen Wert aus der Datenbank abrufen:

$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);

fetchRow()

function fetchRow(string $sql, array $params = []): ?Collection

Eine einzelne Zeile als Collection (Array-/Objektzugriff) abrufen:

$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// or
echo $user->name;

TIP

SimplePdo fügt automatisch LIMIT 1 zu fetchRow()-Abfragen hinzu, falls es noch nicht vorhanden ist, was Ihre Abfragen effizienter macht.

fetchAll()

function fetchAll(string $sql, array $params = []): array<Collection>

Alle Zeilen als Array von Collections abrufen:

$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
    echo $user['name'];
    // or
    echo $user->name;
}

fetchColumn()

function fetchColumn(string $sql, array $params = []): array

Eine einzelne Spalte als Array abrufen:

$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// Returns: [1, 2, 3, 4, 5]

fetchPairs()

function fetchPairs(string $sql, array $params = []): array

Ergebnisse als Schlüssel-Wert-Paare abrufen (erste Spalte als Schlüssel, zweite als Wert):

$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// Returns: [1 => 'John', 2 => 'Jane', 3 => 'Bob']

Verwendung von IN()-Platzhaltern

Sie können einen einzelnen ? in einer IN()-Klausel verwenden und ein Array übergeben:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);

Hilfsmethoden

Einer der Hauptvorteile von SimplePdo gegenüber PdoWrapper ist die Hinzufügung bequemer Hilfsmethoden für gängige Datenbankoperationen.

insert()

function insert(string $table, array $data): string

Eine oder mehrere Zeilen einfügen und die letzte Einfüge-ID zurückgeben.

Einzelner Einfügevorgang:

$id = Flight::db()->insert('users', [
    'name' => 'John',
    'email' => 'john@example.com'
]);

Massen-Einfügevorgang:

$id = Flight::db()->insert('users', [
    ['name' => 'John', 'email' => 'john@example.com'],
    ['name' => 'Jane', 'email' => 'jane@example.com'],
]);

update()

function update(string $table, array $data, string $where, array $whereParams = []): int

Zeilen aktualisieren und die Anzahl der betroffenen Zeilen zurückgeben:

$affected = Flight::db()->update(
    'users',
    ['name' => 'Jane', 'email' => 'jane@example.com'],
    'id = ?',
    [1]
);

HINWEIS

SQLite's rowCount() gibt die Anzahl der Zeilen zurück, in denen Daten tatsächlich geändert wurden. Wenn Sie eine Zeile mit denselben Werten aktualisieren, die sie bereits hat, gibt rowCount() 0 zurück. Dies unterscheidet sich vom Verhalten von MySQL bei der Verwendung von PDO::MYSQL_ATTR_FOUND_ROWS.

delete()

function delete(string $table, string $where, array $whereParams = []): int

Zeilen löschen und die Anzahl der gelöschten Zeilen zurückgeben:

$deleted = Flight::db()->delete('users', 'id = ?', [1]);

transaction()

function transaction(callable $callback): mixed

Einen Callback innerhalb einer Transaktion ausführen. Die Transaktion wird automatisch bei Erfolg committet oder bei Fehler zurückgerollt:

$result = Flight::db()->transaction(function($db) {
    $db->insert('users', ['name' => 'John']);
    $db->insert('logs', ['action' => 'user_created']);
    return $db->lastInsertId();
});

Wenn innerhalb des Callbacks eine Ausnahme geworfen wird, wird die Transaktion automatisch zurückgerollt und die Ausnahme erneut geworfen.

Fortgeschrittene Verwendung

Abfrageprotokollierung & APM

Wenn Sie die Abfrageleistung verfolgen möchten, aktivieren Sie die APM-Verfolgung bei der Registrierung:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name',
    'user',
    'pass',
    [/* PDO options */],
    [
        'trackApmQueries' => true,
        'maxQueryMetrics' => 1000
    ]
]);

Nach dem Ausführen von Abfragen können Sie sie manuell protokollieren, aber das APM protokolliert sie automatisch, wenn aktiviert:

Flight::db()->logQueries();

Dies löst ein Ereignis (flight.db.queries) mit Verbindungs- und Abfragemetriken aus, das Sie mit Flights Ereignissystem abhören können.

Vollständiges Beispiel

Flight::route('/users', function () {
    // Get all users
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // Stream all users
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
    }

    // Get a single user
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // Get a single value
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // Get a single column
    $ids = Flight::db()->fetchColumn('SELECT id FROM users');

    // Get key-value pairs
    $userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');

    // Special IN() syntax
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);

    // Insert a new user
    $id = Flight::db()->insert('users', [
        'name' => 'Bob',
        'email' => 'bob@example.com'
    ]);

    // Bulk insert users
    Flight::db()->insert('users', [
        ['name' => 'Bob', 'email' => 'bob@example.com'],
        ['name' => 'Jane', 'email' => 'jane@example.com']
    ]);

    // Update a user
    $affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);

    // Delete a user
    $deleted = Flight::db()->delete('users', 'id = ?', [123]);

    // Use a transaction
    $result = Flight::db()->transaction(function($db) {
        $db->insert('users', ['name' => 'John', 'email' => 'john@example.com']);
        $db->insert('audit_log', ['action' => 'user_created']);
        return $db->lastInsertId();
    });
});

Migration von PdoWrapper

Wenn Sie derzeit PdoWrapper verwenden, ist die Migration zu SimplePdo unkompliziert:

  1. Aktualisieren Sie Ihre Registrierung:

    // Old
    Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]);
    
    // New
    Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]);
  2. Alle bestehenden PdoWrapper-Methoden funktionieren in SimplePdo - Es gibt keine Breaking Changes. Ihr bestehender Code wird weiterhin funktionieren.

  3. Optional die neuen Hilfsmethoden verwenden - Beginnen Sie mit insert(), update(), delete() und transaction(), um Ihren Code zu vereinfachen.

Siehe auch

Fehlerbehebung

Changelog

Learn/flight_vs_symfony

Flight vs Symfony

Was ist Symfony?

Symfony ist eine Reihe von wiederverwendbaren PHP-Komponenten und ein PHP-Framework für Webprojekte.

Das Standardfundament, auf dem die besten PHP-Anwendungen aufgebaut sind. Wählen Sie eine der 50 eigenständigen Komponenten für Ihre eigenen Anwendungen aus.

Beschleunigen Sie die Erstellung und Wartung Ihrer PHP-Webanwendungen. Beenden Sie wiederholende Codieraufgaben und genießen Sie die Kontrolle über Ihren Code.

Vor- und Nachteile im Vergleich zu Flight

Vorteile im Vergleich zu Flight

Nachteile im Vergleich zu Flight

Learn/configuration

Konfiguration

Überblick

Flight bietet eine einfache Möglichkeit, verschiedene Aspekte des Frameworks an die Bedürfnisse Ihrer Anwendung anzupassen. Einige sind standardmäßig festgelegt, aber Sie können sie bei Bedarf überschreiben. Sie können auch eigene Variablen festlegen, die in Ihrer gesamten Anwendung verwendet werden können.

Eine klare, gestufte Konfiguration (Datei-Standardwerte + Umgebungsgeheimnisse) hilft auch KI-Codierungstools: Agenten lernen einen Ort für Literale und einen Ort für Geheimnisse kennen, anstatt $_ENV-Zugriffe in Controllern zu erfinden.

Verständnis

Sie können bestimmte Verhaltensweisen von Flight anpassen, indem Sie Konfigurationswerte über die set-Methode festlegen.

Flight::set('flight.log_errors', true);

In einer strukturierten Anwendung (einschließlich des Skeletons) laden Sie normalerweise Projekteinstellungen aus app/config/config.php und wenden dann relevante Schlüssel auf die Engine an (z. B. flight.base_url, flight.views.path). Sie können auch ein kleines Konfigurationsobjekt in Controller injizieren, anstatt überall Globale zu lesen – freundlicher für Tests und für Agenten, die AGENTS.md folgen.

Grundlegende Verwendung

Flight-Konfigurationsoptionen

Im Folgenden finden Sie eine Liste aller verfügbaren Konfigurationseinstellungen:

Loader-Konfiguration

Es gibt zusätzlich eine weitere Konfigurationseinstellung für den Loader. Diese ermöglicht es Ihnen, Klassen mit _ im Klassennamen automatisch zu laden.

// Klassenladung mit Unterstrichen aktivieren
// Standardmäßig true
Loader::$v2ClassLoading = false;

Denken Sie daran, dass automatisches Laden auch von der Groß-/Kleinschreibung der Ordner abhängt, die mit Ihren Namespaces übereinstimmen muss – insbesondere mit dem App\- + app/Controller/-Layout des Skeletons.

Projektkonfiguration und .env (Skeleton-Muster)

Der Flight-Kern erfordert keine .env-Dateien. Viele Anwendungen verwenden nur ein PHP-Konfigurationsarray. Das offizielle Skeleton schichtet die Konfiguration so, dass Geheimnisse nicht in Git gelangen, während Runway weiterhin literale Konfiguration sicher neu schreiben kann:

  1. .env / echte Umgebung – Geheimnisse und Bereitstellungs-Overrides (gitignoriert).
  2. app/config/config.php – literale PHP-Array-Standardwerte (kopiert aus config_sample.php). Bevorzugen Sie keine $_ENV[...]-Ausdrücke in dieser Datei: Tools wie runway config:set können sie als statische Werte neu schreiben und Geheimnisse in die Datei einbrennen.
  3. Zusammenführen beim Bootstrap – Umgebungsvariablen haben Vorrang bei zugeordneten Schlüsseln; Anwendungscode liest ein Konfigurationsobjekt oder $app->get(), nicht $_ENV in Controllern.

Beispielstruktur von config_sample.php / config.php (vereinfacht):

<?php
// Nur Literale – Geheimnisse gehören in .env für den Skeleton-Workflow
return [
    'app' => [
        'env' => 'development',
        'debug' => true,
        'base_url' => '/',
        'timezone' => 'UTC',
    ],
    'database' => [
        'driver' => 'sqlite', // oder mysql, oder '' zum Deaktivieren
        'host' => 'localhost',
        'dbname' => '',
        'user' => '',
        'password' => '',
        'file_path' => __DIR__ . '/../../database.sqlite',
    ],
    // ...
];
# .env.example → .env (Skeleton)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...

Diese Trennung ist bewusst für KI-freundliche Projekte gedacht: Anweisungen können besagen: „Standardwerte in config.php, Geheimnisse in .env, Config/Engine injizieren – niemals Env-Zugriffe in einem Controller erfinden.“ Bestehende Anwendungen können .env vollständig ignorieren und eine einzige Konfigurationsdatei verwenden.

Variablen

Flight erlaubt es Ihnen, Variablen zu speichern, damit sie überall in Ihrer Anwendung verwendet werden können.

// Speichern Sie Ihre Variable
Flight::set('id', 123);

// An anderer Stelle in Ihrer Anwendung
$id = Flight::get('id');

Um zu überprüfen, ob eine Variable gesetzt wurde, können Sie Folgendes tun:

if (Flight::has('id')) {
  // Etwas tun
}

Sie können eine Variable löschen, indem Sie Folgendes tun:

// Löscht die id-Variable
Flight::clear('id');

// Löscht alle Variablen
Flight::clear();

Hinweis: Nur weil Sie eine Variable setzen können, bedeutet das nicht, dass Sie es tun sollten. Verwenden Sie diese Funktion sparsam. Der Grund dafür ist, dass alles, was hier gespeichert wird, eine globale Variable wird. Globale Variablen sind schlecht, weil sie von überall in Ihrer Anwendung geändert werden können, was es schwer macht, Fehler aufzuspüren. Außerdem kann dies Dinge wie Unit-Tests verkomplizieren. Bevorzugen Sie Konstruktor-Injektion (wie im Skeleton + Dice-Setup) für Services und Konfiguration, die Controller benötigen.

Fehler und Ausnahmen

Alle Fehler und Ausnahmen werden von Flight abgefangen und an die error-Methode übergeben, wenn flight.handle_errors auf true gesetzt ist.

Das Standardverhalten besteht darin, eine generische HTTP 500 Internal Server Error-Antwort mit einigen Fehlerinformationen zu senden.

Sie können dieses Verhalten für Ihre eigenen Bedürfnisse überschreiben:

Flight::map('error', function (Throwable $error) {
  // Fehler behandeln
  echo $error->getTraceAsString();
});

Standardmäßig werden Fehler nicht im Webserverprotokoll aufgezeichnet. Sie können dies aktivieren, indem Sie die Konfiguration ändern:

Flight::set('flight.log_errors', true);

404 Nicht gefunden

Wenn eine URL nicht gefunden werden kann, ruft Flight die notFound-Methode auf. Das Standardverhalten besteht darin, eine HTTP 404 Not Found-Antwort mit einer einfachen Meldung zu senden.

Sie können dieses Verhalten für Ihre eigenen Bedürfnisse überschreiben:

Flight::map('notFound', function () {
  // Nicht gefunden behandeln
});

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/json

JSON Wrapper

Übersicht

Die Json-Klasse in Flight bietet eine einfache, konsistente Möglichkeit, JSON-Daten in Ihrer Anwendung zu kodieren und zu dekodieren. Sie umhüllt die nativen JSON-Funktionen von PHP mit besserer Fehlerbehandlung und einigen hilfreichen Standardeinstellungen, was die Arbeit mit JSON einfacher und sicherer macht.

Verständnis

Die Arbeit mit JSON ist in modernen PHP-Apps sehr üblich, insbesondere beim Aufbau von APIs oder der Behandlung von AJAX-Anfragen. Die Json-Klasse zentralisiert alle Ihre JSON-Kodierungen und -Dekodierungen, sodass Sie sich keine Gedanken über seltsame Randfälle oder kryptische Fehler aus den integrierten Funktionen von PHP machen müssen.

Wichtige Funktionen:

Grundlegende Verwendung

Daten zu JSON kodieren

Um PHP-Daten in einen JSON-String umzuwandeln, verwenden Sie Json::encode():

use flight\util\Json;

$data = [
  'framework' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$json = Json::encode($data);
echo $json;
// Ausgabe: {"framework":"Flight","version":3,"features":["routing","views","extending"]}

Falls die Kodierung fehlschlägt, erhalten Sie eine Ausnahme mit einer hilfreichen Fehlermeldung.

Schöne Ausgabe

Möchten Sie, dass Ihr JSON lesbar für Menschen ist? Verwenden Sie prettyPrint():

echo Json::prettyPrint($data);
/*
{
  "framework": "Flight",
  "version": 3,
  "features": [
    "routing",
    "views",
    "extending"
  ]
}
*/

JSON-Strings dekodieren

Um einen JSON-String zurück in PHP-Daten umzuwandeln, verwenden Sie Json::decode():

$json = '{"framework":"Flight","version":3}';
$data = Json::decode($json);
echo $data->framework; // Ausgabe: Flight

Wenn Sie ein assoziatives Array anstelle eines Objekts möchten, übergeben Sie true als zweiten Argument:

$data = Json::decode($json, true);
echo $data['framework']; // Ausgabe: Flight

Falls die Dekodierung fehlschlägt, erhalten Sie eine Ausnahme mit einer klaren Fehlermeldung.

JSON validieren

Überprüfen Sie, ob ein String gültiges JSON ist:

if (Json::isValid($json)) {
  // Es ist gültig!
} else {
  // Kein gültiges JSON
}

Letzten Fehler abrufen

Wenn Sie die letzte JSON-Fehlermeldung überprüfen möchten (aus den nativen PHP-Funktionen):

$error = Json::getLastError();
if ($error !== '') {
  echo "Letzter JSON-Fehler: $error";
}

Erweiterte Verwendung

Sie können Kodierungs- und Dekodierungsoptionen anpassen, wenn Sie mehr Kontrolle benötigen (siehe PHP's json_encode-Optionen):

// Kodieren mit HEX_TAG-Option
$json = Json::encode($data, JSON_HEX_TAG);

// Dekodieren mit benutzerdefinierter Tiefe
$data = Json::decode($json, false, 1024);

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/ai

KI & Entwicklererfahrung mit Flight

Übersicht

Flight ist dafür konzipiert, mit KI-Programmierwerkzeugen zu arbeiten – nicht gegen sie. Eine kleine, vorhersehbare API, ein klares App-Layout im offiziellen Skeleton und projektspezifische Anweisungsdateien bedeuten, dass Assistenten wie GitHub Copilot, Cursor, Windsurf, Claude Code und Gemini dieselben Muster befolgen können, die du von Hand schreiben würdest.

Mit eingebauten Runway-Befehlen zum Verbinden mit LLM-Anbietern und zum Generieren von Projektanweisungen hilft Flight dir und deinem Team, konsistente, relevante Hilfe zu erhalten, ohne denselben Kontext in jeden Chat einzufügen.

Verständnis

KI-Programmierassistenten sind am hilfreichsten, wenn sie den Kontext, die Konventionen und die Ziele deines Projekts verstehen. Die KI-Helfer von Flight ermöglichen dir:

Diese Funktionen sind in der Flight-Core-CLI enthalten (über Runway) und im offiziellen flightphp/skeleton Starter vorkonfiguriert.

Was das Skeleton für KI mitbringt

Der offizielle Starter behandelt AGENTS.md als die Quelle der Wahrheit für KI-Tools:

Datei Rolle
AGENTS.md (Projektwurzel) Globale Regeln, Boot-Ablauf, Namensräume, DI, „Was man nicht tun sollte“
Bereichsbezogene AGENTS.md unter app/, migrations/, tests/ usw. Kompakte, ordnerspezifische Tipps, wenn du in diesem Verzeichnisbaum arbeitest
SECURITY.md Geheimnisse, Header, XSS/SQL, Meldung – Sicherheit bleibt bewusst getrennt

Es gibt keine separate House-Style-Datei für Copilot / Cursor / Gemini / Windsurf im Skeleton. Weise deinen Assistenten auf die AGENTS.md im Wurzelverzeichnis an (und lass ihn Links zu bereichsbezogenen Dateien folgen). Menschen können diese Dateien komplett ignorieren und die README verwenden; das Layout ist in beiden Fällen dasselbe.

Dokumente lehren APIs; das Skeleton lehrt Layout. Kurze Flight::-Beispiele in diesen Dokumenten sind hervorragend zum Lernen. In einer Skeleton-App bevorzuge App\…-Klassen, Konstruktorinjektion und $this->app gegenüber der statischen Fassade in Controllern. Siehe Installation und Autoloading.

Grundlegende Verwendung

Einrichten der LLM-Anmeldeinformationen

Der Befehl ai:init führt dich durch das Verbinden deines Projekts mit einem LLM-Anbieter.

php runway ai:init

Du wirst aufgefordert:

Dies erstellt die Anmeldeinformationen, die für spätere LLM-Anfragen verwendet werden (zum Beispiel zum Generieren von Anweisungen).

Beispiel:

Welcome to AI Init!
Which LLM API do you want to use? [1] openai, [2] grok, [3] claude: 1
Enter the base URL for the LLM API [https://api.openai.com]:
Enter your API key for openai: sk-...
Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credentials saved to .runway-creds.json

Generieren projektspezifischer KI-Anweisungen

Der Befehl ai:generate-instructions erstellt oder aktualisiert Anweisungen für KI-Programmierassistenten, zugeschnitten auf dein Projekt.

php runway ai:generate-instructions

Du beantwortest ein paar Fragen (Beschreibung, Datenbank, Templating, Sicherheit, Teamgröße usw.). Flight verwendet deinen LLM-Anbieter, um Anweisungen zu generieren, und schreibt sie hauptsächlich in:

Je nach CLI-Version und Optionen kann der Befehl auch toolspezifische Kopien für ältere Workflows schreiben (zum Beispiel Copilot-, Cursor-, Windsurf- oder Gemini-Regeldateien). Behandle bei neuen Projekten aus dem Skeleton AGENTS.md (plus alle bereichsbezogenen AGENTS.md-Dateien, die du unter app/ behältst) als einzige Quelle der Wahrheit – pflege nicht fünf abweichende Anweisungsdateien von Hand.

Beispiel:

Please describe what your project is for? My awesome API
What database are you planning on using? MySQL
What HTML templating engine will you plan on using (if any)? twig
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.

Jetzt können KI-Tools Code vorschlagen, der zu deinem tatsächlichen Stack und Layout passt – nicht zu einem generischen PHP-Tutorial.

Fortgeschrittene Verwendung

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/uploaded_file

Uploaded File Handler

Übersicht

Die UploadedFile-Klasse in Flight erleichtert es, Datei-Uploads in Ihrer Anwendung sicher und einfach zu handhaben. Sie umschließt die Details des PHP-Datei-Upload-Prozesses und bietet Ihnen eine einfache, objektorientierte Möglichkeit, auf Dateiinformationen zuzugreifen und hochgeladene Dateien zu verschieben.

Verständnis

Wenn ein Benutzer eine Datei über ein Formular hochlädt, speichert PHP Informationen über die Datei in der $_FILES-Superglobal. In Flight interagieren Sie selten direkt mit $_FILES. Stattdessen stellt das Request-Objekt von Flight (erreichbar über Flight::request()) eine Methode getUploadedFiles() bereit, die ein Array von UploadedFile-Objekten zurückgibt, was den Datei-Handling viel bequemer und robuster macht.

Die UploadedFile-Klasse bietet Methoden zum:

Diese Klasse hilft Ihnen, gängige Fallstricke bei Datei-Uploads zu vermeiden, wie z. B. das Handhaben von Fehlern oder das sichere Verschieben von Dateien.

Grundlegende Verwendung

Zugriff auf hochgeladene Dateien aus einer Anfrage

Der empfohlene Weg, um auf hochgeladene Dateien zuzugreifen, ist über das Request-Objekt:

Flight::route('POST /upload', function() {
    // Für ein Formularfeld namens <input type="file" name="myFile">
    $uploadedFiles = Flight::request()->getUploadedFiles();
    $file = $uploadedFiles['myFile'];

    // Nun können Sie die UploadedFile-Methoden verwenden
    if ($file->getError() === UPLOAD_ERR_OK) {
        $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
        echo "Datei erfolgreich hochgeladen!";
    } else {
        echo "Upload fehlgeschlagen: " . $file->getError();
    }
});

Handhabung mehrerer Datei-Uploads

Wenn Ihr Formular name="myFiles[]" für mehrere Uploads verwendet, erhalten Sie ein Array von UploadedFile-Objekten:

Flight::route('POST /upload', function() {
    // Für ein Formularfeld namens <input type="file" name="myFiles[]">
    $uploadedFiles = Flight::request()->getUploadedFiles();
    foreach ($uploadedFiles['myFiles'] as $file) {
        if ($file->getError() === UPLOAD_ERR_OK) {
            $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
            echo "Hochgeladen: " . $file->getClientFilename() . "<br>";
        } else {
            echo "Upload fehlgeschlagen: " . $file->getClientFilename() . "<br>";
        }
    }
});

Manuelles Erstellen einer UploadedFile-Instanz

Normalerweise erstellen Sie keine UploadedFile manuell, aber Sie können es tun, wenn es benötigt wird:

use flight\net\UploadedFile;

$file = new UploadedFile(
  $_FILES['myfile']['name'],
  $_FILES['myfile']['type'],
  $_FILES['myfile']['size'],
  $_FILES['myfile']['tmp_name'],
  $_FILES['myfile']['error']
);

Zugriff auf Dateiinformationen

Sie können leicht Details über die hochgeladene Datei abrufen:

echo $file->getClientFilename();   // Ursprünglicher Dateiname vom Computer des Benutzers
echo $file->getClientMediaType();  // MIME-Typ (z. B. image/png)
echo $file->getSize();             // Dateigröße in Bytes
echo $file->getTempName();         // Temporärer Dateipfad auf dem Server
echo $file->getError();            // Upload-Fehlercode (0 bedeutet kein Fehler)

Verschieben der hochgeladenen Datei

Nach der Validierung der Datei verschieben Sie sie an einen permanenten Speicherort:

try {
  $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
  echo "Datei erfolgreich hochgeladen!";
} catch (Exception $e) {
  echo "Upload fehlgeschlagen: " . $e->getMessage();
}

Die moveTo()-Methode wirft eine Exception, wenn etwas schiefgeht (wie ein Upload-Fehler oder ein Berechtigungsproblem).

Handhabung von Upload-Fehlern

Wenn es während des Uploads ein Problem gab, können Sie eine lesbare Fehlermeldung abrufen:

if ($file->getError() !== UPLOAD_ERR_OK) {
  // Sie können den Fehlercode verwenden oder die Exception von moveTo() abfangen
  echo "Es gab einen Fehler beim Hochladen der Datei.";
}

Siehe auch

Fehlerbehebung

Changelog

Learn/unit_testing_and_solid_principles

Dieser Artikel wurde ursprünglich 2015 auf Airpair veröffentlicht. Alle Credits gehen an Airpair und Brian Fenton, der den Artikel ursprünglich geschrieben hat, obwohl die Website nicht mehr verfügbar ist und der Artikel nur in der Wayback Machine existiert. Dieser Artikel wurde der Seite zu Lern- und Bildungszwecken für die PHP-Community hinzugefügt.

1 Einrichtung und Konfiguration

1.1 Aktuell bleiben

Lassen Sie uns das von Anfang an klären – eine deprimierend kleine Anzahl von PHP-Installationen in der Praxis ist aktuell oder wird aktuell gehalten. Ob das auf Einschränkungen bei Shared-Hosting, Standardeinstellungen, die niemand ändert, oder auf fehlender Zeit/Budget für Upgradetests zurückzuführen ist, die bescheidenen PHP-Binaries werden oft zurückgelassen. Eine klare Best Practice, die mehr Betonung verdient, ist daher, immer eine aktuelle Version von PHP zu verwenden (5.6.x zum Zeitpunkt dieses Artikels). Darüber hinaus ist es wichtig, regelmäßige Upgrades sowohl von PHP selbst als auch von Erweiterungen oder Vendor-Bibliotheken durchzuführen. Upgrades bringen neue Sprachfunktionen, verbesserte Geschwindigkeit, geringeren Speicherverbrauch und Sicherheitsupdates. Je häufiger Sie upgraden, desto weniger schmerzhaft wird der Prozess.

1.2 Sinnvolle Standardeinstellungen

PHP macht einen anständigen Job, gute Standardeinstellungen mit seinen Dateien php.ini.development und php.ini.production vorzunehmen, aber wir können es besser machen. Zum einen legen sie keine Datums-/Zeitzone für uns fest. Das ergibt Sinn aus Sicht der Distribution, aber ohne eine wird PHP einen E_WARNING-Fehler auslösen, wann­ever wir eine datums-/zeitbezogene Funktion aufrufen. Hier sind einige empfohlene Einstellungen:

1.3 Erweiterungen

Es ist auch eine gute Idee, Erweiterungen zu deaktivieren (oder zumindest nicht zu aktivieren), die Sie nicht verwenden, wie Datenbank-Treiber. Um zu sehen, was aktiviert ist, führen Sie den phpinfo()-Befehl aus oder gehen Sie zur Kommandozeile und führen Sie das aus.

$ php -i

Die Informationen sind die gleichen, aber phpinfo() hat HTML-Formatierung hinzugefügt. Die CLI-Version ist einfacher zu pipen und mit grep zu filtern, um spezifische Informationen zu finden. Zum Beispiel:

$ php -i | grep error_log

Ein Haken bei dieser Methode: Es ist möglich, dass unterschiedliche PHP-Einstellungen für die webseitige Version und die CLI-Version gelten.

2 Composer verwenden

Das könnte überraschen, aber eine der besten Praktiken für modernes PHP-Schreiben ist, weniger davon zu schreiben. Obwohl es wahr ist, dass man, um gut zu programmieren, programmieren muss, gibt es eine große Anzahl von Problemen, die im PHP-Bereich bereits gelöst wurden, wie Routing, grundlegende Input-Validierungsbibliotheken, Einheitenumwandlung, Datenbank-Abstraktionsschichten usw. Schauen Sie einfach auf Packagist und stöbern Sie herum. Sie werden wahrscheinlich feststellen, dass erhebliche Teile des Problems, das Sie lösen möchten, bereits geschrieben und getestet wurden.

Obwohl es verlockend ist, den gesamten Code selbst zu schreiben (und es ist nichts Falsches daran, Ihren eigenen Framework oder Ihre eigene Bibliothek als Lernerfahrung zu schreiben), sollten Sie gegen diese Gefühle von „Nicht von mir erfunden“ ankämpfen und sich Zeit und Kopfschmerzen sparen. Folgen Sie stattdessen der Doktrin von PIE – Proudly Invented Elsewhere. Und wenn Sie sich entscheiden, Ihr eigenes Etwas zu schreiben, veröffentlichen Sie es nicht, es sei denn, es tut etwas signifikant anderes oder Besseres als bestehende Angebote.

Composer ist ein Paketmanager für PHP, ähnlich wie pip in Python, gem in Ruby und npm in Node. Es ermöglicht Ihnen, eine JSON-Datei zu definieren, die die Abhängigkeiten Ihres Codes auflistet, und es wird versuchen, diese Anforderungen zu erledigen, indem es die notwendigen Code-Bundles herunterlädt und installiert.

2.1 Composer installieren

Wir gehen davon aus, dass dies ein lokales Projekt ist, also installieren wir eine Instanz von Composer nur für das aktuelle Projekt. Navigieren Sie zu Ihrem Projektverzeichnis und führen Sie das aus:

$ curl -sS https://getcomposer.org/installer | php

Beachten Sie, dass das Pipen eines Downloads direkt in einen Skript-Interpreter (sh, ruby, php usw.) ein Sicherheitsrisiko darstellt, also lesen Sie den Installationscode und stellen Sie sicher, dass Sie damit einverstanden sind, bevor Sie einen solchen Befehl ausführen.

Aus Gründen der Bequemlichkeit (wenn Sie lieber composer install tippen als php composer.phar install), können Sie diesen Befehl verwenden, um eine einzelne Kopie von Composer global zu installieren:

$ mv composer.phar /usr/local/bin/composer
$ chmod +x composer

Sie müssen diese möglicherweise mit sudo ausführen, je nach Ihren Dateiberechtigungen.

2.2 Composer verwenden

Composer hat zwei Hauptkategorien von Abhängigkeiten, die es verwalten kann: „require“ und „require-dev“. Abhängigkeiten, die als „require“ aufgelistet sind, werden überall installiert, aber „require-dev“-Abhängigkeiten werden nur installiert, wenn sie explizit angefordert werden. Normalerweise handelt es sich dabei um Tools für die aktive Entwicklung, wie PHP_CodeSniffer. Die Zeile unten zeigt ein Beispiel, wie man Guzzle installiert, eine beliebte HTTP-Bibliothek.

$ php composer.phar require guzzle/guzzle

Um ein Tool nur für Entwicklungszwecke zu installieren, fügen Sie die --dev-Flag hinzu:

$ php composer.phar require --dev 'sebastian/phpcpd'

Das installiert PHP Copy-Paste Detector, ein weiteres Code-Qualitäts-Tool als Entwicklungs-abhängigkeit.

2.3 Install vs. Update

Wenn wir composer install das erste Mal ausführen, installiert es alle Bibliotheken und ihre Abhängigkeiten, basierend auf der composer.json-Datei. Wenn das erledigt ist, erstellt Composer eine Lock-Datei, passend benannt composer.lock. Diese Datei enthält eine Liste der Abhängigkeiten, die Composer für uns gefunden hat, und ihre genauen Versionen mit Hashes. Jedes Mal, wenn wir composer install in Zukunft ausführen, schaut es in die Lock-Datei und installiert genau diese Versionen.

composer update ist ein bisschen anders. Es ignoriert die composer.lock-Datei (falls vorhanden) und versucht, die neuesten Versionen jeder Abhängigkeit zu finden, die immer noch den Einschränkungen in composer.json entsprechen. Es schreibt dann eine neue composer.lock-Datei, wenn es fertig ist.

2.4 Autoloading

Sowohl composer install als auch composer update generieren einen Autoloader für uns, der PHP sagt, wo es alle notwendigen Dateien für die Bibliotheken findet, die wir gerade installiert haben. Um ihn zu verwenden, fügen Sie einfach diese Zeile hinzu (normalerweise zu einer Bootstrap-Datei, die bei jeder Anfrage ausgeführt wird):

require 'vendor/autoload.php';

3 Gute Designprinzipien befolgen

3.1 SOLID

SOLID ist ein Akronym, das uns an fünf Schlüsselprinzipien im guten objektorientierten Software-Design erinnert.

3.1.1 S - Single Responsibility Principle

Das besagt, dass Klassen nur eine Verantwortung haben sollten, oder anders ausgedrückt, sie sollten nur einen Grund zum Ändern haben. Das passt gut zur Unix-Philosophie von vielen kleinen Tools, die eine Sache gut machen. Klassen, die nur eine Sache tun, sind viel einfacher zu testen und zu debuggen und überraschen Sie weniger. Sie wollen nicht, dass ein Methodenaufruf zu einer Validator-Klasse DB-Datensätze aktualisiert. Hier ist ein Beispiel für eine Verletzung des SRP, wie man es in einer Anwendung basierend auf dem ActiveRecord-Pattern häufig sieht.

class Person extends Model
{
    public $name;
    public $birthDate;
    protected $preferences;
    public function getPreferences() {}
    public function save() {}
}

Das ist ein ziemlich grundlegendes Entity-Modell. Eines dieser Dinge gehört hier nicht hin. Die einzige Verantwortung eines Entity-Modells sollte das Verhalten sein, das mit der Entität zusammenhängt, die es repräsentiert, es sollte nicht für seine eigene Persistenz verantwortlich sein.

class Person extends Model
{
    public $name;
    public $birthDate;
    protected $preferences;
    public function getPreferences() {}
}
class DataStore
{
    public function save(Model $model) {}
}

Das ist besser. Das Person-Modell ist wieder nur eine Sache, und das Save-Verhalten wurde zu einem Persistenz-Objekt verschoben. Beachten Sie auch, dass ich nur auf Model getippt habe, nicht auf Person. Wir kommen darauf zurück, wenn wir zu den L- und D-Teilen von SOLID kommen.

3.1.2 O - Open Closed Principle

Es gibt einen tollen Test dafür, der ziemlich genau zusammenfasst, worum es bei diesem Prinzip geht: Denken Sie an eine Funktion, die Sie implementieren sollen, wahrscheinlich die neueste, an der Sie gearbeitet haben oder arbeiten. Können Sie diese Funktion in Ihrem bestehenden Codebasis SOLELY implementieren, indem Sie neue Klassen hinzufügen und keine bestehenden Klassen in Ihrem System ändern? Ihre Konfiguration und Verkabelungscode bekommt ein bisschen Nachsicht, aber in den meisten Systemen ist das überraschend schwierig. Sie müssen sich stark auf polymorphe Dispatch verlassen und die meisten Codebasen sind nicht dafür eingerichtet. Wenn Sie daran interessiert sind, gibt es einen guten Google-Talk auf YouTube über Polymorphismus und Code-Schreiben ohne Ifs, der das weiter ausführt. Als Bonus wird der Talk von Miško Hevery gehalten, den viele als den Erfinder von AngularJs kennen.

3.1.3 L - Liskov Substitution Principle

Dieses Prinzip ist nach Barbara Liskov benannt und lautet wie folgt:

„Objekte in einem Programm sollten durch Instanzen ihrer Untertypen ersetzbar sein, ohne die Korrektheit dieses Programms zu ändern.“

Das klingt alles gut und schön, aber es wird klarer illustriert mit einem Beispiel.

abstract class Shape
{
    public function getHeight();
    public function setHeight($height);
    public function getLength();
    public function setLength($length);
}

Das wird unsere grundlegende vierseitige Form darstellen. Nichts Ausgefallenes hier.

class Square extends Shape
{
    protected $size;
    public function getHeight() {
        return $this->size;
    }
    public function setHeight($height) {
        $this->size = $height;
    }
    public function getLength() {
        return $this->size;
    }
    public function setLength($length) {
        $this->size = $length;
    }
}

Hier ist unsere erste Form, das Quadrat. Eine ziemlich unkomplizierte Form, oder? Sie können annehmen, dass es einen Konstruktor gibt, in dem wir die Dimensionen festlegen, aber Sie sehen hier aus dieser Implementierung, dass Länge und Höhe immer gleich sein werden. Quadrate sind einfach so.

class Rectangle extends Shape
{
    protected $height;
    protected $length;
    public function getHeight() {
        return $this->height;
    }
    public function setHeight($height) {
        $this->height = $height;
    }
    public function getLength() {
        return $this->length;
    }
    public function setLength($length) {
        $this->length = $length;
    }
}

Also haben wir hier eine andere Form. Sie hat immer noch die gleichen Methodensignaturen, es ist immer noch eine vierseitige Form, aber was, wenn wir anfangen, sie gegeneinander zu verwenden? Plötzlich, wenn wir die Höhe unserer Shape ändern, können wir nicht mehr annehmen, dass die Länge unserer Shape übereinstimmt. Wir haben den Vertrag verletzt, den wir mit dem Benutzer hatten, als wir ihm unsere Square-Form gaben.

Das ist ein Lehrbuchbeispiel für eine Verletzung des LSP, und wir brauchen ein solches Prinzip, um das Beste aus einem Typsystem zu machen. Sogar Duck Typing wird uns nicht sagen, ob das zugrunde liegende Verhalten anders ist, und da wir das nicht wissen können, ohne dass es bricht, ist es am besten, sicherzustellen, dass es nicht anders ist.

3.1.3 I - Interface Segregation Principle

Dieses Prinzip sagt, dass man vielen kleinen, feingliedrigen Interfaces den Vorzug geben sollte, im Vergleich zu einem großen. Interfaces sollten auf Verhalten basieren und nicht auf „es ist eine dieser Klassen“. Denken Sie an Interfaces, die mit PHP kommen. Traversable, Countable, Serializable, Dinge wie das. Sie werben für Fähigkeiten, die das Objekt besitzt, nicht für das, wovon es erbt. Halten Sie Ihre Interfaces also klein. Sie wollen kein Interface mit 30 Methoden darauf, 3 ist ein viel besseres Ziel.

3.1.4 D - Dependency Inversion Principle

Sie haben das wahrscheinlich an anderen Stellen gehört, die über Dependency Injection gesprochen haben, aber Dependency Inversion und Dependency Injection sind nicht ganz dasselbe. Dependency Inversion ist wirklich nur eine Möglichkeit zu sagen, dass Sie auf Abstraktionen in Ihrem System und nicht auf seine Details angewiesen sein sollten. Was bedeutet das für Sie im Alltag?

Verwenden Sie nicht direkt mysqli_query() überall in Ihrem Code, verwenden Sie stattdessen etwas wie DataStore->query().

Der Kern dieses Prinzips geht eigentlich um Abstraktionen. Es geht mehr darum zu sagen „verwenden Sie einen Datenbank-Adapter“, anstatt auf direkte Aufrufe wie mysqli_query zu vertrauen. Wenn Sie mysqli_query direkt in der Hälfte Ihrer Klassen verwenden, binden Sie alles direkt an Ihre Datenbank. Nichts für oder gegen MySQL hier, aber wenn Sie mysqli_query verwenden, sollte diese Art von niedrigstufigem Detail in nur einem Ort versteckt werden und dann diese Funktionalität über eine generische Wrapper freigegeben werden.

Ich weiß, das ist ein bisschen ein abgedroschener Beispiel, wenn man drüber nachdenkt, weil die Anzahl der Male, in denen Sie Ihren Datenbank-Engine vollständig ändern werden, nachdem Ihr Produkt in Produktion ist, sehr, sehr niedrig ist. Ich habe es gewählt, weil ich dachte, die Leute wären mit der Idee aus ihrem eigenen Code vertraut. Auch, selbst wenn Sie eine Datenbank haben, bei der Sie bleiben, ermöglicht Ihnen dieses abstrakte Wrapper-Objekt, Fehler zu beheben, Verhalten zu ändern oder Funktionen zu implementieren, die Sie sich von Ihrer gewählten Datenbank wünschen. Es macht auch Unit-Testing möglich, wo niedrigstufige Aufrufe das nicht tun würden.

4 Object Calisthenics

Das ist kein voller Einstieg in diese Prinzipien, aber die ersten zwei sind leicht zu merken, bieten guten Wert und können sofort auf fast jeden Codebase angewendet werden.

4.1 Nicht mehr als eine Ebene der Einrückung pro Methode

Das ist eine hilfreiche Möglichkeit, Methoden in kleinere Chunks zu zerlegen, was zu Code führt, der klarer und selbstdokumentierender ist. Je mehr Ebenen der Einrückung Sie haben, desto mehr tut die Methode und desto mehr Zustand müssen Sie im Kopf behalten, während Sie damit arbeiten.

Sofort weiß ich, dass Leute dagegen einwenden werden, aber das ist nur eine Richtlinie/Heuristik, keine harte und schnelle Regel. Ich erwarte nicht, dass jemand PHP_CodeSniffer-Regeln dafür durchsetzt (obwohl Leute das getan haben).

Lassen Sie uns ein schnelles Beispiel durchgehen, wie das aussehen könnte:

public function transformToCsv($data)
{
    $csvLines = array();
    $csvLines[] = implode(',', array_keys($data[0]));
    foreach ($data as $row) {
        if (!$row) {
            continue;
        }
        $csvLines[] = implode(',', $row);
    }
    return $csvLines;
}

Obwohl das technisch korrekter, testbarer usw. Code ist, können wir viel mehr tun, um das klarer zu machen. Wie reduzieren wir die Ebenen der Verschachtelung hier?

Wir wissen, dass wir den Inhalt der foreach-Schleife stark vereinfachen müssen (oder sie ganz entfernen), also beginnen wir da.

if (!$row) {
    continue;
}

Das erste Bit ist einfach. Das ignoriert nur leere Zeilen. Wir können diesen gesamten Prozess abkürzen, indem wir eine eingebaute PHP-Funktion verwenden, bevor wir überhaupt zur Schleife kommen.

$data = array_filter($data);
foreach ($data as $row) {
    $csvLines[] = implode(',', $row);
}

Jetzt haben wir unsere einzelne Ebene der Verschachtelung. Aber wenn man sich das ansieht, tun wir nichts anderes, als eine Funktion auf jedes Element in einem Array anzuwenden. Wir brauchen nicht einmal die foreach-Schleife dafür.

$data = array_filter($data);
$csvLines = array_map(function($row) {
    return implode(',', $row);
}, $data);

Jetzt haben wir gar keine Verschachtelung mehr, und der Code wird wahrscheinlich schneller sein, da wir alle Schleifen mit nativen C-Funktionen anstelle von PHP machen. Wir müssen ein bisschen Trickserei betreiben, um das Komma an implode zu übergeben, also könnte man argumentieren, dass der Stopp beim vorherigen Schritt viel verständlicher ist.

4.2 Versuchen Sie, else nicht zu verwenden

Das behandelt wirklich zwei Hauptideen. Die erste ist mehrere Return-Anweisungen aus einer Methode. Wenn Sie genug Informationen haben, um eine Entscheidung über das Ergebnis der Methode zu treffen, treffen Sie diese Entscheidung und returnen Sie. Die zweite ist eine Idee, die als Guard Clauses bekannt ist. Das sind im Wesentlichen Validierungsprüfungen kombiniert mit frühen Returns, normalerweise ganz oben in einer Methode. Lassen Sie mich Ihnen zeigen, was ich meine.

public function addThreeInts($first, $second, $third) {
    if (is_int($first)) {
        if (is_int($second)) {
            if (is_int($third)) {
                $sum = $first + $second + $third;
            } else {
                return null;
            }
        } else {
            return null;
        }
    } else {
        return null;
    }
    return $sum;
}

Das ist wieder ziemlich unkompliziert, es addiert 3 Integers und gibt das Ergebnis zurück oder null, wenn irgendeiner der Parameter kein Integer ist. Wenn man davon absieht, dass wir all diese Prüfungen in eine einzelne Zeile mit AND-Operatoren kombinieren könnten, denke ich, Sie können sehen, wie die verschachtelte if/else-Struktur den Code schwerer zu folgen macht. Schauen Sie sich stattdessen dieses Beispiel an.

public function addThreeInts($first, $second, $third) {
    if (!is_int($first)) {
        return null;
    }
    if (!is_int($second)) {
        return null;
    }
    if (!is_int($third)) {
        return null;
    }
    return $first + $second + $third;
}

Für mich ist dieses Beispiel viel einfacher zu folgen. Hier verwenden wir Guard Clauses, um unsere anfänglichen Annahmen über die Parameter zu überprüfen und die Methode sofort zu verlassen, wenn sie nicht bestehen. Wir haben auch keine Zwischenvariable mehr, um die Summe durch die Methode zu verfolgen. In diesem Fall haben wir überprüft, dass wir bereits auf dem happy path sind, und können einfach tun, wofür wir hier sind. Wieder könnten wir all diese Prüfungen in einem if machen, aber das Prinzip sollte klar sein.

5 Unit-Testing

Unit-Testing ist die Praxis, kleine Tests zu schreiben, die Verhalten in Ihrem Code überprüfen. Sie werden fast immer in derselben Sprache wie der Code (in diesem Fall PHP) geschrieben und sind so schnell gedacht, dass sie jederzeit ausgeführt werden können. Sie sind extrem wertvoll als Tool, um Ihren Code zu verbessern. Neben den offensichtlichen Vorteilen, sicherzustellen, dass Ihr Code tut, was Sie denken, dass er tut, kann Unit-Testing auch sehr nützliches Design-Feedback geben. Wenn ein Stück Code schwer zu testen ist, zeigt das oft Designprobleme auf. Sie geben Ihnen auch ein Sicherheitsnetz gegen Regressionen und ermöglichen es Ihnen, viel öfter zu refactorisieren und Ihren Code zu einer saubereren Design zu entwickeln.

5.1 Tools

Es gibt mehrere Unit-Testing-Tools in PHP, aber mit Abstand das häufigste ist PHPUnit. Sie können es installieren, indem Sie eine PHAR-Datei direkt herunterladen oder es mit Composer installieren. Da wir Composer für alles andere verwenden, zeigen wir diese Methode. Da PHPUnit wahrscheinlich nicht in Produktion deployed wird, können wir es als Dev-Abhängigkeit mit dem folgenden Befehl installieren:

composer require --dev phpunit/phpunit

5.2 Tests sind eine Spezifikation

Die wichtigste Rolle von Unit-Tests in Ihrem Code ist es, eine ausführbare Spezifikation zu bieten, was der Code tun soll. Selbst wenn der Testcode falsch ist oder der Code Fehler hat, ist das Wissen, was das System soll tun, unbezahlbar.

5.3 Schreiben Sie Ihre Tests zuerst

Wenn Sie die Chance hatten, einen Satz Tests zu sehen, der vor dem Code geschrieben wurde, und einen, der nach dem Code geschrieben wurde, sind sie auffallend unterschiedlich. Die „nach“-Tests sind viel mehr mit den Implementierungsdetails der Klasse beschäftigt und stellen sicher, dass sie gute Zeilenumfänge haben, während die „vor“-Tests mehr darum gehen, das gewünschte externe Verhalten zu überprüfen. Das ist wirklich das, was uns mit Unit-Tests interessiert, nämlich sicherzustellen, dass die Klasse das richtige Verhalten zeigt. Auf Implementierung fokussierte Tests machen Refactoring tatsächlich schwieriger, weil sie brechen, wenn die Interna der Klassen ändern, und Sie haben sich gerade die Vorteile der Informationsversteckung in OOP gekostet.

5.4 Was ein guter Unit-Test ausmacht

Gute Unit-Tests teilen viele der folgenden Merkmale:

Es gibt Gründe, gegen einige davon zu gehen, aber als allgemeine Richtlinien werden sie Ihnen gut dienen.

5.5 Wenn Testing schmerzhaft ist

Unit-Testing zwingt Sie, den Schmerz eines schlechten Designs vorneweg zu spüren – Michael Feathers

Wenn Sie Unit-Tests schreiben, zwingen Sie sich, die Klasse tatsächlich zu verwenden, um Dinge zu erledigen. Wenn Sie Tests am Ende schreiben oder, schlimmer noch, den Code einfach über die Wand für QA oder wen auch immer werfen, um Tests zu schreiben, bekommen Sie kein Feedback darüber, wie sich die Klasse tatsächlich verhält. Wenn wir Tests schreiben und die Klasse ein echtes Problem ist, finden wir das heraus, während wir sie schreiben, was fast die günstigste Zeit ist, es zu beheben.

Wenn eine Klasse schwer zu testen ist, ist das ein Designfehler. Verschiedene Fehler manifestieren sich auf unterschiedliche Weisen. Wenn Sie eine Menge Mocking machen müssen, hat Ihre Klasse wahrscheinlich zu viele Abhängigkeiten oder Ihre Methoden tun zu viel. Je mehr Setup Sie für jeden Test machen müssen, desto wahrscheinlicher ist es, dass Ihre Methoden zu viel tun. Wenn Sie wirklich komplizierte Test-Szenarien schreiben müssen, um Verhalten auszuführen, tun die Methoden der Klasse wahrscheinlich zu viel. Wenn Sie in eine Menge privater Methoden und Zustände eintauchen müssen, um Dinge zu testen, versucht vielleicht eine andere Klasse herauszukommen. Unit-Testing ist sehr gut darin, „Eisberg-Klassen“ aufzudecken, bei denen 80% dessen, was die Klasse tut, in geschütztem oder privatem Code versteckt ist. Ich war früher ein großer Fan davon, so viel wie möglich geschützt zu machen, aber jetzt habe ich erkannt, dass ich nur meine individuellen Klassen für zu viel verantwortlich gemacht habe, und die echte Lösung war, die Klasse in kleinere Stücke zu zerlegen.

Geschrieben von Brian Fenton – Brian Fenton ist seit 8 Jahren PHP-Entwickler im Mittleren Westen und in der Bay Area, derzeit bei Thismoment. Er konzentriert sich auf Code-Craftsmanship und Designprinzipien. Blog auf www.brianfenton.us, Twitter unter @brianfenton. Wenn er nicht beschäftigt ist, Vater zu sein, genießt er Essen, Bier, Gaming und Lernen.

Learn/flight_vs_another_framework

Vergleich von Flight mit einem anderen Framework

Wenn Sie von einem anderen Framework wie Laravel, Slim, Fat-Free oder Symfony zu Flight migrieren, hilft Ihnen diese Seite, die Unterschiede zwischen den beiden zu verstehen.

Laravel

Laravel ist ein funktionsreiches Framework mit allen Extras und einem erstaunlichen, auf Entwickler ausgerichteten Ökosystem, aber zu einem Preis in Leistung und Komplexität.

Sehen Sie den Vergleich zwischen Laravel und Flight.

Slim

Slim ist ein Micro-Framework, das Flight ähnelt. Es ist darauf ausgelegt, leichtgewichtig und einfach zu bedienen zu sein, kann aber etwas komplexer sein als Flight.

Sehen Sie den Vergleich zwischen Slim und Flight.

Fat-Free

Fat-Free ist ein Full-Stack-Framework in einem viel kleineren Paket. Obwohl es alle Werkzeuge im Werkzeugkasten hat, hat es eine Datenarchitektur, die einige Projekte komplexer machen kann, als sie sein müssen.

Sehen Sie den Vergleich zwischen Fat-Free und Flight.

Symfony

Symfony ist ein modulares Enterprise-Level-Framework, das darauf ausgelegt ist, flexibel und skalierbar zu sein. Für kleinere Projekte oder neuere Entwickler kann Symfony etwas überwältigend sein.

Sehen Sie den Vergleich zwischen Symfony und Flight.

Learn/flight_vs_laravel

Flight vs Laravel

Was ist Laravel?

Laravel ist ein vollständiges Framework mit allen Klingeln und Pfiffen und einem beeindruckenden, auf Entwickler fokussierten Ökosystem, aber zu Lasten von Leistung und Komplexität. Das Ziel von Laravel ist es, dass der Entwickler das höchste Maß an Produktivität erreicht und gängige Aufgaben einfach macht. Laravel ist eine großartige Wahl für Entwickler, die eine vollständige, unternehmensorientierte Webanwendung aufbauen möchten. Das geht mit einigen Kompromissen einher, speziell in Bezug auf Leistung und Komplexität. Der Einstieg in Laravel kann einfach sein, aber die Meisterschaft im Framework zu erlangen, kann einige Zeit in Anspruch nehmen.

Es gibt auch so viele Laravel-Module, dass Entwickler oft das Gefühl haben, der einzige Weg, Probleme zu lösen, sei die Nutzung dieser Module, obwohl man tatsächlich einfach eine andere Bibliothek verwenden oder eigenen Code schreiben könnte.

Vorteile im Vergleich zu Flight

Nachteile im Vergleich zu Flight

Learn/pdo_wrapper

PdoWrapper PDO-Hilfsklasse

WARNUNG

Veraltet: PdoWrapper ist veraltet ab Flight v3.18.0. Es wird in einer zukünftigen Version nicht entfernt, aber nur für Abwärtskompatibilität gewartet. Bitte verwenden Sie stattdessen SimplePdo, das die gleiche Funktionalität plus zusätzliche Hilfsmethoden für gängige Datenbankoperationen bietet.

Überblick

Die PdoWrapper-Klasse in Flight ist ein freundlicher Helfer für die Arbeit mit Datenbanken unter Verwendung von PDO. Sie vereinfacht gängige Datenbankaufgaben, fügt einige nützliche Methoden zum Abrufen von Ergebnissen hinzu und gibt Ergebnisse als Collections zurück, um einen einfachen Zugriff zu ermöglichen. Sie unterstützt auch Query-Logging und Application Performance Monitoring (APM) für fortgeschrittene Anwendungsfälle.

Verständnis

Die Arbeit mit Datenbanken in PHP kann etwas umständlich sein, insbesondere bei der direkten Verwendung von PDO. PdoWrapper erweitert PDO und fügt Methoden hinzu, die Abfragen, Abrufen und Behandeln von Ergebnissen viel einfacher machen. Statt mit Prepared Statements und Fetch-Modi zu jonglieren, erhalten Sie einfache Methoden für gängige Aufgaben, und jede Zeile wird als Collection zurückgegeben, sodass Sie Array- oder Objekt-Notation verwenden können.

Sie können PdoWrapper als geteilten Service in Flight registrieren und es dann überall in Ihrer App über Flight::db() verwenden.

Grundlegende Verwendung

Registrieren des PDO-Helpers

Zuerst registrieren Sie die PdoWrapper-Klasse bei Flight:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

Nun können Sie Flight::db() überall verwenden, um Ihre Datenbankverbindung zu erhalten.

Ausführen von Abfragen

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

Verwenden Sie dies für INSERTs, UPDATEs oder wenn Sie Ergebnisse manuell abrufen möchten:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row is an array
}

Sie können es auch für Schreibvorgänge verwenden:

$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);

fetchField()

function fetchField(string $sql, array $params = []): mixed

Holen Sie einen einzelnen Wert aus der Datenbank:

$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);

fetchRow()

function fetchRow(string $sql, array $params = []): Collection

Holen Sie eine einzelne Zeile als Collection (Array/Objekt-Zugriff):

$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// or
echo $user->name;

fetchAll()

function fetchAll(string $sql, array $params = []): array<Collection>

Holen Sie alle Zeilen als Array von Collections:

$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
    echo $user['name'];
    // or
    echo $user->name;
}

Verwendung von IN()-Platzhaltern

Sie können ein einzelnes ? in einer IN()-Klausel verwenden und ein Array oder einen komma-separierten String übergeben:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
// or
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", ['1,2,3']);

Fortgeschrittene Verwendung

Query-Logging & APM

Wenn Sie die Query-Leistung verfolgen möchten, aktivieren Sie das APM-Tracking bei der Registrierung:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* options */], true // letzter Parameter aktiviert APM
]);

Nach dem Ausführen von Abfragen können Sie sie manuell loggen, aber das APM loggt sie automatisch, wenn aktiviert:

Flight::db()->logQueries();

Dies löst ein Event (flight.db.queries) mit Verbindungs- und Query-Metriken aus, das Sie mit Flights Event-System abhören können.

Vollständiges Beispiel

Flight::route('/users', function () {
    // Get all users
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // Stream all users
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
    }

    // Get a single user
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // Get a single value
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // Special IN() syntax
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', ['1,2,3,4,5']);

    // Insert a new user
    Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
    $insert_id = Flight::db()->lastInsertId();

    // Update a user
    Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);

    // Delete a user
    Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);

    // Get the number of affected rows
    $statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
    $affected_rows = $statement->rowCount();
});

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/flight_vs_slim

Flight vs. Slim

Was ist Slim?

Slim ist ein PHP-Micro-Framework, das dir hilft, schnell einfache, aber leistungsstarke Webanwendungen und APIs zu erstellen.

Ein Großteil der Inspiration für einige der v3-Funktionen von Flight stammt tatsächlich von Slim. Das Gruppieren von Routen und das Ausführen von Middleware in einer bestimmten Reihenfolge sind zwei Funktionen, die von Slim inspiriert wurden. Slim v3 wurde mit Fokus auf Einfachheit veröffentlicht, aber es gibt gemischte Bewertungen bezüglich v4.

Vorteile im Vergleich zu Flight

Nachteile im Vergleich zu Flight

Learn/extending

Erweitern

Überblick

Flight ist so konzipiert, dass es ein erweiterbares Framework ist. Das Framework kommt mit einer Reihe von Standardmethoden und -komponenten, erlaubt es Ihnen jedoch, Ihre eigenen Methoden zuzuordnen, Ihre eigenen Klassen zu registrieren oder sogar bestehende Klassen und Methoden zu überschreiben.

Verständnis

Es gibt 2 Wege, wie Sie die Funktionalität von Flight erweitern können:

  1. Methoden zuordnen - Dies wird verwendet, um einfache benutzerdefinierte Methoden zu erstellen, die Sie von überall in Ihrer Anwendung aufrufen können. Diese werden typischerweise für Hilfsfunktionen verwendet, die Sie von überall in Ihrem Code aufrufen möchten.
  2. Klassen registrieren - Dies wird verwendet, um Ihre eigenen Klassen bei Flight zu registrieren. Dies wird typischerweise für Klassen verwendet, die Abhängigkeiten haben oder Konfiguration erfordern.

Sie können auch bestehende Framework-Methoden überschreiben, um ihr Standardverhalten zu ändern, um den Bedürfnissen Ihres Projekts besser zu entsprechen.

Wenn Sie nach einem DIC (Dependency Injection Container) suchen, schauen Sie auf der Dependency Injection Container-Seite vorbei.

Grundlegende Verwendung

Framework-Methoden überschreiben

Flight erlaubt es Ihnen, seine Standardfunktionalität zu überschreiben, um Ihren eigenen Bedürfnissen zu entsprechen, ohne Code zu modifizieren. Sie können alle überschreibbaren Methoden unten ansehen.

Zum Beispiel ruft Flight, wenn es eine URL nicht einer Route zuordnen kann, die notFound-Methode auf, die eine generische HTTP 404-Antwort sendet. Sie können dieses Verhalten überschreiben, indem Sie die map-Methode verwenden:

Flight::map('notFound', function() {
  // Anzeigen einer benutzerdefinierten 404-Seite
  include 'errors/404.html';
});

Flight erlaubt es Ihnen auch, Kernkomponenten des Frameworks zu ersetzen. Zum Beispiel können Sie die Standard-Router-Klasse durch Ihre eigene benutzerdefinierte Klasse ersetzen:

// Erstellen Sie Ihre benutzerdefinierte Router-Klasse
class MyRouter extends \flight\net\Router {
    // Methoden hier überschreiben
    // Zum Beispiel eine Abkürzung für GET-Anfragen, um die
    // Pass-Route-Funktion zu entfernen
    public function get($pattern, $callback, $alias = '') {
        return parent::get($pattern, $callback, false, $alias);
    }
}

// Registrieren Sie Ihre benutzerdefinierte Klasse
Flight::register('router', MyRouter::class);

// Wenn Flight die Router-Instanz lädt, wird Ihre Klasse geladen
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
  echo "Hello World!";
}, 'hello_alias');

Framework-Methoden wie map und register können jedoch nicht überschrieben werden. Sie erhalten einen Fehler, wenn Sie es versuchen (sehen Sie wieder unten für eine Liste der Methoden).

Zuordbare Framework-Methoden

Das Folgende ist die vollständige Menge der Methoden für das Framework. Es besteht aus Kernmethoden, die reguläre statische Methoden sind, und erweiterbaren Methoden, die zugeordnete Methoden sind, die gefiltert oder überschrieben werden können.

Kernmethoden

Diese Methoden sind zentral für das Framework und können nicht überschrieben werden.

Flight::map(string $name, callable $callback, bool $pass_route = false) // Erstellt eine benutzerdefinierte Framework-Methode.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registriert eine Klasse für eine Framework-Methode.
Flight::unregister(string $name) // Entregistriert eine Klasse für eine Framework-Methode.
Flight::before(string $name, callable $callback) // Fügt einen Filter vor einer Framework-Methode hinzu.
Flight::after(string $name, callable $callback) // Fügt einen Filter nach einer Framework-Methode hinzu.
Flight::path(string $path) // Fügt einen Pfad für das Autoloading von Klassen hinzu.
Flight::get(string $key) // Holt eine Variable, die von Flight::set() gesetzt wurde.
Flight::set(string $key, mixed $value) // Setzt eine Variable im Flight-Engine.
Flight::has(string $key) // Überprüft, ob eine Variable gesetzt ist.
Flight::clear(array|string $key = []) // Löscht eine Variable.
Flight::init() // Initialisiert das Framework mit seinen Standardeinstellungen.
Flight::app() // Holt die Anwendungsobjekt-Instanz
Flight::request() // Holt die Request-Objekt-Instanz
Flight::response() // Holt die Response-Objekt-Instanz
Flight::router() // Holt die Router-Objekt-Instanz
Flight::view() // Holt die View-Objekt-Instanz

Erweiterbare Methoden

Flight::start() // Startet das Framework.
Flight::stop() // Stoppt das Framework und sendet eine Antwort.
Flight::halt(int $code = 200, string $message = '') // Stoppt das Framework mit einem optionalen Statuscode und einer Nachricht.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Ordnet ein URL-Muster einem Callback zu.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Ordnet ein POST-Request-URL-Muster einem Callback zu.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Ordnet ein PUT-Request-URL-Muster einem Callback zu.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Ordnet ein PATCH-Request-URL-Muster einem Callback zu.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Ordnet ein DELETE-Request-URL-Muster einem Callback zu.
Flight::group(string $pattern, callable $callback) // Erstellt Gruppierungen für URLs, das Muster muss ein String sein.
Flight::getUrl(string $name, array $params = []) // Generiert eine URL basierend auf einem Route-Alias.
Flight::redirect(string $url, int $code) // Leitet zu einer anderen URL um.
Flight::download(string $filePath) // Lädt eine Datei herunter.
Flight::render(string $file, array $data, ?string $key = null) // Rendert eine Template-Datei.
Flight::error(Throwable $error) // Sendet eine HTTP-500-Antwort.
Flight::notFound() // Sendet eine HTTP-404-Antwort.
Flight::etag(string $id, string $type = 'string') // Führt ETag-HTTP-Caching durch.
Flight::lastModified(int $time) // Führt letztes-Änderungs-HTTP-Caching durch.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sendet eine JSON-Antwort.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sendet eine JSONP-Antwort.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sendet eine JSON-Antwort und stoppt das Framework.
Flight::onEvent(string $event, callable $callback) // Registriert einen Event-Listener.
Flight::triggerEvent(string $event, ...$args) // Löst ein Event aus.

Jede benutzerdefinierte Methode, die mit map und register hinzugefügt wurde, kann auch gefiltert werden. Für Beispiele, wie man diese Methoden filtert, siehe die Filtering Methods-Anleitung.

Erweiterbare Framework-Klassen

Es gibt mehrere Klassen, deren Funktionalität Sie durch Erweiterung und Registrierung Ihrer eigenen Klasse überschreiben können. Diese Klassen sind:

Flight::app() // Anwendungsklasse - erweitern Sie die flight\Engine-Klasse
Flight::request() // Request-Klasse - erweitern Sie die flight\net\Request-Klasse
Flight::response() // Response-Klasse - erweitern Sie die flight\net\Response-Klasse
Flight::router() // Router-Klasse - erweitern Sie die flight\net\Router-Klasse
Flight::view() // View-Klasse - erweitern Sie die flight\template\View-Klasse
Flight::eventDispatcher() // Event-Dispatcher-Klasse - erweitern Sie die flight\core\Dispatcher-Klasse

Benutzerdefinierte Methoden zuordnen

Um Ihre eigene einfache benutzerdefinierte Methode zuzuordnen, verwenden Sie die map-Funktion:

// Ordnen Sie Ihre Methode zu
Flight::map('hello', function (string $name) {
  echo "hello $name!";
});

// Rufen Sie Ihre benutzerdefinierte Methode auf
Flight::hello('Bob');

Während es möglich ist, einfache benutzerdefinierte Methoden zu erstellen, wird empfohlen, einfach Standardfunktionen in PHP zu erstellen. Dies hat Autovervollständigung in IDEs und ist einfacher zu lesen. Das Äquivalent des obigen Codes wäre:

function hello(string $name) {
  echo "hello $name!";
}

hello('Bob');

Dies wird mehr verwendet, wenn Sie Variablen in Ihre Methode übergeben müssen, um einen erwarteten Wert zu erhalten. Die Verwendung der register()-Methode wie unten ist mehr für das Übergeben von Konfiguration und dann das Aufrufen Ihrer vorkonfigurierten Klasse.

Benutzerdefinierte Klassen registrieren

Um Ihre eigene Klasse zu registrieren und sie zu konfigurieren, verwenden Sie die register-Funktion. Der Vorteil, den dies gegenüber map() hat, ist, dass Sie dieselbe Klasse wiederverwenden können, wenn Sie diese Funktion aufrufen (wäre hilfreich mit Flight::db(), um dieselbe Instanz zu teilen).

// Registrieren Sie Ihre Klasse
Flight::register('user', User::class);

// Holen Sie eine Instanz Ihrer Klasse
$user = Flight::user();

Die register-Methode erlaubt es Ihnen auch, Parameter an den Konstruktor Ihrer Klasse zu übergeben. Wenn Sie also Ihre benutzerdefinierte Klasse laden, wird sie voreingestellt initialisiert. Sie können die Konstruktor-Parameter definieren, indem Sie ein zusätzliches Array übergeben. Hier ist ein Beispiel für das Laden einer Datenbankverbindung:

// Klasse mit Konstruktor-Parametern registrieren
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);

// Holen Sie eine Instanz Ihrer Klasse
// Dies wird ein Objekt mit den definierten Parametern erstellen
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();

// und wenn Sie es später in Ihrem Code benötigen, rufen Sie einfach dieselbe Methode erneut auf
class SomeController {
  public function __construct() {
    $this->db = Flight::db();
  }
}

Wenn Sie einen zusätzlichen Callback-Parameter übergeben, wird er unmittelbar nach der Klassenkonstruktion ausgeführt. Dies erlaubt es Ihnen, alle Einrichtungsverfahren für Ihr neues Objekt durchzuführen. Die Callback-Funktion nimmt einen Parameter: eine Instanz des neuen Objekts.

// Der Callback wird das konstruierte Objekt übergeben
Flight::register(
  'db',
  PDO::class,
  ['mysql:host=localhost;dbname=test', 'user', 'pass'],
  function (PDO $db) {
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  }
);

Standardmäßig erhalten Sie bei jedem Laden Ihrer Klasse eine geteilte Instanz. Um eine neue Instanz einer Klasse zu erhalten, übergeben Sie einfach false als Parameter:

// Geteilte Instanz der Klasse
$shared = Flight::db();

// Neue Instanz der Klasse
$new = Flight::db(false);

Hinweis: Beachten Sie, dass zugeordnete Methoden Vorrang vor registrierten Klassen haben. Wenn Sie beide mit demselben Namen deklarieren, wird nur die zugeordnete Methode aufgerufen.

Beispiele

Hier sind einige Beispiele, wie Sie Flight mit Funktionalität erweitern können, die nicht im Kern integriert ist.

Logging

Flight hat kein integriertes Logging-System, es ist jedoch wirklich einfach, eine Logging-Bibliothek mit Flight zu verwenden. Hier ist ein Beispiel mit der Monolog-Bibliothek:

// services.php

// Registrieren Sie den Logger bei Flight
Flight::register('log', Monolog\Logger::class, [ 'name' ], function(Monolog\Logger $log) {
    $log->pushHandler(new Monolog\Handler\StreamHandler('path/to/your.log', Monolog\Logger::WARNING));
});

Nun, da es registriert ist, können Sie es in Ihrer Anwendung verwenden:

// In Ihrem Controller oder Route
Flight::log()->warning('This is a warning message');

Dies wird eine Nachricht in die von Ihnen angegebene Log-Datei schreiben. Was, wenn Sie etwas protokollieren möchten, wenn ein Fehler auftritt? Sie können die error-Methode verwenden:

// In Ihrem Controller oder Route
Flight::map('error', function(Throwable $ex) {
    Flight::log()->error($ex->getMessage());
    // Zeigen Sie Ihre benutzerdefinierte Fehlerseite an
    include 'errors/500.html';
});

Sie könnten auch ein einfaches APM (Application Performance Monitoring)-System mit den before- und after-Methoden erstellen:

// In Ihrer services.php-Datei

Flight::before('start', function() {
    Flight::set('start_time', microtime(true));
});

Flight::after('start', function() {
    $end = microtime(true);
    $start = Flight::get('start_time');
    Flight::log()->info('Request '.Flight::request()->url.' took ' . round($end - $start, 4) . ' seconds');

    // Sie könnten auch Ihre Request- oder Response-Header hinzufügen
    // um sie zu protokollieren (seien Sie vorsichtig, da dies eine 
    // Menge Daten sein würde, wenn Sie viele Anfragen haben)
    Flight::log()->info('Request Headers: ' . json_encode(Flight::request()->headers));
    Flight::log()->info('Response Headers: ' . json_encode(Flight::response()->headers));
});

Caching

Flight hat kein integriertes Caching-System, es ist jedoch wirklich einfach, eine Caching-Bibliothek mit Flight zu verwenden. Hier ist ein Beispiel mit der PHP File Cache-Bibliothek:

// services.php

// Registrieren Sie den Cache bei Flight
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
    $cache->setDevMode(ENVIRONMENT === 'development');
});

Nun, da es registriert ist, können Sie es in Ihrer Anwendung verwenden:

// In Ihrem Controller oder Route
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
    // Führen Sie einige Verarbeitung durch, um die Daten zu erhalten
    $data = [ 'some' => 'data' ];
    Flight::cache()->set('my_cache_key', $data, 3600); // Cache für 1 Stunde
}

Einfache DIC-Objekt-Instantiierung

Wenn Sie einen DIC (Dependency Injection Container) in Ihrer Anwendung verwenden, können Sie Flight verwenden, um Ihnen bei der Instantiierung Ihrer Objekte zu helfen. Hier ist ein Beispiel mit der Dice-Bibliothek:

// services.php

// Erstellen Sie einen neuen Container
$container = new \Dice\Dice;
// Vergessen Sie nicht, ihn sich selbst zuzuweisen wie unten!
$container = $container->addRule('PDO', [
    // shared bedeutet, dass dasselbe Objekt jedes Mal zurückgegeben wird
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// Nun können wir eine zuordbare Methode erstellen, um jedes Objekt zu erstellen. 
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// Dies registriert den Container-Handler, damit Flight weiß, dass er ihn für Controller/Middleware verwendet
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});


// Sagen wir, wir haben die folgende Beispielklasse, die ein PDO-Objekt im Konstruktor nimmt
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // Code, der eine E-Mail sendet
    }
}

// Und schließlich können Sie Objekte mit Dependency Injection erstellen
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

Cool, oder?

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/security

Sicherheit

Übersicht

Sicherheit ist ein großes Thema bei Webanwendungen. Sie möchten sicherstellen, dass Ihre Anwendung sicher ist und die Daten Ihrer Benutzer geschützt sind. Flight bietet eine Reihe von Funktionen, die Ihnen helfen, Ihre Webanwendungen zu sichern.

Das offizielle Skeleton enthält außerdem eine dedizierte SECURITY.md sowie eine Middleware für Sicherheits-Header, sodass KI-Codierungswerkzeuge (und Menschen) einen bewussten Ort für Geheimnisse, Header und XSS-/SQL-Regeln haben – getrennt vom allgemeinen Codierungsstil in AGENTS.md.

Grundlegendes Verständnis

Es gibt eine Reihe häufiger Sicherheitsbedrohungen, die Sie beim Entwickeln von Webanwendungen kennen sollten. Zu den häufigsten Bedrohungen gehören:

Templates helfen bei XSS, indem sie Ausgaben standardmäßig escapen (Twig und Latte tun dies; nutzen Sie diesen Vorteil). Sessions können bei CSRF helfen, indem ein CSRF-Token in der Benutzersitzung gespeichert wird, wie unten beschrieben. Die Verwendung von Prepared Statements mit PDO – oder Helfern auf SimplePdo – hilft, SQL-Injection zu verhindern. CORS kann mit einem einfachen Hook vor dem Aufruf von Flight::start() behandelt werden.

Alle diese Methoden arbeiten zusammen, um Ihre Webanwendungen sicher zu halten. Es sollte immer im Hinterkopf behalten werden, Sicherheits-Best-Practices zu lernen und zu verstehen. Fragen Sie keinen KI-Assistenten, "CSP zu deaktivieren" oder Header zu schwächen, nur um eine Seite laden zu lassen, ohne den Kompromiss zu verstehen.

Grundlegende Verwendung

Header

HTTP-Header sind einer der einfachsten Wege, Ihre Webanwendungen zu sichern. Sie können Header verwenden, um Clickjacking, XSS und andere Angriffe zu verhindern. Es gibt mehrere Möglichkeiten, diese Header zu Ihrer Anwendung hinzuzufügen.

Zwei großartige Websites, um die Sicherheit Ihrer Header zu überprüfen, sind securityheaders.com und observatory.mozilla.org. Nachdem Sie den folgenden Code eingerichtet haben, können Sie einfach mit diesen beiden Websites überprüfen, ob Ihre Header funktionieren.

Das Skeleton enthält App\Middleware\SecurityHeadersMiddleware (CSP mit einem Nonce pro Anfrage, Frame-Optionen, HSTS und mehr). Bevorzugen Sie, das bewusst zu erweitern, anstatt Header zu deaktivieren.

Manuell hinzufügen

Sie können diese Header manuell mit der header-Methode auf dem Flight\Response-Objekt hinzufügen.

// Setzen Sie den X-Frame-Options-Header, um Clickjacking zu verhindern
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// Setzen Sie den Content-Security-Policy-Header, um XSS zu verhindern
// Hinweis: Dieser Header kann sehr komplex werden, daher sollten Sie
//  Beispiele im Internet für Ihre Anwendung konsultieren
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// Setzen Sie den X-XSS-Protection-Header, um XSS zu verhindern
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// Setzen Sie den X-Content-Type-Options-Header, um MIME-Sniffing zu verhindern
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// Setzen Sie den Referrer-Policy-Header, um zu steuern, wie viele Referrer-Informationen gesendet werden
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// Setzen Sie den Strict-Transport-Security-Header, um HTTPS zu erzwingen
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// Setzen Sie den Permissions-Policy-Header, um zu steuern, welche Funktionen und APIs verwendet werden können
Flight::response()->header('Permissions-Policy', 'geolocation=()');

Diese können am Anfang Ihrer routes.php- oder index.php-Dateien hinzugefügt werden.

Als Filter hinzufügen

Sie können sie auch in einem Filter/Hook wie folgt hinzufügen:

// Header in einem Filter hinzufügen
Flight::before('start', function() {
    Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
    Flight::response()->header("Content-Security-Policy", "default-src 'self'");
    Flight::response()->header('X-XSS-Protection', '1; mode=block');
    Flight::response()->header('X-Content-Type-Options', 'nosniff');
    Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
    Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    Flight::response()->header('Permissions-Policy', 'geolocation=()');
});

Als Middleware hinzufügen

Sie können sie auch als Middleware-Klasse hinzufügen, die die größte Flexibilität bietet, auf welche Routen dies angewendet wird. Im Allgemeinen sollten diese Header auf alle HTML- und API-Antworten angewendet werden.

Skeleton-Stil Pfad und Namespace (Ordnernamen müssen zu App\Middleware passen):

// app/Middleware/SecurityHeadersMiddleware.php

namespace App\Middleware;

use flight\Engine;

class SecurityHeadersMiddleware
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function before(array $params): void
    {
        $response = $this->app->response();
        // Bevorzugen Sie ein CSP-Nonce aus dem Bootstrap, wenn Sie Inline-Skripte haben (Skeleton setzt csp_nonce)
        $nonce = $this->app->get('csp_nonce');
        $csp = $nonce
            ? "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'"
            : "default-src 'self'";

        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header('Content-Security-Policy', $csp);
        $response->header('X-XSS-Protection', '1; mode=block');
        $response->header('X-Content-Type-Options', 'nosniff');
        $response->header('Referrer-Policy', 'no-referrer-when-downgrade');
        $response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->header('Permissions-Policy', 'geolocation=()');
    }
}

// app/config/routes.php – leere String-Gruppe = globale Middleware für alle Routen
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;

$router->group('', function (Router $router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // weitere Routen
}, [SecurityHeadersMiddleware::class]);

Ältere Projekte verwenden möglicherweise weiterhin app/middlewares und app\middlewares; das funktioniert, wenn die Ordner übereinstimmen. Neue Skeleton-Apps verwenden app/Middleware/ und App\Middleware. Siehe Autoloading.

Cross Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF) ist eine Angriffsart, bei der eine bösartige Website den Browser eines Benutzers dazu bringen kann, eine Anfrage an Ihre Website zu senden. Dies kann verwendet werden, um Aktionen auf Ihrer Website ohne Wissen des Benutzers auszuführen. Flight bietet keinen eingebauten CSRF-Schutzmechanismus, aber Sie können leicht einen eigenen mit Middleware implementieren.

Einrichtung

Zuerst müssen Sie ein CSRF-Token generieren und in der Benutzersitzung speichern. Sie können dieses Token dann in Ihren Formularen verwenden und es überprüfen, wenn das Formular abgesendet wird. Wir verwenden das flightphp/session Plugin zur Verwaltung von Sitzungen.

// Generieren Sie ein CSRF-Token und speichern Sie es in der Benutzersitzung
// (angenommen, Sie haben ein Sitzungsobjekt erstellt und an Flight angehängt)
// siehe die Sitzungsdokumentation für weitere Informationen
Flight::register('session', flight\Session::class);

// Sie müssen nur ein einziges Token pro Sitzung generieren (damit es 
// über mehrere Tabs und Anfragen für denselben Benutzer funktioniert)
if(Flight::session()->get('csrf_token') === null) {
    Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
Mit der Standard-PHP-Flight-Vorlage
<!-- Verwenden Sie das CSRF-Token in Ihrem Formular -->
<form method="post">
    <input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
    <!-- weitere Formularfelder -->
</form>
Mit Twig (Skeleton-Standard)

Registrieren Sie eine Twig-Funktion oder übergeben Sie das Token an jede Formularansicht. Minimales Beispiel mit einem Global und einem Formularfeld:

// Bei der Konfiguration von Twig (z. B. services.php)
$twig->addGlobal('csrf_token', $app->session()->get('csrf_token'));
{# app/views/form.twig #}
<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    {# weitere Felder #}
</form>
Mit Latte

Sie können auch eine benutzerdefinierte Funktion festlegen, um das CSRF-Token in Ihren Latte-Templates auszugeben.


Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // weitere Konfigurationen...

    // Eine benutzerdefinierte Funktion festlegen, um das CSRF-Token auszugeben
    $latte->addFunction('csrf', function() {
        $csrfToken = Flight::session()->get('csrf_token');
        return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
    });

    $latte->render($finalPath, $data, $block);
});

Und jetzt können Sie in Ihren Latte-Templates die Funktion csrf() verwenden, um das CSRF-Token auszugeben.

<form method="post">
    {csrf()}
    <!-- weitere Formularfelder -->
</form>

CSRF-Token überprüfen

Sie können das CSRF-Token mit mehreren Methoden überprüfen.

Middleware
// app/Middleware/CsrfMiddleware.php

namespace App\Middleware;

use flight\Engine;

class CsrfMiddleware
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function before(array $params): void
    {
        if($this->app->request()->method == 'POST') {
            $token = $this->app->request()->data->csrf_token;
            if($token !== $this->app->session()->get('csrf_token')) {
                $this->app->halt(403, 'Invalid CSRF token');
            }
        }
    }
}

// routes.php
use App\Middleware\CsrfMiddleware;

$router->group('', function ($router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // weitere Routen
}, [CsrfMiddleware::class]);
Event-Filter
// Diese Middleware prüft, ob die Anfrage eine POST-Anfrage ist, und wenn ja, ob das CSRF-Token gültig ist
Flight::before('start', function() {
    if(Flight::request()->method == 'POST') {

        // das CSRF-Token aus den Formularwerten erfassen
        $token = Flight::request()->data->csrf_token;
        if($token !== Flight::session()->get('csrf_token')) {
            Flight::halt(403, 'Invalid CSRF token');
            // oder für eine JSON-Antwort
            Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
        }
    }
});

Cross Site Scripting (XSS)

Cross Site Scripting (XSS) ist eine Angriffsart, bei der eine bösartige Formulareingabe Code in Ihre Website injizieren kann. Die meisten dieser Möglichkeiten stammen aus Formularwerten, die Ihre Endbenutzer ausfüllen. Sie sollten niemals der Ausgabe Ihrer Benutzer vertrauen! Gehen Sie immer davon aus, dass alle die besten Hacker der Welt sind. Sie können bösartiges JavaScript oder HTML in Ihre Seite injizieren. Dieser Code kann verwendet werden, um Informationen von Ihren Benutzern zu stehlen oder Aktionen auf Ihrer Website auszuführen. Mit der View-Klasse von Flight oder einer Template-Engine wie Twig oder Latte können Sie Ausgaben einfach escapen, um XSS-Angriffe zu verhindern.

// Nehmen wir an, der Benutzer ist clever und versucht, dies als seinen Namen zu verwenden
$name = '<script>alert("XSS")</script>';

// Dies escapet die Ausgabe
Flight::view()->set('name', $name);
// Dies gibt aus: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// Twig (Skeleton-Standard) und Latte escapen standardmäßig automatisch – bevorzugen Sie diese gegenüber rohem PHP-Echo
Flight::render('template', ['name' => $name]);
// Twig: {{ name }}  → escaped
// Vermeiden Sie |raw / unescapte Ausgabe, es sei denn, der Inhalt ist vollständig vertrauenswürdig

SQL-Injection

SQL-Injection ist eine Angriffsart, bei der ein bösartiger Benutzer SQL-Code in Ihre Datenbank injizieren kann. Dies kann verwendet werden, um Informationen aus Ihrer Datenbank zu stehlen oder Aktionen in Ihrer Datenbank auszuführen. Auch hier sollten Sie niemals der Eingabe Ihrer Benutzer vertrauen! Gehen Sie immer davon aus, dass sie auf Ärger aus sind. Verwenden Sie Prepared Statements – SimplePdo Helfer machen dies zum Standardpfad.

// Angenommen, Sie haben Flight::db() als SimplePdo registriert (oder injizieren SimplePdo im Controller)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// SimplePdo (bevorzugt) – Einzeiler mit gebundenen Parametern
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// Gleiche Idee mit ?-Platzhaltern
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

In Skeleton-Controllern bevorzugen Sie die Konstruktor-Injektion von SimplePdo anstelle von Flight::db(), damit Tests und KI-generierter Code konsistent bleiben (DIC).

Unsicheres Beispiel

Das Folgende zeigt, warum wir SQL-Prepared-Statements verwenden, um sich vor unschuldigen Beispielen wie diesem zu schützen:

// Endbenutzer füllt ein Webformular aus.
// Als Wert des Formulars gibt der Hacker etwa Folgendes ein:
$username = "' OR 1=1; -- ";

$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// Nachdem die Abfrage erstellt wurde, sieht sie so aus
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5

// Es sieht seltsam aus, ist aber eine gültige Abfrage, die funktioniert. Tatsächlich
// ist es ein sehr häufiger SQL-Injection-Angriff, der alle Benutzer zurückgibt.

var_dump($users); // dies gibt alle Benutzer in der Datenbank aus, nicht nur den einen Benutzernamen

Geheimnisse und Konfiguration

Learn/templates

HTML-Ansichten und Templates

Überblick

Flight bietet standardmäßig eine grundlegende HTML-Template-Funktionalität. Templating ist eine sehr effektive Möglichkeit, Ihre Anwendungslogik von Ihrer Präsentationsebene zu entkoppeln. Eine dedizierte Engine (Twig, Latte, usw.) gibt KI-Coding-Tools ebenfalls eine vertraute, eingeschränkte Syntax, sodass diese weniger dazu neigen, Geschäftslogik in Ihr HTML zu schreiben.

Verständnis

Wenn Sie eine Anwendung erstellen, haben Sie wahrscheinlich HTML, das Sie an den Endbenutzer ausliefern möchten. PHP ist selbst eine Template-Sprache, aber es ist sehr einfach, Geschäftslogik wie Datenbankaufrufe, API-Aufrufe usw. in Ihre HTML-Datei zu packen und Testen und Entkopplung zu einem sehr schwierigen Prozess zu machen. Indem Sie Daten in ein Template verschieben und das Template sich selbst rendern lassen, wird es viel einfacher, Ihren Code zu entkoppeln und Unit-Tests zu schreiben. Sie werden uns danken, wenn Sie Templates verwenden!

Grundlegende Verwendung

Flight ermöglicht es Ihnen, die Standard-View-Engine einfach durch Zuordnung von render (oder Registrierung einer View-Klasse) auszutauschen. Scrollen Sie nach unten für Twig, Latte, Smarty, Blade und mehr.

Skeleton-Standard: Das offizielle flightphp/skeleton verwendet unter app/views/ (*.twig) nur Twig. Controller rufen $this->app->render('welcome', $data) auf (Endung optional). Das ist eine Anwendungsentscheidung für neue Projekte – keine Anforderung des Flight-Kerns. Latte und andere Engines bleiben voll unterstützt.

Twig

Skeleton-Standard

Twig ist eine flexible, schnelle und sichere Template-Engine, die von Symfony und vielen anderen PHP-Projekten verwendet wird. KI-Coding-Tools kennen Twig besonders gut, und es escaped Ausgaben standardmäßig automatisch, was vor XSS schützt.

Installation

composer require twig/twig

(Bereits enthalten, wenn Sie composer create-project flightphp/skeleton verwenden.)

Grundlegende Konfiguration

Überschreiben Sie die render-Methode, um Twig anstelle des standardmäßigen PHP-Renderers zu verwenden:

// überschreibe die render-Methode, um Twig anstelle des standardmäßigen PHP-Renderers zu verwenden
Flight::map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Wo Twig seine kompilierten Templates speichert
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ]);

    // "welcome" oder "welcome.twig" erlauben
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }

    echo $twig->render($template, $data);
});

Im Skeleton befindet sich diese Verdrahtung in app/config/services.php (gemeinsame Twig-Umgebung, Cache-Pfad, Globals wie base_url / CSP-Nonce). Bevorzugen Sie das Injizieren von Engine und das Aufrufen von $app->render() aus Controllern, damit der Code KI- und testfreundlich bleibt.

Twig in Flight verwenden

Jetzt, wo Sie mit Twig rendern können, können Sie beispielsweise Folgendes tun:

{# app/views/home.twig #}
<html>
  <head>
    <title>{% if title %}{{ title }} - {% endif %}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {{ name }}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.twig', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

Wenn Sie in Ihrem Browser /Bob aufrufen, wäre die Ausgabe:

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

Weiterführende Lektüre

Ein vollständigeres Beispiel für die Verwendung von Twig mit Layouts finden Sie im Abschnitt awesome plugins dieser Dokumentation. Für Metriken zur Renderzeit in der Tracy-Leiste siehe Twig-Panel in Tracy Extensions.

Weitere Informationen zu den vollständigen Funktionen von Twig finden Sie in der offiziellen Dokumentation.

Latte

großartige Alternative

Latte ist eine voll ausgestattete Engine mit einer PHP-ähnlichen Syntax. Sie ist nach wie vor eine ausgezeichnete Wahl für Flight-Apps; das Skeleton standardisiert lediglich auf Twig als gemeinsamen Standard (besonders hilfreich, wenn KI-Tools Templates generieren).

Installation

composer require latte/latte

Grundlegende Konfiguration

Die Grundidee ist, dass Sie die render-Methode überschreiben, um Latte anstelle des standardmäßigen PHP-Renderers zu verwenden.

// überschreibe die render-Methode, um Latte anstelle des standardmäßigen PHP-Renderers zu verwenden
Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Wo Latte seinen Cache speichert
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

    $latte->render($finalPath, $data, $block);
});

Latte in Flight verwenden

Jetzt, wo Sie mit Latte rendern können, können Sie beispielsweise Folgendes tun:

<!-- app/views/home.latte -->
<html>
  <head>
    <title>{$title ? $title . ' - '}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {$name}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.latte', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

Wenn Sie in Ihrem Browser /Bob aufrufen, wäre die Ausgabe:

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

Weiterführende Lektüre

Ein komplexeres Beispiel für die Verwendung von Latte mit Layouts finden Sie im Abschnitt awesome plugins dieser Dokumentation.

Weitere Informationen zu den vollständigen Funktionen von Latte, einschließlich Übersetzungs- und Sprachfunktionen, finden Sie in der offiziellen Dokumentation.

Integrierte View-Engine

veraltet

Hinweis: Dies ist weiterhin die Standardfunktionalität und funktioniert technisch weiterhin.

Um eine View-Template anzuzeigen, rufen Sie die render-Methode mit dem Namen der Template-Datei und optionalen Template-Daten auf:

Flight::render('hello.php', ['name' => 'Bob']);

Die übergebenen Template-Daten werden automatisch in das Template injiziert und können wie lokale Variablen referenziert werden. Template-Dateien sind einfach PHP-Dateien. Wenn der Inhalt der Template-Datei hello.php ist:

Hello, <?= $name ?>!

Die Ausgabe wäre:

Hello, Bob!

Sie können Ansichtsvariablen auch manuell über die set-Methode festlegen:

Flight::view()->set('name', 'Bob');

Die Variable name ist nun in allen Ihren Ansichten verfügbar. Sie können also einfach Folgendes tun:

Flight::render('hello');

Beachten Sie, dass Sie bei der Angabe des Templatenamens in der render-Methode die Erweiterung .php weglassen können.

Standardmäßig sucht Flight in einem views-Verzeichnis nach Template-Dateien. Sie können einen alternativen Pfad für Ihre Templates festlegen, indem Sie die folgende Konfiguration setzen:

Flight::set('flight.views.path', '/path/to/views');

Standardmäßig akzeptiert die integrierte View von Flight auch einen absoluten Template-Pfad oder einen Namen, der aus diesem Verzeichnis hinausgeht. Für die meisten Anwendungen sollten Sie das einschränken:

Flight::set('flight.views.restrict_to_path', true);

Das hält render(), fetch() und exists() innerhalb von flight.views.path. Es ist standardmäßig deaktiviert, um Abwärtskompatibilität zu gewährleisten. Siehe Sicherheit.

Layouts

Es ist üblich, dass Websites eine einzige Layout-Template-Datei mit wechselndem Inhalt haben. Um Inhalte zu rendern, die in einem Layout verwendet werden sollen, können Sie einen optionalen Parameter an die render-Methode übergeben.

Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');

Ihre Ansicht hat dann gespeicherte Variablen namens headerContent und bodyContent. Sie können Ihr Layout dann wie folgt rendern:

Flight::render('layout', ['title' => 'Home Page']);

Wenn die Template-Dateien wie folgt aussehen:

header.php:

<h1><?= $heading ?></h1>

body.php:

<div><?= $body ?></div>

layout.php:

<html>
  <head>
    <title><?= $title ?></title>
  </head>
  <body>
    <?= $headerContent ?>
    <?= $bodyContent ?>
  </body>
</html>

Die Ausgabe wäre:

<html>
  <head>
    <title>Home Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <div>World</div>
  </body>
</html>

Smarty

Hier erfahren Sie, wie Sie die Smarty-Template-Engine für Ihre Ansichten verwenden:

// Smarty-Bibliothek laden
require './Smarty/libs/Smarty.class.php';

// Smarty als View-Klasse registrieren
// Außerdem eine Callback-Funktion übergeben, um Smarty beim Laden zu konfigurieren
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
  $smarty->setTemplateDir('./templates/');
  $smarty->setCompileDir('./templates_c/');
  $smarty->setConfigDir('./config/');
  $smarty->setCacheDir('./cache/');
});

// Template-Daten zuweisen
Flight::view()->assign('name', 'Bob');

// Template anzeigen
Flight::view()->display('hello.tpl');

Der Vollständigkeit halber sollten Sie auch die standardmäßige render-Methode von Flight überschreiben:

Flight::map('render', function(string $template, array $data): void {
  Flight::view()->assign($data);
  Flight::view()->display($template);
});

Blade

Hier erfahren Sie, wie Sie die Blade-Template-Engine für Ihre Ansichten verwenden:

Zuerst müssen Sie die BladeOne-Bibliothek über Composer installieren:

composer require eftec/bladeone

Dann können Sie BladeOne als View-Klasse in Flight konfigurieren:

<?php
// BladeOne-Bibliothek laden
use eftec\bladeone\BladeOne;

// BladeOne als View-Klasse registrieren
// Außerdem eine Callback-Funktion übergeben, um BladeOne beim Laden zu konfigurieren
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
  $views = __DIR__ . '/../views';
  $cache = __DIR__ . '/../cache';

  $blade->setPath($views);
  $blade->setCompiledPath($cache);
});

// Template-Daten zuweisen
Flight::view()->share('name', 'Bob');

// Template anzeigen
echo Flight::view()->run('hello', []);

Der Vollständigkeit halber sollten Sie auch die standardmäßige render-Methode von Flight überschreiben:

<?php
Flight::map('render', function(string $template, array $data): void {
  echo Flight::view()->run($template, $data);
});

In diesem Beispiel könnte die Template-Datei hello.blade.php wie folgt aussehen:

<?php
Hello, {{ $name }}!

Die Ausgabe wäre:

Hello, Bob!

Siehe auch

Fehlerbehebung

Changelog

Learn/why_frameworks

Warum ein Framework?

Einige Programmierer sind vehement gegen die Verwendung von Frameworks. Sie argumentieren, dass Frameworks aufgebläht, langsam und schwer zu erlernen sind. Sie sagen, dass Frameworks unnötig sind und dass man besseren Code ohne sie schreiben kann. Es gibt sicherlich einige überzeugende Argumente gegen die Verwendung von Frameworks vorzubringen. Allerdings gibt es auch viele Vorteile bei der Verwendung von Frameworks.

Gründe für die Verwendung eines Frameworks

Hier sind ein paar Gründe, warum Sie in Betracht ziehen sollten, ein Framework zu verwenden:

Flight ist ein Mikro-Framework. Das bedeutet, dass es klein und leichtgewichtig ist. Es bietet nicht so viele Funktionen wie größere Frameworks wie Laravel oder Symfony. Allerdings bietet es viele der Funktionen, die Sie benötigen, um Webanwendungen zu erstellen. Es ist auch einfach zu erlernen und zu verwenden. Das macht es zu einer guten Wahl, um Webanwendungen schnell und einfach zu erstellen. Wenn Sie neu in der Welt der Frameworks sind, ist Flight ein großartiges Anfänger-Framework, mit dem Sie beginnen können. Es hilft Ihnen, die Vorteile der Verwendung von Frameworks kennenzulernen, ohne Sie mit zu viel Komplexität zu überfordern. Nachdem Sie etwas Erfahrung mit Flight gesammelt haben, wird es einfacher sein, auf komplexere Frameworks wie Laravel oder Symfony umzusteigen, Flight kann jedoch immer noch eine erfolgreiche robuste Anwendung ermöglichen.

Was ist Routing?

Routing ist der Kern des Flight Frameworks, aber was genau ist das? Routing ist der Prozess, bei dem eine URL genommen und mit einer bestimmten Funktion in Ihrem Code abgeglichen wird. So können Sie Ihre Website basierend auf der angeforderten URL unterschiedliche Dinge tun lassen. Zum Beispiel möchten Sie möglicherweise das Profil eines Benutzers anzeigen, wenn er /user/1234 besucht, aber eine Liste aller Benutzer anzeigen, wenn er /users besucht. All dies geschieht durch Routing.

Es könnte etwa so funktionieren:

Und warum ist das wichtig?

Eine ordnungsgemäß zentralisierte Routerung kann tatsächlich Ihr Leben dramatisch vereinfachen! Es kann nur anfangs schwer zu erkennen sein. Hier sind ein paar Gründe:

Sie sind sicherlich vertraut mit dem Script für Script-Weg, eine Website zu erstellen. Sie könnten eine Datei namens index.php haben, die eine Reihe von if-Anweisungen enthält, um die URL zu überprüfen und dann eine bestimmte Funktion auf der Grundlage der URL auszuführen. Dies ist eine Form der Routenführung, aber sie ist nicht sehr organisiert und kann schnell außer Kontrolle geraten. Flights Routensystem ist eine viel organisiertere und leistungsfähigere Art, die Routenführung zu handhaben.

Dies hier?


// /user/view_profile.php?id=1234
if ($_GET['id']) {
    $id = $_GET['id'];
    viewUserProfile($id);
}

// /user/edit_profile.php?id=1234
if ($_GET['id']) {
    $id = $_GET['id'];
    editUserProfile($id);
}

// etc...

Oder das hier?


// index.php
Flight::route('/user/@id', [ 'BenutzerController', 'BenutzerprofilAnzeigen' ]);
Flight::route('/user/@id/edit', [ 'BenutzerController', 'BenutzerprofilBearbeiten' ]);

// Vielleicht in Ihrem app/controllers/UserController.php
class UserController {
    public function viewUserProfile($id) {
        // Mach etwas
    }

    public function editUserProfile($id) {
        // Mach etwas
    }
}

Hoffentlich erkennen Sie langsam die Vorteile der Verwendung eines zentralisierten Routingsystems. Es ist viel einfacher zu verwalten und zu verstehen auf lange Sicht!

Anfragen und Antworten

Flight bietet eine einfache und unkomplizierte Möglichkeit, Anfragen und Antworten zu bearbeiten. Dies ist der Kern dessen, was ein Web-Framework macht. Es nimmt eine Anfrage von einem Benutzerbrowser entgegen, verarbeitet sie und sendet dann eine Antwort zurück. So können Sie Webanwendungen erstellen, die Dinge wie das Anzeigen eines Benutzerprofils, das Einloggen eines Benutzers oder das Posten eines neuen Blogposts ermöglichen.

Anfragen

Eine Anfrage ist das, was der Browser eines Benutzers an Ihren Server sendet, wenn er Ihre Website besucht. Diese Anfrage enthält Informationen darüber, was der Benutzer tun möchte. Zum Beispiel könnte sie Informationen darüber enthalten, welche URL der Benutzer besuchen möchte, welche Daten der Benutzer an Ihren Server senden möchte oder welche Art von Daten der Benutzer von Ihrem Server erhalten möchte. Es ist wichtig zu wissen, dass eine Anfrage schreibgeschützt ist. Sie können die Anfrage nicht ändern, aber Sie können daraus lesen.

Flight bietet eine einfache Möglichkeit, Informationen zur Anfrage abzurufen. Sie können über die Methode Flight::request() Informationen zur Anfrage abrufen. Diese Methode gibt ein Request-Objekt zurück, das Informationen zur Anfrage enthält. Mit diesem Objekt können Sie Informationen zur Anfrage abrufen, wie die URL, die Methode oder die Daten, die der Benutzer an Ihren Server gesendet hat.

Antworten

Eine Antwort ist das, was Ihr Server an den Browser eines Benutzers zurücksendet, wenn er Ihre Website besucht. Diese Antwort enthält Informationen darüber, was Ihr Server tun möchte. Zum Beispiel könnte es Informationen darüber enthalten, welche Art von Daten Ihr Server an den Benutzer senden möchte, welche Art von Daten Ihr Server von dem Benutzer erhalten möchte oder welche Art von Daten Ihr Server auf dem Computer des Benutzers speichern möchte.

Flight bietet eine einfache Möglichkeit, eine Antwort an den Browser eines Benutzers zu senden. Sie können eine Antwort mit der Methode Flight::response() senden. Diese Methode nimmt ein Response-Objekt als Argument und sendet die Antwort an den Browser des Benutzers. Sie können dieses Objekt verwenden, um eine Antwort an den Browser des Benutzers zu senden, wie z. B. HTML, JSON oder eine Datei. Flight hilft Ihnen dabei, einige Teile der Antwort automatisch zu generieren, um die Dinge zu vereinfachen, aber letztendlich haben Sie die Kontrolle darüber, was Sie dem Benutzer zurücksenden.

Learn/middleware

Middleware

Überblick

Flight unterstützt Route- und Gruppen-Route-Middleware. Middleware ist ein Teil Ihrer Anwendung, in dem Code ausgeführt wird, bevor (oder nach) dem Route-Callback. Dies ist eine großartige Möglichkeit, API-Authentifizierungsprüfungen in Ihrem Code hinzuzufügen oder zu überprüfen, ob der Benutzer die Berechtigung hat, auf die Route zuzugreifen.

Verständnis

Middleware kann Ihre App erheblich vereinfachen. Anstatt komplexer abstrakter Klassenvererbung oder Methoden-Überschreibungen ermöglicht Middleware Ihnen, Ihre Routen zu steuern, indem Sie Ihre benutzerdefinierte App-Logik zuweisen. Sie können Middleware wie ein Sandwich betrachten. Sie haben Brot außen und dann Schichten von Zutaten wie Salat, Tomaten, Fleisch und Käse. Stellen Sie sich vor, jede Anfrage ist wie ein Bissen des Sandwiches, bei dem Sie zuerst die äußeren Schichten essen und zum Kern vordringen.

Hier ist eine visuelle Darstellung, wie Middleware funktioniert. Dann zeigen wir Ihnen ein praktisches Beispiel, wie dies funktioniert.

Benutzeranfrage an URL /api ----> 
    Middleware->before() ausgeführt ----->
        Callable/ Methode an /api ausgeführt und Antwort generiert ------>
    Middleware->after() ausgeführt ----->
Benutzer erhält Antwort vom Server

Und hier ist ein praktisches Beispiel:

Benutzer navigiert zu URL /dashboard
    LoggedInMiddleware->before() wird ausgeführt
        before() prüft auf gültige angemeldete Sitzung
            wenn ja, nichts tun und Ausführung fortsetzen
            wenn nein, Benutzer zu /login umleiten
                Callable/ Methode an /api ausgeführt und Antwort generiert
    LoggedInMiddleware->after() hat nichts definiert, also lässt es die Ausführung fortfahren
Benutzer erhält Dashboard-HTML vom Server

Ausführungsreihenfolge

Middleware-Funktionen werden in der Reihenfolge ausgeführt, in der sie der Route hinzugefügt werden. Die Ausführung ähnelt der Art und Weise, wie Slim Framework dies handhabt.

before()-Methoden werden in der Reihenfolge ausgeführt, in der sie hinzugefügt wurden, und after()-Methoden werden in umgekehrter Reihenfolge ausgeführt.

Beispiel: Middleware1->before(), Middleware2->before(), Middleware2->after(), Middleware1->after().

Grundlegende Verwendung

Sie können Middleware als jede aufrufbare Methode verwenden, einschließlich einer anonymen Funktion oder einer Klasse (empfohlen).

Anonyme Funktion

Hier ist ein einfaches Beispiel:

Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
    echo 'Middleware first!';
});

Flight::start();

// Dies wird "Middleware first! Here I am!" ausgeben

Hinweis: Bei der Verwendung einer anonymen Funktion wird nur eine before()-Methode interpretiert. Sie können kein after()-Verhalten mit einer anonymen Klasse definieren.

Verwendung von Klassen

Middleware kann (und sollte) als Klasse registriert werden. Wenn Sie die "after"-Funktionalität benötigen, müssen Sie eine Klasse verwenden.

class MyMiddleware {
    public function before($params) {
        echo 'Middleware first!';
    }

    public function after($params) {
        echo 'Middleware last!';
    }
}

$MyMiddleware = new MyMiddleware();
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware); 
// auch ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);

Flight::start();

// Dies wird "Middleware first! Here I am! Middleware last!" anzeigen

Sie können auch nur den Klassenname der Middleware definieren, und sie wird die Klasse instanziieren.

Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class); 

Hinweis: Wenn Sie nur den Namen der Middleware übergeben, wird sie automatisch vom Dependency Injection Container ausgeführt, und die Middleware wird mit den Parametern ausgeführt, die sie benötigt. Wenn kein Dependency Injection Container registriert ist, wird standardmäßig die flight\Engine-Instanz in den __construct(Engine $app) übergeben.

Verwendung von Routen mit Parametern

Wenn Sie Parameter aus Ihrer Route benötigen, werden sie in einem einzelnen Array an Ihre Middleware-Funktion übergeben. (function($params) { ... } oder public function before($params) { ... }). Der Grund dafür ist, dass Sie Ihre Parameter in Gruppen strukturieren können und in einigen dieser Gruppen Ihre Parameter möglicherweise in einer anderen Reihenfolge erscheinen, was die Middleware-Funktion durch Verweis auf den falschen Parameter kaputt machen würde. Auf diese Weise können Sie sie nach Namen anstelle der Position zugreifen.

use flight\Engine;

class RouteSecurityMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $clientId = $params['clientId'];

        // jobId kann übergeben oder nicht übergeben werden
        $jobId = $params['jobId'] ?? 0;

        // vielleicht wenn es keine Job-ID gibt, müssen Sie nichts nachschlagen.
        if($jobId === 0) {
            return;
        }

        // Führen Sie eine Suche in Ihrer Datenbank durch
        $isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);

        if($isValid !== true) {
            $this->app->halt(400, 'You are blocked, muahahaha!');
        }
    }
}

// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {

    // Diese Gruppe unten erhält immer noch die Parent-Middleware
    // Aber die Parameter werden in einem einzelnen Array 
    // in der Middleware übergeben.
    $router->group('/job/@jobId', function(Router $router) {
        $router->get('', [ JobController::class, 'view' ]);
        $router->put('', [ JobController::class, 'update' ]);
        $router->delete('', [ JobController::class, 'delete' ]);
        // mehr Routen...
    });
}, [ RouteSecurityMiddleware::class ]);

Gruppierung von Routen mit Middleware

Sie können eine Route-Gruppe hinzufügen, und dann wird jede Route in dieser Gruppe dieselbe Middleware haben. Dies ist nützlich, wenn Sie eine Menge von Routen gruppieren müssen, z. B. mit einer Auth-Middleware, um den API-Schlüssel im Header zu prüfen.


// am Ende der group-Methode hinzugefügt
Flight::group('/api', function() {

    // Diese "leere" Route passt tatsächlich zu /api
    Flight::route('', function() { echo 'api'; }, false, 'api');
    // Dies passt zu /api/users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // Dies passt zu /api/users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

Wenn Sie eine globale Middleware auf alle Ihre Routen anwenden möchten, können Sie eine "leere" Gruppe hinzufügen:


// am Ende der group-Methode hinzugefügt
Flight::group('', function() {

    // Dies ist immer noch /users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // Und dies ist immer noch /users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // oder [ new ApiAuthMiddleware() ], dasselbe

Häufige Anwendungsfälle

API-Schlüssel-Validierung

Wenn Sie Ihre /api-Routen schützen möchten, indem Sie überprüfen, ob der API-Schlüssel korrekt ist, können Sie das leicht mit Middleware handhaben.

use flight\Engine;

class ApiMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $authorizationHeader = $this->app->request()->getHeader('Authorization');
        $apiKey = str_replace('Bearer ', '', $authorizationHeader);

        // Führen Sie eine Suche in Ihrer Datenbank für den API-Schlüssel durch
        $apiKeyHash = hash('sha256', $apiKey);
        $hasValidApiKey = !!$this->db()->fetchField("SELECT 1 FROM api_keys WHERE hash = ? AND valid_date >= NOW()", [ $apiKeyHash ]);

        if($hasValidApiKey !== true) {
            $this->app->jsonHalt(['error' => 'Invalid API Key']);
        }
    }
}

// routes.php
$router->group('/api', function(Router $router) {
    $router->get('/users', [ ApiController::class, 'getUsers' ]);
    $router->get('/companies', [ ApiController::class, 'getCompanies' ]);
    // mehr Routen...
}, [ ApiMiddleware::class ]);

Jetzt sind alle Ihre API-Routen durch diese API-Schlüssel-Validierungs-Middleware geschützt, die Sie eingerichtet haben! Wenn Sie mehr Routen in die Router-Gruppe einfügen, erhalten sie sofort denselben Schutz!

Anmeldungs-Validierung

Möchten Sie einige Routen schützen, damit sie nur für angemeldete Benutzer verfügbar sind? Das kann leicht mit Middleware erreicht werden!

use flight\Engine;

class LoggedInMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $session = $this->app->session();
        if($session->get('logged_in') !== true) {
            $this->app->redirect('/login');
            exit;
        }
    }
}

// routes.php
$router->group('/admin', function(Router $router) {
    $router->get('/dashboard', [ DashboardController::class, 'index' ]);
    $router->get('/clients', [ ClientController::class, 'index' ]);
    // mehr Routen...
}, [ LoggedInMiddleware::class ]);

Route-Parameter-Validierung

Möchten Sie Ihre Benutzer schützen, indem Sie verhindern, dass sie Werte in der URL ändern, um auf Daten zuzugreifen, die sie nicht sollten? Das kann mit Middleware gelöst werden!

use flight\Engine;

class RouteSecurityMiddleware {

    protected Engine $app;

    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function before(array $params) {
        $clientId = $params['clientId'];
        $jobId = $params['jobId'];

        // Führen Sie eine Suche in Ihrer Datenbank durch
        $isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);

        if($isValid !== true) {
            $this->app->halt(400, 'You are blocked, muahahaha!');
        }
    }
}

// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
    $router->get('', [ JobController::class, 'view' ]);
    $router->put('', [ JobController::class, 'update' ]);
    $router->delete('', [ JobController::class, 'delete' ]);
    // mehr Routen...
}, [ RouteSecurityMiddleware::class ]);

Handhabung der Middleware-Ausführung

Sagen wir, Sie haben eine Auth-Middleware und möchten den Benutzer auf eine Login-Seite umleiten, wenn er nicht authentifiziert ist. Sie haben ein paar Optionen zur Verfügung:

  1. Sie können false von der Middleware-Funktion zurückgeben, und Flight gibt automatisch einen 403 Forbidden-Fehler zurück, aber ohne Anpassung.
  2. Sie können den Benutzer auf eine Login-Seite umleiten mit Flight::redirect().
  3. Sie können einen benutzerdefinierten Fehler in der Middleware erstellen und die Ausführung der Route stoppen.

Einfach und Unkompliziert

Hier ist ein einfaches return false;-Beispiel:

class MyMiddleware {
    public function before($params) {
        $hasUserKey = Flight::session()->exists('user');
        if ($hasUserKey === false) {
            return false;
        }

        // da es wahr ist, läuft alles einfach weiter
    }
}

Umleitungs-Beispiel

Hier ist ein Beispiel für die Umleitung des Benutzers auf eine Login-Seite:

class MyMiddleware {
    public function before($params) {
        $hasUserKey = Flight::session()->exists('user');
        if ($hasUserKey === false) {
            Flight::redirect('/login');
            exit;
        }
    }
}

Benutzerdefiniertes Fehler-Beispiel

Sagen wir, Sie müssen einen JSON-Fehler werfen, weil Sie eine API bauen. Sie können das so tun:

class MyMiddleware {
    public function before($params) {
        $authorization = Flight::request()->getHeader('Authorization');
        if(empty($authorization)) {
            Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
            // oder
            Flight::json(['error' => 'You must be logged in to access this page.'], 403);
            exit;
            // oder
            Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']));
        }
    }
}

Siehe auch

Fehlerbehebung

Änderungsprotokoll

Learn/filtering

Filtering

Überblick

Flight ermöglicht es Ihnen, gemappte Methoden vor und nach ihrem Aufruf zu filtern.

Verständnis

Es gibt keine vordefinierten Hooks, die Sie merken müssen. Sie können alle Standard-Framework-Methoden sowie alle benutzerdefinierten Methoden filtern, die Sie gemappt haben.

Eine Filterfunktion sieht so aus:

/**
 * @param array $params Die an die gefilterte Methode übergebenen Parameter.
 * @param string $output (nur v2 Output Buffering) Die Ausgabe der gefilterten Methode.
 * @return bool Geben Sie true/void zurück oder geben Sie nichts zurück, um die Kette fortzusetzen, false, um die Kette zu unterbrechen.
 */
function (array &$params, string &$output): bool {
  // Filtercode
}

Mit den übergebenen Variablen können Sie die Eingabeparameter und/oder die Ausgabe manipulieren.

Sie können einen Filter vor einer Methode ausführen, indem Sie Folgendes tun:

Flight::before('start', function (array &$params, string &$output): bool {
  // Etwas tun
});

Sie können einen Filter nach einer Methode ausführen, indem Sie Folgendes tun:

Flight::after('start', function (array &$params, string &$output): bool {
  // Etwas tun
});

Sie können so viele Filter wie gewünscht zu jeder Methode hinzufügen. Sie werden in der Reihenfolge aufgerufen, in der sie deklariert wurden.

Hier ist ein Beispiel für den Filterprozess:

// Eine benutzerdefinierte Methode mappen
Flight::map('hello', function (string $name) {
  return "Hello, $name!";
});

// Einen Before-Filter hinzufügen
Flight::before('hello', function (array &$params, string &$output): bool {
  // Den Parameter manipulieren
  $params[0] = 'Fred';
  return true;
});

// Einen After-Filter hinzufügen
Flight::after('hello', function (array &$params, string &$output): bool {
  // Die Ausgabe manipulieren
  $output .= " Have a nice day!";
  return true;
});

// Die benutzerdefinierte Methode aufrufen
echo Flight::hello('Bob');

Dies sollte anzeigen:

Hello Fred! Have a nice day!

Wenn Sie mehrere Filter definiert haben, können Sie die Kette unterbrechen, indem Sie false in einer Ihrer Filterfunktionen zurückgeben:

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'one';
  return true;
});

Flight::before('start', function (array &$params, string &$output): bool {
  echo 'two';

  // Dies beendet die Kette
  return false;
});

// Dies wird nicht aufgerufen
Flight::before('start', function (array &$params, string &$output): bool {
  echo 'three';
  return true;
});

Hinweis: Kernmethoden wie map und register können nicht gefiltert werden, da sie direkt aufgerufen und nicht dynamisch aufgerufen werden. Siehe Erweiterung von Flight für weitere Informationen.

Siehe auch

Fehlerbehebung

Changelog

Learn/autoloading

Autoloading

Übersicht

Autoloading ist ein Konzept in PHP, bei dem Sie ein Verzeichnis oder mehrere Verzeichnisse angeben, aus denen Klassen geladen werden. Dies ist viel vorteilhafter als require oder include zum Laden von Klassen zu verwenden. Es ist auch eine Voraussetzung für die Verwendung von Composer-Paketen.

Ein korrektes Autoloading ist auch für KI-gestützte Entwicklung wichtig: Agents legen Dateien dort ab, wohin der Namespace zeigt. Wenn die Groß-/Kleinschreibung von Ordnern und Namespaces nicht übereinstimmt, treten unter Linux Fehler wie „Klasse nicht gefunden" auf, selbst wenn die Dinge auf einer Mac-Festplatte ohne Beachtung der Groß-/Kleinschreibung funktioniert haben.

Verständnis

Standardmäßig wird jede Flight-Klasse dank Composer automatisch für Sie geladen. Für Ihre Anwendungsklassen gibt es zwei gängige Ansätze:

  1. Composer PSR-4 (was das offizielle Grundgerüst verwendet): Ordnen Sie ein Namespace-Präfix einem Verzeichnis in composer.json zu und führen Sie dann composer dump-autoload aus.
  2. Flight::path(): Weisen Sie den Loader von Flight auf Verzeichnisse hin (praktisch für einfache Apps oder wenn Sie Composer nicht für Anwendungscode verwenden).

Die Verwendung eines Autoloaders vereinfacht Ihren Code erheblich. Anstatt einer Wand von include / require am Anfang jeder Datei werden Klassen geladen, wenn Sie sie zum ersten Mal verwenden.

Groß-/Kleinschreibung (lesen Sie dies zweimal)

Namespaces müssen mit der Verzeichnisstruktur und der Groß-/Kleinschreibung dieser Verzeichnisse übereinstimmen.

Funktioniert Schlägt unter Linux fehl
App\Controller\HomeController → app/Controller/HomeController.php App\Controller\… mit Ordner app/controllers/
app\controllers\MyController → app/controllers/MyController.php Mischung aus App\ mit kleingeschriebenem controllers

PHP-Namespaces sind in manchen Kontexten case-insensitiv (Groß-/Kleinschreibung wird ignoriert), aber Composer und das Dateisystem sind es nicht. Das offizielle Grundgerüst standardisiert auf:

Ältere Dokumentationen und Beispiele aus der Community verwendeten manchmal kleingeschriebenes app\controllers. Das funktioniert weiterhin, wenn Ihre Ordner kleingeschrieben sind – aber neue Skeleton-Projekte verwenden App\ + PascalCase-Ordner. Wählen Sie eine Konvention pro Projekt und bleiben Sie dabei, damit Menschen und KI-Tools kein zweites Layout erfinden.

Skeleton (empfohlen für neue Projekte)

Nach composer create-project flightphp/skeleton wird Anwendungscode über Composer automatisch geladen – für App\-Klassen ist kein Flight::path() erforderlich:

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}
// app/Controller/HomeController.php
namespace App\Controller;

use flight\Engine;

class HomeController
{
    protected Engine $app;

    public function __construct(Engine $app)
    {
        $this->app = $app;
    }

    public function index(): void
    {
        $this->app->render('welcome', ['message' => 'Hello!']);
    }
}
// app/config/routes.php – Dice löst App\Controller\… über den Container auf
$router->get('/', [HomeController::class, 'index']);

Siehe Installation für den vollständigen Verzeichnisbaum und KI & Entwicklererfahrung für die Dokumentation dieses Layouts für Programmierassistenten in AGENTS.md.

Grundlegende Verwendung (Flight::path())

Nehmen wir an, wir haben einen Verzeichnisbaum wie den folgenden:

# Beispielpfad
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers – enthält die Controller für dieses Projekt
│   ├── translations
│   ├── UTILS – enthält Klassen nur für diese Anwendung (dies ist absichtlich in Großbuchstaben für ein späteres Beispiel)
│   └── views
└── public
    └── css
    └── js
    └── index.php

Ihnen ist vielleicht aufgefallen, dass dies einem typischen App-Verzeichnisbaum ähnelt (die Dokumentationsseite selbst verwendet eine strukturierte Darstellung). Kleingeschriebenes controllers ist hier eine bewusste Wahl – es ist nur nicht die aktuelle Standardeinstellung des Skeletons.

Sie können jedes Verzeichnis zum Laden wie folgt angeben:


/**
 * public/index.php
 */

// Fügen Sie einen Pfad zum Autoloader hinzu
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');


/**
 * app/controllers/MyController.php
 */

// Keine Namespaces erforderlich

// Alle automatisch geladenen Klassen sollten in Pascal Case sein (jedes Wort großgeschrieben, keine Leerzeichen)
class MyController {

    public function index() {
        // etwas tun
    }
}

Namespaces mit Flight::path()

Wenn Sie Namespaces verwenden, wird die Implementierung tatsächlich sehr einfach. Sie sollten die Methode Flight::path() verwenden, um das Wurzelverzeichnis (nicht das Dokumentenwurzelverzeichnis oder den public/-Ordner) Ihrer Anwendung anzugeben.


/**
 * public/index.php
 */

// Fügen Sie einen Pfad zum Autoloader hinzu
Flight::path(__DIR__.'/../');

So könnte Ihr Controller nun aussehen. Schauen Sie sich das folgende Beispiel an, aber achten Sie auf die Kommentare mit wichtigen Informationen.

/**
 * app/controllers/MyController.php
 */

// Namespaces sind erforderlich
// Namespaces entsprechen der Verzeichnisstruktur
// Namespaces müssen die gleiche Groß-/Kleinschreibung wie die Verzeichnisstruktur verwenden
// Namespaces und Verzeichnisse dürfen keine Unterstriche enthalten (außer wenn Loader::setV2ClassLoading(false) gesetzt ist)
namespace app\controllers;

// Alle automatisch geladenen Klassen sollten in Pascal Case sein (jedes Wort großgeschrieben, keine Leerzeichen)
// Ab 3.7.2 können Sie Pascal_Snake_Case für Ihre Klassennamen verwenden, indem Sie Loader::setV2ClassLoading(false); ausführen.
class MyController {

    public function index() {
        // etwas tun
    }
}

Und wenn Sie eine Klasse in Ihrem utils-Verzeichnis automatisch laden möchten, gehen Sie im Grunde genauso vor:


/**
 * app/UTILS/ArrayHelperUtil.php
 */

// Namespace muss mit der Verzeichnisstruktur und der Groß-/Kleinschreibung übereinstimmen (beachten Sie, dass das UTILS-Verzeichnis komplett groß geschrieben ist
//     wie im obigen Verzeichnisbaum)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // etwas tun
    }
}

Skeleton-Stil-Namespace (gleiche Regeln, andere Groß-/Kleinschreibung)

/**
 * app/Controller/MyController.php
 */
namespace App\Controller;

class MyController {
    // ...
}

Die Regel hat sich nicht geändert – nur die vom Skeleton gewählte Groß-/Kleinschreibung von Ordnern und Namespaces. Egal welche Groß-/Kleinschreibung Ihre Ordner verwenden, Ihre namespace-Zeile muss übereinstimmen.

Unterstriche in Klassennamen

Ab 3.7.2 können Sie Pascal_Snake_Case für Ihre Klassennamen verwenden, indem Sie Loader::setV2ClassLoading(false); ausführen. Dadurch können Sie Unterstriche in Ihren Klassennamen verwenden. Dies wird nicht empfohlen, ist aber für diejenigen verfügbar, die es benötigen.

use flight\core\Loader;

/**
 * public/index.php
 */

// Fügen Sie einen Pfad zum Autoloader hinzu
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);

/**
 * app/controllers/My_Controller.php
 */

// Keine Namespaces erforderlich

class My_Controller {

    public function index() {
        // etwas tun
    }
}

Siehe auch

Fehlerbehebung

Wenn Sie nicht herausfinden können, warum Ihre Namespace-Klassen nicht gefunden werden, denken Sie daran: Verwenden Sie mit Flight::path() das Projektwurzelverzeichnis (oder die korrekte Basis für Ihren Namespace), nicht nur einen verschachtelten Ordner, den Sie im Namespace vergessen haben zu spiegeln.

Führen Sie bei Composer PSR-4 nach Änderungen an den composer.json-Zuordnungen composer dump-autoload aus.

Bei Linux-CI oder Produktion ist eine falsche Groß-/Kleinschreibung von Ordnern ein sehr häufiger Fall von „Funktioniert auf meinem Rechner"-Fehlern.

Klasse nicht gefunden (Autoloading funktioniert nicht)

Es kann mehrere Gründe geben, warum dies nicht funktioniert. Im Folgenden finden Sie einige Beispiele.

Falscher Dateiname

Der häufigste Grund ist, dass der Klassenname nicht mit dem Dateinamen übereinstimmt.

Wenn Sie eine Klasse mit dem Namen MyClass haben, sollte die Datei MyClass.php heißen. Wenn Sie eine Klasse mit dem Namen MyClass haben und die Datei myclass.php heißt, kann der Autoloader sie nicht finden.

Falscher Namespace oder falsche Groß-/Kleinschreibung des Ordners

Wenn Sie Namespaces verwenden, sollte der Namespace der Verzeichnisstruktur entsprechen, einschließlich der Groß-/Kleinschreibung.

// ...code...

// wenn sich Ihr MyController in app/Controller (Skeleton) befindet und App\Controller als Namespace hat
// das funktioniert nicht:
Flight::route('/hello', 'MyController->hello');

// Skeleton-Stil:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);

// Älteres Layout mit Kleinbuchstaben (nur wenn Ihre Ordner tatsächlich app/controllers heißen):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// oder voll qualifiziert:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);

path() nicht definiert (Anwendungscode ohne Composer)

Wenn Sie für Anwendungsklassen auf Flight::path() anstelle von Composer setzen, definieren Sie den Pfad vor Routen, die diese Klassen referenzieren (oft früh im Bootstrap / in public/index.php):

// Einen Pfad zum Autoloader hinzufügen (Projektwurzelverzeichnis für Apps mit Namespaces)
Flight::path(__DIR__.'/../');

Das offizielle Skeleton verwendet hauptsächlich Composer PSR-4 für App\, daher benötigen Sie dort normalerweise kein Flight::path() für Controller und Modelle.

Änderungsprotokoll