php网站开发架构_PHP的干净代码架构和测试驱动开发

news/2024/5/20 4:45:06/文章来源:https://blog.csdn.net/culi3118/article/details/108374107

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

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:

您可能想知道,为什么我们不仅仅使用模拟框架来创建依赖关系? 有两个原因:

  1. Because it is easy to create an actual class with the editor. So there is no need to use them.

    因为使用编辑器创建实际的类很容易。 因此,无需使用它们。
  2. 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.

该测试将失败,因此我们必须修改UseCaseprocess方法,并将方法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网站开发架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_785182.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小型企业服务器选择_小型企业的最佳免费和廉价网站选择

小型企业服务器选择Not long ago, if you wanted a website, you had to be willing to shell out at least a few grand for the most basic of static sites. I remember about ten years ago at a firm I worked for, quotes for ecommerce sites, or sites with other dyna…

cms数据库设计_使用Statamic CMS构建无数据库的网站

cms数据库设计A content management system (CMS) is a package of code around which you build a dynamic website—with components that change, adapt and update automatically, in contrast to a hard-coded, static site. 内容管理系统(CMS)是一揽子代码&#xff0c;您…

编辑器生成静态网页_使用静态网站生成器的7个理由

编辑器生成静态网页Static site generators have become increasingly popular and, if my prediction is correct, usage will explode during 2016. Let’s establish what we mean by the term “static site generator” … 静态站点生成器已变得越来越流行&#xff0c;如果…

wordpress 自定义_为您的WordPress网站设计自定义主页

wordpress 自定义WordPress is used on a large portion of sites on the web. It allows us to create a variety of different types of sites, but one of the most important components of any website is always the home page. The perfect landing page will help you …

编辑器生成静态网页_不使用静态网站生成器的7个理由

编辑器生成静态网页Trending posts on SitePoint today: 今天在SitePoint上的热门帖子&#xff1a; 7 Ways to Make WordPress Simpler for Users 为用户简化WordPress的7种方法 I Need a Website. What Do I Need to Know About Hosting? 我需要一个网站。 关于托管我需要了…

wordpress插件_审查的顶级WordPress SEO插件

wordpress插件WordPress provides great native SEO features, so might your theme. However, by using a dedicated SEO plugin (and knowing how to use it) we can further optimize our site. In this article we compare the top SEO plugins for WordPress side-by-side…

如何安装和使用WP-CLI来管理WordPress网站

Speeding up your work process should be one of your top priorities. Simply put, if you do more work in less time, then you will have more time to work on more projects, study and rest. 加快工作流程应该是您的首要任务之一。 简而言之&#xff0c;如果您用更少的…

wordpress快速建站_快速提示:使用WordPress联络表7增强选择加入

wordpress快速建站You might have heard the saying, “the money is in the list”. Your email list, to be exact. Email marketing is widely considered to be the most powerful form of marketing today, and there are plenty of stats to back it up. 您可能已经听说过…

php网站功能块学习_学习PHP 7,了解新功能等

php网站功能块学习PHP 7, the next version of the world’s most popular programming language, has been released. We’d love to shoot fireworks and get drunk with our newfound power (seriously, the language is in the true big leagues now, functionality and pe…

wordpress网站迁移_如何将您的WordPress网站迁移到新的托管服务提供商

wordpress网站迁移This article is part of a series created in partnership with SiteGround. Thank you for supporting the partners who make SitePoint possible. 本文是与SiteGround合作创建的系列文章的一部分。 感谢您支持使SitePoint成为可能的合作伙伴。 We’ve be…

wordpress 静态化_地理定位WordPress内容以个性化您的网站

wordpress 静态化So what does that phrase mean, “Geo-Targeting WordPress Content”? First, let’s back up and look at an example of personalization. 那么&#xff0c;这句话是什么意思&#xff0c;“以地理定位的WordPress内容”&#xff1f; 首先&#xff0c;让我…

next主题seo优化_SEO可见性的5个最佳WordPress主题

next主题seo优化This article is part of a series created in partnership with SiteGround. Thank you for supporting the partners who make SitePoint possible. 本文是与SiteGround合作创建的系列文章的一部分。 感谢您支持使SitePoint成为可能的合作伙伴。 Theme selec…

cloudflare_使用Cloudflare使您的网站更快,更安全

cloudflareCloudflare is an industry leader in the content-delivery space, reducing load and speeding up millions of websites. Cloudflare是内容交付领域的行业领导者&#xff0c;可减轻负载并加快数百万个网站的速度。 What is peculiar about this provider is that…

六个最受欢迎的 CMS 建站系统,不容错过|Gitee项目推荐

CMS 是 Content Management System 的缩写&#xff0c;意为"内容管理系统"。 内容管理系统是企业信息化建设和电子政务的新宠&#xff0c;也是一个相对较新的市场。对于内容管理&#xff0c;业界还没有一个统一的定义&#xff0c;不同的机构有不同的理解。CMS 在各行…

html 相对路径 网站根目录,html中的绝对路径URL和相对路径URL及子目录、父目录、根目录...

绝对URL用于表示Internet中特定文件所需要的全部内容&#xff0c;Internet中的每一个文件都有一个唯一的URL&#xff0c;这就是在网页中搜索时需要输入到地址栏的连接。例如&#xff0c;要进入百度一下的网页&#xff0c;则在网页地址栏中输入&#xff1a;即可。一旦进入到某个…

php网站整合ck播放器,网页视频播放器-ckplayer 整合到wordpress

在这里我将教大家怎样将这款播放器整合到wordpress中来&#xff0c;并教大家怎么将优酷视频用到这款播放器上面!第一步&#xff1a;我们要下载一个播放器文件第二步&#xff1a;将下载文件解压出来&#xff0c;打开里面player文件夹下ckplayer.txt文本文档&#xff0c;将里面的…

【最新】某素材网站上的虎年高质量海报素材合集,附带源文件和预览图

原格式PSD提供给大家同时提供jpg预览图&#xff0c;马上过年了&#xff0c;这套素材用的上&#xff0c;红色喜庆 醉了&#xff0c;本来想整理的上传的&#xff0c;但是发现有水印&#xff0c;现在水印已经全部去除&#xff0c;放心食用&#xff0c;每个图都比较大&#xff0c;一…

使用SSM重新开发计科院网站

一、游览 在游览器地址栏输入&#xff1a;http://localhost:8080/index&#xff0c;即访问计科院首页&#xff0c;由于前期对数据库以及JavaBean的设计考虑不够充分&#xff0c;导致后期的代码臃肿&#xff0c;所以项目启动时对首页进行数据的填充难以实现&#xff0c;必须输入…

airpods固件更新方法_AirPods Pro 固件更新,空间音频来了丨 网站证书更新

09/15 网站证书更新 证书已上传,网站福利应用正在更新中…… 福利区已修复完毕,不知道能活多久! 证书已经上传(回复 证书 获取证书下载链接),证书使用教程查看历史文章 【福利升级】iOS苹果端免越狱 手机端签名神器教程来了! 删掉之前的APP再重新下载即可! 证书很不稳…

java程序员语录_「java程序员面试题」2018java程序员面试题整理 - seo实验室

java程序员面试题1.PathVariable注解和requestParam注解的区别。RequestParam注解是获取静态URL传入的参数PathVariable是获取请求路径中的变量作为参数/需要和RequestMAPPing("item/{itemId}") 配合使用2.Param注解和RequestParam注解的区别。Parm 指定request中必…