本文是对Laravel框架Hanami框架手册的一部分的改编。 是什么引起了对这种特殊材料的兴趣? 它提供了分步说明,并演示了编程语言和框架的常见用法,例如:
应当立即指出,这些不仅是具有不同意识形态的不同框架(特别是关于ORM),而且是不同的编程语言,每种语言都有其特定的文化并出于历史原因建立了“最佳实践”。 不同的编程语言和框架往往会相互借鉴最成功的解决方案,因此,尽管细节有所不同,但基本内容不会有所不同,除非我们当然将PL最初采用不同的范例。 比较有趣的是,比较如何在不同的生态系统中解决同一问题。
因此,最初我们有Hanami(ruby)框架-这是一个相当新的框架,在思想上更倾向于Symfony,并且ORM位于“存储库”中。 而目标Laravel \ Lumen(php)框架具有Active Record。
在适应过程中,最锐角被切掉:
- 该项目的初始化,对框架功能的描述以及类似的特定内容都省略了指南的第一部分。
- ORM Eloquent被推向全球,并充当存储库。
- 生成代码和发送电子邮件模板的步骤。
保存并强调:
- 交互器-在接口上进行了最低限度的适当实现。
- 通过TDD进行测试,逐步开发。
Hanami原始教程的第一部分,将在下面的文本中引用。
原始交互式教程文本
在文本末尾使用修改后的php代码链接到存储库。
互动者
新功能:电子邮件通知
功能场景:作为管理员,当我添加一本书时,我想接收电子邮件通知。
由于该应用程序没有身份验证,因此任何人都可以添加新书。 我们将通过环境变量指定管理员的电子邮件地址。
这只是显示何时使用交互器,尤其是如何使用Hanami交互器的示例。
此示例可以用作其他功能的基础,例如管理员在发行新书之前对其进行确认。 或者使用户能够通过特殊链接指定电子邮件地址来编辑图书。
实际上,您可以使用交互器来实现从网络层抽象的任何业务逻辑。 当您想要组合几件事以控制代码库的复杂性时,这特别有用。
它们用于遵循单一职责原则隔离非平凡的业务逻辑。
在Web应用程序中,通常是通过控制器操作来使用它们。 这样,您就可以分离任务,业务逻辑对象和交互器;它们对应用程序的网络层一无所知。
回调? 我们不需要它们!
实现电子邮件通知的最简单方法是添加回调。
也就是说,在数据库中创建新书条目之后,将发送电子邮件。
在结构上,Hanami不提供这种机制。 这是因为我们认为模型回调是一种反模式。 他们违反了唯一责任原则。 在我们的案例中,他们错误地将持久层与电子邮件通知混合在一起。
在测试期间(很可能在其他情况下),您将要跳过回调。 这很快就会造成混乱,因为可以按特定顺序触发单个事件的多个回调。 另外,您可以跳过一些回调。 回调使代码易碎且难以理解。
相反,我们建议使用显式而不是隐式。
交互器是代表特定用例的对象。
他们允许每个班级都有一个责任。 交互者的唯一责任是将对象和方法调用结合起来以达到特定的结果。
主意
交互器的主要思想是将功能的隔离部分提取到新类中。
您应该只编写两个公共方法: __construct
和call
。
在交互器的php实现中,call方法具有protected修饰符,并通过__invoke
进行__invoke
。
这意味着此类对象易于解释,因为只有一种可用的方法可以使用它们。
将行为封装在一个对象中使测试变得更加容易。 它还使您更容易理解代码库,而不仅仅是隐式地保留隐藏的复杂性。
准备工作
假设我们有“入门 ”书架应用程序,并且我们想添加“已添加书的电子邮件通知”功能。
编写一个交互器
让我们为交互器创建一个文件夹,为他们的测试创建一个文件夹:
$ mkdir lib/bookshelf/interactors $ mkdir tests/bookshelf/interactors
我们将它们放在lib/bookshelf
因为它们与Web应用程序无关。 您可以稍后通过管理门户,API甚至命令行实用程序添加书籍。
添加AddBook
交互器并编写一个新的测试tests/bookshelf/interactors/AddBookTest.php
:
由于没有AddBook
类,因此运行测试套件将引发Class does not exist
is Class does not exist
错误。 让我们在文件lib/bookshelf/interactors/AddBook.php
:
<?php namespace Lib\Bookshelf\Interactors; use Lib\Interactor\Interactor; class AddBook { use Interactor; public function __construct() { } protected function call() { } }
此类仅应包含两个方法:用于设置数据的__construct
和用于实现脚本的call
。
这些方法(尤其是call
)应调用您编写的私有方法。
默认情况下,结果被认为是成功的,因为我们没有明确指出操作失败。
让我们运行测试:
$ phpunit
所有测试必须通过!
现在,让我们的AddBook
交互器真正发挥作用!
书籍创作
更改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); }
如果运行phpunit
测试,将会看到错误:
Exception: Undefined property Lib\Interactor\InteractorResult::$book
让我们填写我们的交互器,然后解释我们做了什么:
<?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); } }
这里要注意的两个重要事项:
protected static $expose = ["book"];
字符串protected static $expose = ["book"];
将book
属性添加到调用交互器时将返回的结果对象。
call
方法将Book
模型分配给book
属性,结果将可用。
现在测试应该通过了。
我们初始化了Book
模型,但是它没有存储在数据库中。
保存书
我们有一本新书,是从书名和作者那里获得的,但尚未在数据库中。
我们需要使用BookRepository
保存它。
如果运行测试,您将看到一个新错误,并显示消息Failed asserting that null is not null
。
这是因为我们创建的书没有标识符,因为它只有在保存时才会收到。
为了使测试通过,我们需要创建一个保存的书。 另一种同样正确的方法是保存我们已经拥有的书。
在lib/bookshelf/interactors/AddBook.php
编辑call
方法:
protected function call($bookAttributes) { $this->book = Book::create($bookAttributes); }
我们不调用new Book
而是使用new Book
的属性来Book::create
。
该方法仍返回书籍,并将此记录保存在数据库中。
如果现在运行测试,您将看到所有测试均通过。
依赖注入
让我们重构为使用依赖注入。
测试仍然可以进行,但是它们取决于保存到数据库的功能(id属性是在成功保存之后确定的)。 这是保护工作原理的实施细节。 例如,如果要在保存前创建一个UUID并通过保存ID列以外的其他方式指示保存成功,则必须更改此测试。
我们可以修改测试和交互器以使其更可靠:由于其文件外部的更改,它不太容易损坏。
这是我们可以在交互器中使用依赖注入的方法:
从本质上讲,创建repository
属性的过程是相同的,只是添加了一些代码。
现在,测试将检查create
方法的行为,以确保其标识符填充有$this->assertNotNull($result->book->id)
。
这是一个实现细节。
相反,我们可以修改测试以确保在存储库上调用了create
方法,并相信存储库将保存对象(因为这是它的责任)。
让我们更改testPersistsBook
测试:
现在我们的测试没有违反其区域的边界。
我们所做的只是在存储库上添加了交互器依赖项。
电邮通知
让我们添加电子邮件通知!
您还可以在此处执行任何操作,例如,发送短信,发送聊天消息或激活Web挂钩。
我们将邮件正文保留为空,但是在主题字段中,我们将指示“已添加图书!”。
创建一个通知测试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); } }
添加通知类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'); } }
现在我们所有的测试都通过了!
但是通知尚未发送。 我们需要从AddBook
交互器调用发送。
AddBook
测试,以确保将调用该邮件:
public function testSendMail() { Mail::fake(); $this->subjectCall(); Mail::assertSent(BookAddedNotification::class, 1); }
如果运行测试, 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.
。
现在,我们将通知发送集成到交互器中。
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); }
结果,交互器将发送有关将书添加到电子邮件的通知。
控制器整合
最后,我们需要从动作中调用交互器。
编辑动作文件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)); } }
我们的测试通过了,但是有一个小问题。
我们对书籍创建代码进行了两次测试。
这通常是一个不好的做法,我们可以通过说明交互器的另一个优点来修复它。
我们将在测试中删除BookRepository
上的提及, BookRepository
模拟用于我们的AddBook
交互器:
<?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); } }
现在我们的测试通过了,它们更加可靠!
该动作(从http请求的参数中)获取输入,并调用交互程序以完成其工作。 该操作的唯一责任是使用网络。 交互器与我们真正的业务逻辑一起工作。
这极大地简化了动作及其测试。
实际上,操作不受业务逻辑的约束。
当我们修改交互器时,我们不再需要更改操作或其测试。
请注意,在实际的应用程序中,您可能想要做的不仅仅是上述逻辑,例如,确保结果成功。 如果发生故障,您将需要从交互器返回错误。
带有代码的存储库