php网站开发架构
The Clean Code Architecture was introduced by Robert C. Martin on the 8light blog. The idea was to create an architecture which is independent of any external agency. Your business logic should not be coupled to a framework, a database, or to the web itself. With the independence, you have several advantages. For example, you have the ability to defer technical decisions to a later point during development (e.g. choosing a framework and choosing a database engine/provider). You can also easily switch the implementations or compare different implementations, but the biggest advantage is that your tests will run fast.
Robert C. Martin在8light博客上介绍了Clean Code Architecture。 这个想法是创建一个独立于任何外部机构的架构。 您的业务逻辑不应与框架,数据库或Web本身耦合。 具有独立性,您有几个优点。 例如,您可以将技术决策推迟到开发过程中的较晚一点(例如,选择框架并选择数据库引擎/提供程序)。 您还可以轻松切换实现或比较不同的实现,但是最大的好处是您的测试将快速运行。
Just think about it. Do you really have to run through a router, load a database abstract layer or some ORM magic, or execute some other code just to assert one or more results?
考虑一下。 您是否真的必须通过路由器运行,加载数据库抽象层或某种ORM魔术,还是执行其他一些代码来声明一个或多个结果?
I started to learn and practice this architecture because of my old favorite framework Kohana. At some point, the core developer stopped maintaining the code, which also meant that my projects would not get any further updates or security fixes. This meant that I had to either move to another framework and rewrite the entire project or trust the community development version.
由于我最喜欢的框架Kohana,我开始学习和实践此体系结构。 在某个时候,核心开发人员停止维护代码,这也意味着我的项目将无法获得任何进一步的更新或安全修复程序。 这意味着我要么不得不移至另一个框架并重写整个项目,要么必须信任社区开发版本。
![Clean Code Architecture](https://blog.8thlight.com/assets/posts/2012-08-13-the-clean-architecture/CleanArchitecture-81565aba46f035911a5018e77a0f2d4e.jpg)
I could have chosen to go with another framework. Maybe it would have been better to go with Symfony 1 or Zend 1, but by now that framework would have also changed.
我本可以选择使用另一个框架。 也许最好使用Symfony 1或Zend 1,但是到现在,该框架也将发生变化。
Frameworks will continue to change and evolve. With composer, it is easy to install and replace packages, but it is also easy to abandon a package (composer even has the option to mark a package as abandoned), so it is easy to make “the wrong choice”.
框架将继续改变和发展。 使用composer,很容易安装和替换软件包,但也很容易放弃软件包(作曲家甚至可以选择将软件包标记为已废弃),因此很容易做出“错误的选择”。
In this tutorial, I will show you how we can implement the Clean Code Architecture in PHP, in order to be in control of our own logic, without being dependent on external providers, but while still using them. We will create a simple guestbook application.
在本教程中,我将向您展示如何在PHP中实现Clean Code Architecture,以便在不依赖外部提供程序但仍在使用它们的情况下控制我们自己的逻辑。 我们将创建一个简单的留言簿应用程序。
The image above shows the different layers of the application. The inner layers do not know anything about the outer layers and they all communicate via interfaces.
上图显示了应用程序的不同层。 内层对外层一无所知,它们都通过接口进行通信。
The most interesting part is in the bottom-right corner of the image: Flow of control. The image explains how the framework communicates with our business logic. The Controller passes its data to the input port, which is processed by an interactor to produce an output port which contains data for the presenter.
最有趣的部分在图像的右下角: 控制流 。 该图说明了框架如何与我们的业务逻辑进行通信。 控制器将其数据传递到输入端口,由交互器处理该输入端口,以产生包含用于演示者数据的输出端口。
We will start with the UseCase layer, since this is the layer which contains our application-specific-logic. The Controller layer and other outer layers belong to a Framework.
我们将从UseCase层开始,因为这是包含我们特定于应用程序的逻辑的层。 控制器层和其他外部层属于框架。
Note that all the various stages described below can be cloned and tested from this repo, which was neatly arranged into steps with the help of Git tags. Just download the corresponding step if you’d like to see it in action.
请注意,可以从此repo克隆并测试以下描述的所有各个阶段, 该repo在Git标签的帮助下被整齐地排列为步骤。 如果您想查看实际操作,只需下载相应的步骤即可。
第一次测试 (First test)
We usually begin from the UI point of view. What should we expect to see if we visit a guestbook? There should be some kind of input form, the entries from other visitors, and maybe a navigation panel to search through pages of entries. If the guestbook is empty, we might see a message like “No entries found”.
我们通常从UI的角度开始。 如果我们访问留言簿,我们应该期望看到什么? 应该有某种输入形式,其他访问者的条目,也许还有一个导航面板来搜索条目页面。 如果留言簿为空,我们可能会看到类似“找不到条目”的消息。
In our first test we want to assert an empty list of entries, it looks like this:
在我们的第一个测试中,我们要声明一个空条目列表,如下所示:
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
public function testEntriesNotExists()
{
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase();
$useCase->process($request, $response);
$this->assertEmpty($response->entries);
}
}
In this test, I used a slightly different notation than Uncle Bob. The Interactors are UseCases, Input Ports are Requests, and Output Ports are Responses.
在此测试中,我使用的表达方式与鲍伯叔叔略有不同。 交互器是用例,输入端口是请求,输出端口是响应。
The UseCases always contain the method process which has a type hint to its specific Request and Response interface.
UseCases始终包含方法过程,该过程具有指向其特定的Request and Response接口的类型提示。
According to the Red, Green, and Refactor cycles in TDD, this test should and will fail, because the classes do not exist.
根据TDD中的红色,绿色和重构周期,该测试应该并且将失败,因为这些类不存在。
After creating the class files, methods, and properties, the test passes. Since the classes are empty, we do not need to use the Refactor cycle at this point.
创建类文件,方法和属性后,测试通过。 由于类为空,因此此时我们不需要使用重构周期。
Next, we want to assert that we can actually see some entries.
接下来,我们要断言我们实际上可以看到一些条目。
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
public function testEntriesNotExists()
{
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase();
$useCase->process($request, $response);
$this->assertEmpty($response->entries);
}
public function testCanSeeEntries()
{
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase();
$useCase->process($request, $response);
$this->assertNotEmpty($response->entries);
}
}
As we can see, the test fails, and we are in the red section of the TDD cycle. To make the test pass we have to add some logic into our UseCases.
如我们所见,测试失败,并且我们处于TDD周期的红色部分。 为了使测试通过,我们必须在UseCases中添加一些逻辑。
勾画出UseCase逻辑 (Sketch out the UseCase logic)
Before we start with the logic, we apply the parameter type hints and create the interfaces.
在开始逻辑之前,我们应用参数类型提示并创建接口。
<?php
namespace BlackScorp\GuestBook\UseCase;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;
class ViewEntriesUseCase
{
public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
}
}
This is similar to how graphic artists often work. Instead of drawing the entire picture from beginning to end, they usually draw some shapes and lines to have an idea of what the finished picture might be. Afterwards, they use the shapes as guides and add more details. This process is called “Sketching”.
这类似于图形艺术家通常的工作方式。 他们通常不绘制整个图片,而是绘制一些形状和线条以了解完成的图片可能是什么。 之后,他们将形状用作参考并添加更多细节。 此过程称为“ 草绘 ”。
Instead of shapes and lines, we use, for example, Repositories and Factories as our guides.
除了使用形状和线条之外,我们还使用“ 存储库和工厂”作为指南。
The Repository is an abstract layer for retrieving data from a storage. The storage could be a database, it could be a file, an external API or even in memory.
存储库是用于从存储中检索数据的抽象层。 存储可以是数据库,也可以是文件,外部API甚至是内存。
To view the guestbook entries, we have to find the entities in our repository, convert them to views, and add them to the response.
要查看留言簿条目,我们必须在存储库中找到实体,将其转换为视图,然后将其添加到响应中。
<?php
namespace BlackScorp\GuestBook\UseCase;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;
class ViewEntriesUseCase
{
public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
$entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
if(!$entries){
return;
}
foreach($entries as $entry){
$entryView = $this->entryViewFactory->create($entry);
$response->addEntry($entryView);
}
}
}
You might ask, why do we convert the entry Entity to a View?
您可能会问,为什么我们将条目Entity转换为View?
The reason is that the Entity should not go outside the UseCases layer. We can only find an Entity with the help of the repository, so we modify/copy it if necessary and then put it back into the repository (when modified).
原因是该实体不应超出UseCases层之外。 我们只能在存储库的帮助下找到一个实体,因此,如有必要,我们可以对其进行修改/复制,然后将其放回存储库中(修改后)。
When we begin to move the Entity into the outer layer, it is best to add some additional methods for communication purposes, but the Entity should only contain core business logic.
当我们开始将实体移到外层时,最好添加一些用于通信目的的其他方法,但是实体应仅包含核心业务逻辑。
As we are not sure of how we want to format the entries, we can defer this step.
由于我们不确定如何格式化条目,因此可以推迟此步骤。
Another question might be “Why a factory?”
另一个问题可能是“为什么要建工厂?”
If we create a new instance inside the loop such as
如果我们在循环中创建一个新实例,例如
$entryView = new EntryView($entry);
$response->addEntry($entryView);
we would violate the dependency inversion principle. If, later on, we require another view object in the same UseCase logic, we would have to change the code. With the factory, we have an easy way to implement different views, which might contain different formatting logic, while still using the same UseCase.
我们将违反依赖倒置原则 。 如果以后在相同的UseCase逻辑中需要另一个视图对象,则必须更改代码。 在工厂中,我们有一种简单的方法来实现不同的视图,这些视图可能包含不同的格式化逻辑,同时仍使用相同的UseCase。
实现外部依赖 (Implementing external dependencies)
At this point, we already know the dependencies of the UseCase: $entryViewFactory
and $entryRepository
. We also know the methods of the dependencies. The EntryViewFactory
has a create method which accepts the EntryEntity
, and the EntryRepository
has a findAll()
method which returns an array of EntryEntities
. Now we can create the interfaces with the methods and apply them to the UseCase.
至此,我们已经知道UseCase的依赖项: $entryViewFactory
和$entryRepository
。 我们也知道依赖项的方法。 EntryViewFactory
有一个create方法可以接受EntryEntity
,而EntryRepository
有一个findAll()
方法可以返回EntryEntities
数组。 现在,我们可以使用方法创建接口,并将其应用于UseCase。
The EntryRepository will looks like this:
EntryRepository将如下所示:
<?php
namespace BlackScorp\GuestBook\Repository;
interface EntryRepository {
public function findAllPaginated($offset,$limit);
}
And the UseCase like so
和UseCase一样
<?php
namespace BlackScorp\GuestBook\UseCase;
use BlackScorp\GuestBook\Repository\EntryRepository;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
use BlackScorp\GuestBook\Response\ViewEntriesResponse;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;
class ViewEntriesUseCase
{
/**
* @var EntryRepository
*/
private $entryRepository;
/**
* @var EntryViewFactory
*/
private $entryViewFactory;
/**
* ViewEntriesUseCase constructor.
* @param EntryRepository $entryRepository
* @param EntryViewFactory $entryViewFactory
*/
public function __construct(EntryRepository $entryRepository, EntryViewFactory $entryViewFactory)
{
$this->entryRepository = $entryRepository;
$this->entryViewFactory = $entryViewFactory;
}
public function process(ViewEntriesRequest $request, ViewEntriesResponse $response)
{
$entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
if (!$entries) {
return;
}
foreach ($entries as $entry) {
$entryView = $this->entryViewFactory->create($entry);
$response->addEntry($entryView);
}
}
}
As you can see, the tests still fail because of the missing dependency implementation. So here we just create some fake objects.
如您所见,由于缺少依赖项实现,测试仍然失败。 所以在这里我们只创建一些假对象。
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
public function testEntriesNotExists()
{
$entryRepository = new FakeEntryRepository();
$entryViewFactory = new FakeEntryViewFactory();
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
$useCase->process($request, $response);
$this->assertEmpty($response->entries);
}
public function testCanSeeEntries()
{
$entities = [];
$entities[] = new EntryEntity();
$entryRepository = new FakeEntryRepository($entities);
$entryViewFactory = new FakeEntryViewFactory();
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
$useCase->process($request, $response);
$this->assertNotEmpty($response->entries);
}
}
Because we already created the interfaces for the repository and view factory, we can implement them in the fake classes, and also implement the request/response interfaces.
因为我们已经为存储库和视图工厂创建了接口,所以我们可以在伪类中实现它们,也可以实现请求/响应接口。
The repository now looks like this:
现在,存储库如下所示:
<?php
namespace BlackScorp\GuestBook\Fake\Repository;
use BlackScorp\GuestBook\Repository\EntryRepository;
class FakeEntryRepository implements EntryRepository
{
private $entries = [];
public function __construct(array $entries = [])
{
$this->entries = $entries;
}
public function findAllPaginated($offset, $limit)
{
return array_splice($this->entries, $offset, $limit);
}
}
and the view factory like this:
和这样的视图工厂:
<?php
namespace BlackScorp\GuestBook\Fake\ViewFactory;
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\View\FakeEntryView;
use BlackScorp\GuestBook\View\EntryView;
use BlackScorp\GuestBook\ViewFactory\EntryViewFactory;
class FakeEntryViewFactory implements EntryViewFactory
{
/**
* @param EntryEntity $entity
* @return EntryView
*/
public function create(EntryEntity $entity)
{
$view = new FakeEntryView();
$view->author = $entity->getAuthor();
$view->text = $entity->getText();
return $view;
}
}
You may wonder, why don’t we just use mocking frameworks to create the dependencies? There are two reasons:
您可能想知道,为什么我们不仅仅使用模拟框架来创建依赖关系? 有两个原因:
- Because it is easy to create an actual class with the editor. So there is no need to use them. 因为使用编辑器创建实际的类很容易。 因此,无需使用它们。
- When we start to create the implementation for the framework, we can use these fake classes in the DI Container and fiddle with the template without having to make a real implementation. 当我们开始为框架创建实现时,我们可以在DI容器中使用这些伪造的类并修饰模板,而不必进行实际实现。
The tests now pass, and we can go to refactoring. There is in fact nothing that can be refactored in the UseCase class, only in the test class.
现在测试通过了,我们可以进行重构了。 实际上,只有在测试类中,没有什么可以在UseCase类中进行重构。
重构测试 (Refactoring the test)
The execution is actually the same, we just have a different setup and assertion. We can thus extract the initialization of the fake classes and processing the UseCase to a private function processUseCase
.
执行实际上是相同的,只是设置和断言不同。 因此,我们可以提取假类的初始化,并将UseCase处理为私有函数processUseCase
。
The test class should now look like this
测试类现在应该看起来像这样
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
public function testCanSeeEntries()
{
$entries = [
new EntryEntity('testAuthor','test text')
];
$response = $this->processUseCase($entries);
$this->assertNotEmpty($response->entries);
}
public function testEntriesNotExists()
{
$entities = [];
$response = $this->processUseCase($entities);
$this->assertEmpty($response->entries);
}
/**
* @param $entities
* @return FakeViewEntriesResponse
*/
private function processUseCase($entities)
{
$entryRepository = new FakeEntryRepository($entities);
$entryViewFactory = new FakeEntryViewFactory();
$request = new FakeViewEntriesRequest();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase($entryRepository, $entryViewFactory);
$useCase->process($request, $response);
return $response;
}
}
独立 (Independence)
Now, for example, we can easily create new test cases with invalid entities, and we can move the repository and factory to the setup method and run the tests with real implementations.
现在,例如,我们可以轻松创建带有无效实体的新测试用例,并且可以将存储库和工厂移至设置方法,并使用实际实现运行测试。
As you can see, we can implement a ready to use UseCase into the DI Container and use it inside any framework. The logic is framework agnostic.
如您所见,我们可以在DI容器中实现一个随时可用的UseCase,并在任何框架中使用它。 逻辑与框架无关。
We could create another repository implementation which speaks to an external API, for example, and pass it to the UseCase. The logic is database agnostic.
例如,我们可以创建另一个与外部API通讯的存储库实现,并将其传递给UseCase。 该逻辑与数据库无关。
We could create CLI request/response objects and pass them to the same UseCase which is used inside a controller, so the logic is independent of the platform.
我们可以创建CLI请求/响应对象,并将它们传递给控制器内部使用的同一UseCase,因此逻辑独立于平台。
We could even execute different UseCases in a row where every UseCase might modify the actual response object.
我们甚至可以连续执行不同的UseCases,其中每个UseCase都可以修改实际的响应对象。
class MainController extends BaseController
{
public function indexAction(Request $httpRequest)
{
$indexActionRequest = new IndexActionRequest($httpRequest);
$indexActionResponse = new IndexActionResponse();
$this->getContainer('ViewNavigation')->process($indexActionRequest, $indexActionResponse);
$this->getContainer('ViewNewsEntries')->process($indexActionRequest, $indexActionResponse);
$this->getContainer('ViewUserAvatar')->process($indexActionRequest, $indexActionResponse);
$this->render($indexActionResponse);
}
}
分页 (Pagination)
Now we want to add pagination. The test may look like this:
现在我们要添加分页。 测试可能如下所示:
public function testCanSeeFiveEntries(){
$entities = [];
for($i = 0;$i<10;$i++){
$entities[] = new EntryEntity('Author '.$i,'Text '.$i);
}
$response = $this->processUseCase($entities);
$this->assertNotEmpty($response->entries);
$this->assertSame(5,count($response->entries));
}
This test will fail, so we have to modify the process
method of the UseCase
and also rename the method findAll
to findAllPaginated
.
该测试将失败,因此我们必须修改UseCase
的process
方法,并将方法findAll
重命名为findAllPaginated
。
public function process(ViewEntriesRequest $request, ViewEntriesResponse $response){
$entries = $this->entryRepository->findAllPaginated($request->getOffset(), $request->getLimit());
//....
}
Now we apply the new parameters to the interface and the fake repository and add the new methods to the request interface.
现在,我们将新参数应用于接口和伪存储库,并将新方法添加至请求接口。
The repository’s findAllPaginated
method changes a little:
存储库的findAllPaginated
方法有所变化:
public function findAllPaginated($offset, $limit)
{
return array_splice($this->entries, $offset, $limit);
}
and we have to move the request object in the tests; also, the limit parameter will be required in the constructor of our request object. This way, we will force the setup to create the limit with a new instance.
并且我们必须在测试中移动请求对象; 同样,我们的请求对象的构造函数中将需要limit参数。 这样,我们将强制安装程序使用新实例创建限制。
public function testCanSeeFiveEntries(){
$entities = [];
for($i = 0;$i<10;$i++){
$entities[] = new EntryEntity();
}
$request = new FakeViewEntriesRequest(5);
$response = $this->processUseCase($request, $entities);
$this->assertNotEmpty($response->entries);
$this->assertSame(5,count($response->entries));
}
The test passes, but we have to test if we can see the next five entries. Therefore, we have to extend the request object with a setPage
method.
测试通过了,但是我们必须测试是否可以看到接下来的五个条目。 因此,我们必须使用setPage
方法扩展请求对象。
<?php
namespace BlackScorp\GuestBook\Fake\Request;
use BlackScorp\GuestBook\Request\ViewEntriesRequest;
class FakeViewEntriesRequest implements ViewEntriesRequest{
private $offset = 0;
private $limit = 0;
/**
* FakeViewEntriesRequest constructor.
* @param int $limit
*/
public function __construct($limit)
{
$this->limit = $limit;
}
public function setPage($page = 1){
$this->offset = ($page-1) * $this->limit;
}
public function getOffset()
{
return $this->offset;
}
public function getLimit()
{
return $this->limit;
}
}
With this method, we can test if we can see the next five entries.
使用这种方法,我们可以测试是否可以看到接下来的五个条目。
public function testCanSeeFiveEntriesOnSecondPage(){
$entities = [];
$expectedEntries = [];
$entryViewFactory = new FakeEntryViewFactory();
for($i = 0;$i<10;$i++){
$entryEntity = new EntryEntity();
if($i >= 5){
$expectedEntries[]=$entryViewFactory->create($entryEntity);
}
$entities[] =$entryEntity;
}
$request = new FakeViewEntriesRequest(5);
$request->setPage(2);
$response = $this->processUseCase($request,$entities);
$this->assertNotEmpty($response->entries);
$this->assertSame(5,count($response->entries));
$this->assertEquals($expectedEntries,$response->entries);
}
Now the tests pass and we can refactor. We move the FakeEntryViewFactory
to the setup
method and we are done with this feature. The final test class looks like this:
现在测试通过了,我们可以重构了。 我们将FakeEntryViewFactory
移至setup
方法,并完成了此功能。 最终的测试类如下所示:
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use BlackScorp\GuestBook\Entity\EntryEntity;
use BlackScorp\GuestBook\Fake\Repository\FakeEntryRepository;
use BlackScorp\GuestBook\Fake\Request\FakeViewEntriesRequest;
use BlackScorp\GuestBook\Fake\Response\FakeViewEntriesResponse;
use BlackScorp\GuestBook\Fake\ViewFactory\FakeEntryViewFactory;
use BlackScorp\GuestBook\UseCase\ViewEntriesUseCase;
class ListEntriesTest extends PHPUnit_Framework_TestCase
{
public function testEntriesNotExists()
{
$entries = [];
$request = new FakeViewEntriesRequest(5);
$response = $this->processUseCase($request, $entries);
$this->assertEmpty($response->entries);
}
public function testCanSeeEntries()
{
$entries = [
new EntryEntity('testAuthor', 'test text')
];
$request = new FakeViewEntriesRequest(5);
$response = $this->processUseCase($request, $entries);
$this->assertNotEmpty($response->entries);
}
public function testCanSeeFiveEntries()
{
$entities = [];
for ($i = 0; $i < 10; $i++) {
$entities[] = new EntryEntity('Author ' . $i, 'Text ' . $i);
}
$request = new FakeViewEntriesRequest(5);
$response = $this->processUseCase($request, $entities);
$this->assertNotEmpty($response->entries);
$this->assertSame(5, count($response->entries));
}
public function testCanSeeFiveEntriesOnSecondPage()
{
$entities = [];
$expectedEntries = [];
$entryViewFactory = new FakeEntryViewFactory();
for ($i = 0; $i < 10; $i++) {
$entryEntity = new EntryEntity('Author ' . $i, 'Text ' . $i);
if ($i >= 5) {
$expectedEntries[] = $entryViewFactory->create($entryEntity);
}
$entities[] = $entryEntity;
}
$request = new FakeViewEntriesRequest(5);
$request->setPage(2);
$response = $this->processUseCase($request, $entities);
$this->assertNotEmpty($response->entries);
$this->assertSame(5, count($response->entries));
$this->assertEquals($expectedEntries, $response->entries);
}
/**
* @param $request
* @param $entries
* @return FakeViewEntriesResponse
*/
private function processUseCase($request, $entries)
{
$repository = new FakeEntryRepository($entries);
$factory = new FakeEntryViewFactory();
$response = new FakeViewEntriesResponse();
$useCase = new ViewEntriesUseCase($repository, $factory);
$useCase->process($request, $response);
return $response;
}
}
结论 (Conclusion)
In this tutorial, we have seen how the test leads us to the UseCase, which leads us to interfaces which, in turn, lead us to fake implementations.
在本教程中,我们已经看到了测试如何将我们引向UseCase,该案例使我们引向了接口,这些接口又引向了伪造的实现。
Again, the source code for this article can be found on Github – check the tags for all the various source code stages of this post.
同样,可以在Github上找到本文的源代码–检查本文所有不同源代码阶段的标签。
This tutorial demonstrates that it is not that difficult to apply Test Driven Development and Clean Code Architecture to any newly created project. The great advantage is that while the logic is completely independent, we can still use third-party libraries.
本教程说明,将“测试驱动开发和清理代码体系结构”应用于任何新创建的项目并不难。 最大的优点是,尽管逻辑是完全独立的,但我们仍然可以使用第三方库。
Questions? Comments? Please leave them in the comment section right below the like button!
有什么问题吗 注释? 请将它们留在“喜欢”按钮下方的评论部分!
翻译自: https://www.sitepoint.com/clean-code-architecture-and-test-driven-development-in-php/
php网站开发架构