Vor ein paar Tagen habe ich einen fehlerhaften Test repariert und es stellte sich heraus, dass ich einige eindeutige und gültige Werte in meiner Fabrik brauchte. Laravel umschließt FakerPHP, auf das wir normalerweise über den fake()
Helfer zugreifen. FakerPHP kommt mit Modifikatoren wie valid()
und unique()
, aber Sie können immer nur einen gleichzeitig verwenden, Sie können also nicht fake()->unique()->valid()
verwenden, was genau das ist, was ich brauchte.
Das brachte mich auf die Idee, was wäre, wenn wir unseren eigenen Modifikator erstellen möchten? Zum Beispiel uniqueAndValid()
oder einen anderen Modifikator. Wie können wir das Framework erweitern ?
Ich werde meinen Gedankengang einfach loswerden.
Bevor ich mich auf eine übertriebene Lösung stürze, möchte ich immer prüfen, ob es eine einfachere Option gibt und verstehen, womit ich es zu tun habe. Werfen wir also einen Blick auf den fake()
-Helfer:
function fake($locale = null) { if (app()->bound('config')) { $locale ??= app('config')->get('app.faker_locale'); } $locale ??= 'en_US'; $abstract = \Faker\Generator::class.':'.$locale; if (! app()->bound($abstract)) { app()->singleton($abstract, fn () => \Faker\Factory::create($locale)); } return app()->make($abstract); }
Beim Lesen des Codes können wir sehen, dass Laravel ein Singleton an den Container bindet. Wenn wir uns jedoch die Zusammenfassung ansehen, handelt es sich um eine reguläre Klasse, die keine Schnittstelle implementiert, und das Objekt wird über eine Fabrik erstellt. Das macht die Sache komplizierter. Warum?
Denn wenn es eine Schnittstelle wäre, könnten wir einfach eine neue Klasse erstellen, die die Basisklasse \Faker\Generator
erweitert, einige neue Funktionen hinzufügen und sie erneut an den Container binden. Aber diesen Luxus können wir uns nicht leisten.
Es ist eine Factory beteiligt. Das bedeutet, dass es sich nicht um eine einfache Instanziierung handelt; es wird eine Logik ausgeführt. In diesem Fall fügt die Factory einige Anbieter hinzu (PhoneNumber, Text, UserAgent usw.). Selbst wenn wir also versuchen, erneut zu binden, müssen wir die Factory verwenden, die den ursprünglichen \Faker\Generator
zurückgibt.
Lösungen 🤔? Man könnte denken: „Was hält uns davon ab, unsere eigene Fabrik zu erstellen, die den neuen Generator zurückgibt, wie in Punkt 1 beschrieben?“ Nun, nichts, das können wir tun, aber wir werden es nicht tun! Wir verwenden ein Framework aus mehreren Gründen, einer davon sind Updates. Was passiert, wenn FakerPHP einen neuen Anbieter hinzufügt oder ein größeres Upgrade durchführt? Laravel passt den Code an, und Leute, die keine Änderungen vorgenommen haben, werden nichts bemerken. Wir würden jedoch außen vor bleiben, und unser Code könnte sogar (höchstwahrscheinlich) kaputtgehen. Also, ja, so weit wollen wir nicht gehen.
Nachdem wir nun die grundlegenden Optionen erkundet haben, können wir über fortgeschrittenere nachdenken, wie etwa Designmuster. Wir brauchen keine exakte Implementierung, nur etwas, das unserem Problem vertraut ist. Deshalb sage ich immer, dass es gut ist, sie zu kennen. In diesem Fall können wir die Generator
„verschönern“, indem wir neue Funktionen hinzufügen und gleichzeitig die alten beibehalten. Klingt gut? Mal sehen, wie!
Lassen Sie uns zunächst eine neue Klasse erstellen, FakerGenerator
:
<?php namespace App\Support; use Closure; use Faker\Generator; use Illuminate\Support\Traits\ForwardsCalls; class FakerGenerator { use ForwardsCalls; public function __construct(private readonly Generator $generator) { } public function uniqueAndValid(Closure $validator = null): UniqueAndValidGenerator { return new UniqueAndValidGenerator($this->generator, $validator); } public function __call($method, $parameters): mixed { return $this->forwardCallTo($this->generator, $method, $parameters); } }
Dies wird unser „Dekorator“ (irgendwie). Es ist eine einfache Klasse, die den Generator
als Abhängigkeit erwartet und einen neuen Modifikator einführt, uniqueAndValid()
. Sie verwendet auch das ForwardsCalls
Merkmal von Laravel, das es ermöglicht, Aufrufe an das Basisobjekt weiterzuleiten.
Dieses Merkmal hat zwei Methoden:
forwardCallTo
undforwardDecoratedCallTo
. Verwenden Sie letztere, wenn Sie Methoden auf dem dekorierten Objekt verketten möchten. In unserem Fall haben wir immer einen einzelnen Aufruf.
Wir müssen auch den UniqueAndValidGenerator
implementieren, den benutzerdefinierten Modifikator, aber darum geht es in diesem Artikel nicht. Wenn Sie an der Implementierung interessiert sind: Diese Klasse ist im Grunde eine Mischung aus dem ValidGenerator und dem UniqueGenerator , die mit FakerPHP geliefert werden. Sie finden sie hier .
Lassen Sie uns nun das Framework im AppServiceProvider
erweitern :
<?php namespace App\Providers; use Closure; use Faker\Generator; use App\Support\FakerGenerator; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->extend( $this->fakerAbstractName(), fn (Generator $base) => new FakerGenerator($base) ); } private function fakerAbstractName(): string { // This is important, it matches the name bound by the fake() helper return Generator::class . ':' . app('config')->get('app.faker_locale'); } }
Die Methode extend()
prüft, ob ein Abstract mit dem angegebenen Namen an den Container gebunden wurde. Wenn ja, überschreibt sie ihren Wert mit dem Ergebnis der Closure. Sehen Sie sich das an:
// Laravel: src/Illuminate/Container/Container.php public function extend($abstract, Closure $closure) { $abstract = $this->getAlias($abstract); if (isset($this->instances[$abstract])) { // You are interested here $this->instances[$abstract] = $closure($this->instances[$abstract], $this); $this->rebound($abstract); } else { $this->extenders[$abstract][] = $closure; if ($this->resolved($abstract)) { $this->rebound($abstract); } } }
Aus diesem Grund haben wir die Methode fakerAbstractName()
definiert, die denselben Namen generiert, den die Hilfsbindungen von fake()
im Container herstellen.
Überprüfen Sie den obigen Code noch einmal. Wenn Sie ihn übersehen haben, habe ich einen Kommentar hinterlassen.
Jetzt wird jedes Mal, wenn wir fake()
aufrufen, eine Instanz von FakerGenerator
zurückgegeben und wir haben Zugriff auf den benutzerdefinierten Modifikator, den wir eingeführt haben. Jedes Mal, wenn wir einen Aufruf ausführen, der in der FakerGenerator
Klasse nicht vorhanden ist, wird __call()
ausgelöst und leitet ihn mithilfe der Methode forwardCallTo()
an den Generator
weiter.
Das ist es! Endlich kann ich fake()->uniqueAndValid()->randomElement()
ausführen und es funktioniert wie am Schnürchen!
Bevor wir zum Schluss kommen, möchte ich darauf hinweisen, dass dies kein reines Dekorationsmuster ist. Muster sind jedoch keine heiligen Texte; passen Sie sie Ihren Bedürfnissen an und lösen Sie das Problem.
Frameworks sind unglaublich hilfreich und Laravel verfügt über viele integrierte Funktionen. Sie können jedoch nicht alle Randfälle in Ihren Projekten abdecken und manchmal geraten Sie in eine Sackgasse. In diesem Fall können Sie das Framework jederzeit erweitern. Wir haben gesehen, wie einfach es ist, und ich hoffe, Sie haben die Hauptidee verstanden, die über dieses Faker-Beispiel hinaus gilt.
Beginnen Sie immer einfach und suchen Sie nach der einfachsten Lösung für das Problem. Komplexität entsteht, wenn sie nötig ist. Wenn also die grundlegende Vererbung funktioniert, müssen Sie keinen Dekorator oder ähnliches implementieren. Wenn Sie das Framework erweitern, achten Sie darauf, dass Sie nicht zu weit gehen, sodass der Verlust den Gewinn überwiegt. Sie möchten nicht am Ende einen Teil des Frameworks selbst pflegen müssen.