Dieser Text ist eine Anpassung eines Teils des Hanami-Framework-Handbuchs für das Laravel-Framework. Was hat das Interesse an diesem speziellen Material geweckt? Es enthält eine schrittweise Beschreibung mit einer Demonstration der gängigen Funktionen für Programmiersprachen und Frameworks wie:
- Verwenden des Interactors-Musters.
- Demonstration von TDD \ BDD.
Es sollte sofort angemerkt werden, dass dies nicht nur unterschiedliche Frameworks mit unterschiedlichen Ideologien (insbesondere in Bezug auf ORM) sind, sondern auch unterschiedliche Programmiersprachen, von denen jede ihre eigene spezifische Kultur hat und aus historischen Gründen "Best Practices" etabliert hat. Verschiedene Programmiersprachen und Frameworks leihen sich in der Regel die erfolgreichsten Lösungen voneinander aus. Daher unterscheiden sich die grundlegenden Dinge trotz unterschiedlicher Details nicht, es sei denn, wir nehmen PL mit einem anfangs anderen Paradigma. Es ist interessant genug zu vergleichen, wie ein und dasselbe Problem in verschiedenen Ökosystemen gelöst wird.
Zunächst haben wir also das Hanami-Framework (Ruby-Framework) - ein ziemlich neues Framework, das sich ideologisch mehr für Symfony interessiert, mit ORM "on Repositories". Und das Ziel-Laravel \ Lumen (PHP) -Framework mit Active Record.
Bei der Anpassung wurden die spitzesten Winkel geschnitten:
- Der erste Teil des Handbuchs wurde mit der Initialisierung des Projekts, einer Beschreibung der Merkmale des Frameworks und ähnlichen spezifischen Dingen weggelassen.
- ORM Eloquent wird auf den Globus gezogen und dient auch als Repository.
- Schritte zum Generieren von Code und Vorlagen zum Senden von E-Mails.
Gespeichert und hervorgehoben auf:
- Interaktoren - Auf der Schnittstelle wurde eine minimal angemessene Implementierung vorgenommen.
- Tests, schrittweise Entwicklung durch TDD.
Der erste Teil des ursprünglichen Hanami-Tutorials, auf den im folgenden Text verwiesen wird.
Ursprünglicher interaktiver Tutorial-Text
Link zum Repository mit angepasstem PHP-Code am Ende des Textes.
Interaktoren
Neue Funktion: E-Mail-Benachrichtigungen
Funktionsszenario: Wenn ich als Administrator ein Buch hinzufüge, möchte ich E-Mail-Benachrichtigungen erhalten.
Da die Anwendung nicht authentifiziert ist, kann jeder ein neues Buch hinzufügen. Wir werden die E-Mail-Adresse des Administrators über Umgebungsvariablen angeben.
Dies ist nur ein Beispiel, das zeigt, wann Interaktoren verwendet werden müssen und insbesondere wie der Hanami-Interaktor verwendet werden soll.
Dieses Beispiel kann als Grundlage für andere Funktionen dienen, z. B. die Bestätigung neuer Bücher durch den Administrator, bevor sie veröffentlicht werden. Oder geben Sie Benutzern die Möglichkeit, eine E-Mail-Adresse anzugeben, um das Buch über einen speziellen Link zu bearbeiten.
In der Praxis können Sie Interaktoren verwenden, um jede von der Netzwerkschicht abstrahierte Geschäftslogik zu implementieren. Dies ist besonders nützlich, wenn Sie mehrere Dinge kombinieren möchten, um die Komplexität der Codebasis zu steuern.
Sie werden verwendet, um nicht triviale Geschäftslogik nach dem Prinzip der Einzelverantwortung zu isolieren.
In Webanwendungen werden sie normalerweise aus Controller-Aktionen verwendet. Auf diese Weise trennen Sie Aufgaben, Geschäftslogikobjekte und Interaktoren, die nichts über die Netzwerkschicht der Anwendung wissen.
Rückrufe? Wir brauchen sie nicht!
Der einfachste Weg, eine E-Mail-Benachrichtigung zu implementieren, besteht darin, einen Rückruf hinzuzufügen.
Das heißt, nach dem Erstellen eines neuen Bucheintrags in der Datenbank wird eine E-Mail gesendet.
Architektonisch bietet Hanami keinen solchen Mechanismus. Dies liegt daran, dass wir Modellrückrufe als Anti-Muster betrachten. Sie verstoßen gegen den Grundsatz der alleinigen Verantwortung. In unserem Fall mischen sie die Persistenzschicht falsch mit E-Mail-Benachrichtigungen.
Während des Tests (und höchstwahrscheinlich in einigen anderen Fällen) möchten Sie den Rückruf überspringen. Dies ist schnell verwirrend, da mehrere Rückrufe für ein einzelnes Ereignis in einer bestimmten Reihenfolge ausgelöst werden können. Außerdem können Sie einige Rückrufe überspringen. Rückrufe machen Code zerbrechlich und schwer verständlich.
Stattdessen empfehlen wir explizit statt implizit.
Ein Interaktor ist ein Objekt, das einen bestimmten Anwendungsfall darstellt.
Sie ermöglichen jeder Klasse eine einzige Verantwortung. Es liegt in der alleinigen Verantwortung des Interaktors, die Objekte und Methodenaufrufe zu kombinieren, um ein bestimmtes Ergebnis zu erzielen.
Idee
Die Hauptidee der Interaktoren besteht darin, dass Sie die isolierten Teile der Funktionalität in eine neue Klasse extrahieren.
Sie sollten nur zwei öffentliche Methoden schreiben: __construct
und call
.
In der PHP-Implementierung des Interaktors hat die __invoke
den geschützten Modifikator und wird über __invoke
.
Dies bedeutet, dass solche Objekte leicht zu interpretieren sind, da nur eine Methode zur Verwendung verfügbar ist.
Das Einkapseln des Verhaltens in ein Objekt erleichtert das Testen. Dies erleichtert auch das Verständnis Ihrer Codebasis und lässt nicht nur implizit verborgene Komplexität zurück.
Vorbereitung
Angenommen, wir haben unsere Bücherregalanwendung "Erste Schritte " und möchten die Funktion "E-Mail-Benachrichtigung für hinzugefügtes Buch" hinzufügen.
Einen Interaktor schreiben
Erstellen wir einen Ordner für unsere Interaktoren und einen Ordner für ihre Tests:
$ mkdir lib/bookshelf/interactors $ mkdir tests/bookshelf/interactors
Wir legen sie in lib/bookshelf
da sie nicht mit der Webanwendung zusammenhängen. Sie können Bücher später über das Admin-Portal, die API oder sogar ein Befehlszeilenprogramm hinzufügen.
Fügen Sie den AddBook
Interaktor hinzu und schreiben Sie einen neuen Test test tests/bookshelf/interactors/AddBookTest.php
:
AddBook
eine AddBook
wird ein Fehler " Class does not exist
AddBook
da keine AddBook
Klasse vorhanden ist. Erstellen wir diese Klasse in der Datei lib/bookshelf/interactors/AddBook.php
:
<?php namespace Lib\Bookshelf\Interactors; use Lib\Interactor\Interactor; class AddBook { use Interactor; public function __construct() { } protected function call() { } }
Es gibt nur zwei Methoden, die diese Klasse enthalten sollte: __construct
zum Festlegen von Daten und call
zum Implementieren des Skripts.
Diese Methoden, insbesondere call
, sollten private Methoden call
, die Sie schreiben.
Standardmäßig wird das Ergebnis als erfolgreich angesehen, da wir nicht explizit angegeben haben, dass der Vorgang fehlgeschlagen ist.
Lassen Sie uns den Test ausführen:
$ phpunit
Alle Tests müssen bestanden werden!
Lassen Sie uns AddBook
unseren AddBook
Interaktor wirklich etwas tun!
Bucherstellung
Ändern Sie tests/bookshelf/interactors/AddBookTest.php
:
public function testCreateBook() { $result = $this->subjectCall(); $this->assertEquals("The Fire Next Time", $result->book->title); $this->assertEquals("James Baldwin", $result->book->author); }
Wenn Sie phpunit
Tests phpunit
, wird ein Fehler phpunit
:
Exception: Undefined property Lib\Interactor\InteractorResult::$book
Füllen wir unseren Interaktor aus und erklären dann, was wir getan haben:
<?php namespace Lib\Bookshelf\Interactors; use Lib\Interactor\Interactor; use Lib\Bookshelf\Book; class AddBook { use Interactor; protected static $expose = ["book"]; private $book = null; public function __construct() { } protected function call($bookAttributes) { $this->book = new Book($bookAttributes); } }
Zwei wichtige Dinge, die hier zu beachten sind:
String protected static $expose = ["book"];
Fügt die Bucheigenschaft zum Ergebnisobjekt hinzu, das beim Aufruf des Interaktors zurückgegeben wird.
Die call
weist das Buchmodell der Book
zu, die als Ergebnis verfügbar sein wird.
Jetzt sollten die Tests bestehen.
Wir haben das Buchmodell initialisiert, es ist jedoch nicht in der Datenbank gespeichert.
Buch speichern
Wir haben ein neues Buch, das aus dem Titel und dem Autor stammt, aber es ist noch nicht in der Datenbank.
Wir müssen unser BookRepository
, um es zu speichern.
Wenn Sie die Tests ausführen, wird ein neuer Fehler mit der Meldung " Failed asserting that null is not null
.
Dies liegt daran, dass das von uns erstellte Buch keine Kennung hat, da es diese nur erhält, wenn es gespeichert wird.
Damit der Test bestanden werden kann, müssen wir ein gespeichertes Buch erstellen. Ein anderer, nicht weniger korrekter Weg besteht darin, das Buch zu speichern, das wir bereits haben.
Bearbeiten Sie die lib/bookshelf/interactors/AddBook.php
:
protected function call($bookAttributes) { $this->book = Book::create($bookAttributes); }
Anstatt ein new Book
aufzurufen, führen wir Book::create
mit den Attributen des Buches aus.
Die Methode gibt das Buch weiterhin zurück und speichert diesen Datensatz auch in der Datenbank.
Wenn Sie die Tests jetzt ausführen, werden Sie feststellen, dass alle Tests bestanden wurden.
Abhängigkeitsinjektion
Lassen Sie uns die Abhängigkeitsinjektion umgestalten.
Tests funktionieren immer noch, hängen jedoch von den Funktionen zum Speichern in der Datenbank ab (die ID-Eigenschaft wird nach erfolgreichem Speichern festgelegt). Dies ist ein Implementierungsdetail der Funktionsweise der Erhaltung. Wenn Sie beispielsweise vor dem Speichern eine UUID erstellen möchten und angeben möchten, dass das Speichern auf andere Weise als durch Ausfüllen der ID-Spalte erfolgreich war, müssen Sie diesen Test ändern.
Wir können unseren Test und den Interaktor modifizieren, um ihn zuverlässiger zu machen: Er ist weniger anfällig für Brüche aufgrund von Änderungen außerhalb seiner Datei.
So können wir die Abhängigkeitsinjektion im Interaktor verwenden:
Im Wesentlichen ist dies mit etwas mehr Code dasselbe, um eine repository
Eigenschaft zu erstellen.
Im Moment überprüft der Test das Verhalten der create
Methode, um sicherzustellen, dass ihre Kennung mit $this->assertNotNull($result->book->id)
gefüllt ist.
Dies ist ein Implementierungsdetail.
Stattdessen können wir den Test ändern, um sicherzustellen, dass die Methode create
im Repository aufgerufen wurde, und darauf vertrauen, dass das Repository das Objekt speichert (da dies in seiner Verantwortung liegt).
Lassen Sie uns den testPersistsBook
Test ändern:
Jetzt verletzt unser Test nicht die Grenzen seiner Zone.
Wir haben lediglich die Interaktorabhängigkeit zum Repository hinzugefügt.
E-Mail-Benachrichtigung
Fügen wir eine E-Mail-Benachrichtigung hinzu!
Sie können hier auch alles tun, z. B. eine SMS senden, eine Chat-Nachricht senden oder einen Web-Hook aktivieren.
Wir werden den Nachrichtentext leer lassen, aber im Betrefffeld geben wir "Buch hinzugefügt!" An.
Erstellen Sie einen Benachrichtigungstest tests/bookshelf/mail/BookAddedNotificationTest.php
:
<?php use Lib\Bookshelf\Mail\BookAddedNotification; use Illuminate\Support\Facades\Mail; class BookAddedNotificationTest extends TestCase { public function setUp() { parent::setUp(); Mail::fake(); $this->mail = new BookAddedNotification(); } public function testCorrectAttributes() { $this->mail->build(); $this->assertEquals('no-reply@example.com', $this->mail->from[0]['address']); $this->assertEquals('admin@example.com', $this->mail->to[0]['address']); $this->assertEquals('Book added!', $this->mail->subject); } }
Fügen Sie die Benachrichtigungsklasse lib/Bookshelf/Mail/BookAddedNotification.php
:
<?php namespace Lib\Bookshelf\Mail; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class BookAddedNotification extends Mailable { use SerializesModels; public function build() { $this->from('no-reply@example.com') ->to('admin@example.com') ->subject('Book added!'); return $this->view('emails.book_added_notification'); } }
Jetzt bestehen alle unsere Tests!
Die Benachrichtigung wird jedoch noch nicht gesendet. Wir müssen das Senden von unserem AddBook
Interaktor AddBook
.
AddBook
Test AddBook
, um sicherzustellen, dass der Mailer aufgerufen wird:
public function testSendMail() { Mail::fake(); $this->subjectCall(); Mail::assertSent(BookAddedNotification::class, 1); }
Wenn wir die Tests ausführen, wird folgende Fehlermeldung The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times.
: The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times.
.
Jetzt integrieren wir den Benachrichtigungsversand in den Interaktor.
public function __construct(Book $repository, BookAddedNotification $mail) { $this->repository = $repository; $this->mail = $mail; } protected function call($bookAttributes) { $this->book = $this->repository->create($bookAttributes); Mail::send($this->mail); }
Infolgedessen sendet der Interaktor eine Benachrichtigung über das Hinzufügen des Buches zur E-Mail.
Controller-Integration
Schließlich müssen wir den Interaktor von der Aktion aus aufrufen.
Bearbeiten Sie die Aktionsdatei app/Http/Controllers/BooksCreateController.php
:
<?php namespace App\Http\Controllers; use Lib\Bookshelf\Interactors\AddBook; use Illuminate\Http\Request; use Illuminate\Http\Response; class BooksCreateController extends Controller { public function __construct(AddBook $addBook) { $this->addBook = $addBook; } public function call(Request $request) { $input = $request->all(); ($this->addBook)($input); return (new Response(null, 201)); } }
Unsere Tests bestehen, aber es gibt ein kleines Problem.
Wir testen den Bucherstellungscode zweimal.
Dies ist normalerweise eine schlechte Praxis, und wir können sie beheben, indem wir einen weiteren Vorteil der Interaktoren veranschaulichen.
Wir werden die Erwähnung in BookRepository
in den Tests entfernen und das BookRepository
für unseren AddBook
Interaktor verwenden:
<?php use Lib\Bookshelf\Interactors\AddBook; class BooksCreateControllerTest extends TestCase { public function testCallsInteractor() { $attributes = ['title' => '1984', 'author' => 'George Orwell']; $addBook = Mockery::mock(AddBook::class); $this->app->instance(AddBook::class, $addBook); $addBook->expects()->__invoke($attributes); $response = $this->call('POST', '/books', $attributes); } }
Jetzt bestehen unsere Tests und sie sind viel zuverlässiger!
Die Aktion nimmt Eingaben (aus den Parametern der http-Anforderung) entgegen und ruft den Interaktor auf, um seine Arbeit zu erledigen. Die einzige Verantwortung für die Aktion ist die Arbeit mit dem Netzwerk. Und der Interaktor arbeitet mit unserer realen Geschäftslogik.
Dies vereinfacht Aktionen und deren Tests erheblich.
Aktionen sind praktisch von der Geschäftslogik ausgenommen.
Wenn wir den Interaktor ändern, müssen wir die Aktion oder ihren Test nicht mehr ändern.
Beachten Sie, dass Sie in einer realen Anwendung wahrscheinlich mehr als die oben beschriebene Logik ausführen möchten, um beispielsweise sicherzustellen, dass das Ergebnis erfolgreich ist. Und wenn ein Fehler auftritt, möchten Sie Fehler vom Interaktor zurückgeben.
Repository mit Code