Using Specifications in our Services might be the best example to illustrate how to use Factories within our Services.
Consider the following Service example. Given a request from the outside world, we want to build a feed based on the latest Posts added to the system:
namespace ApplicationService;
use DomainModelPost;
use DomainModelPostRepository;
class LatestPostsFeedService
{
private $postRepository;
public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}
/**
* @param LatestPostsFeedRequest $request
*/
public function execute($request)
{
$posts = $this->postRepository->latestPosts($request->since);
return array_map(function(Post $post) {
return [
'id' => $post->id()->id(),
'content' => $post->body()->content(),
'created_at' => $post-> createdAt()
];
}, $posts);
}
}
Finder methods in Repositories like latestPosts have some limitations, as they keep adding complexity to our Repositories indefinitely. As we discuss in the Chapter 10, Repositories Specifications are a better approach.
Lucky for us, we have a nice query method in our PostRepository that works with Specifications:
class LatestPostsFeedService
{
// ...
public function execute($request)
{
$posts = $this->postRepository->query($specification);
}
}
Using a concrete implementation for the Specification is a bad idea:
class LatestPostsFeedService
{
public function execute($request)
{
$posts = $this->postRepository->query(
new SqlLatestPostSpecification($request->since)
);
}
}
Coupling our high-level application Service with a low-level Specification implementation mixes layers and breaks the Separation of Concerns. In addition, this is a pretty bad way of coupling our Service to a concrete Infrastructure implementation. There's no way you could use this Service outside of the SQL persistence solution. What if we want to test our Service with an in-memory implementation?
The solution to this problem is to decouple Specification creation from the Service itself by using the Abstract Factory pattern. According to OODesign.com:
Abstract Factory offers the interface for creating a family of related objects, without explicitly specifying their classes.
As we might have multiple Specification implementations, we first need to create an interface for the Factory:
namespace DomainModel;
interface PostSpecificationFactory
{
public function createLatestPosts(DateTimeImmutable $since);
}
Then we need to create Factories for each PostRepository implementation. As an example, a Factory for the in-memory PostRepository implementation could look like this:
namespace InfrastructurePersistenceInMemory;
use DomainModelPostSpecificationFactory;
class InMemoryPostSpecificationFactory
implements PostSpecificationFactory
{
public function createLatestPosts(DateTimeImmutable $since)
{
return new InMemoryLatestPostSpecification($since);
}
}
Once we have a centralized place for the creation logic, it's easy to decouple it from the Service:
class LatestPostsFeedService
{
private $postRepository;
private $postSpecificationFactory;
public function __construct(
PostRepository $postRepository,
PostSpecificationFactory $postSpecificationFactory
) {
$this->postRepository = $postRepository;
$this->postSpecificationFactory = $postSpecificationFactory;
}
public function execute($request)
{
$posts = $this->postRepository->query(
$this->postSpecificationFactory->createLatestPosts(
$request->since
)
);
}
}
Now, unit testing our Service through an in-memory PostRepository implementation is pretty easy:
namespace ApplicationService;
use DomainModelBody;
use DomainModelPost;
use DomainModelPostId;
use InfrastructurePersistenceInMemoryInMemoryPostRepositor;
class LatestPostsFeedServiceTest extends PHPUnit_Framework_TestCase
{
/**
* @var InfrastructurePersistenceInMemoryInMemoryPostRepository
*/
private $postRepository;
/**
* @var LatestPostsFeedService
*/
private $latestPostsFeedService;
public function setUp()
{
$this->latestPostsFeedService = new LatestPostsFeedService(
$this->postRepository = new InMemoryPostRepository()
);
}
/**
* @test
*/
public function shouldBuildAFeedFromLatestPosts()
{
$this->addPost(1, 'first', '-2 hours');
$this->addPost(2, 'second', '-3 hours');
$this->addPost(3, 'third', '-5 hours');
$feed = $this->latestPostsFeedService->execute(
new LatestPostsFeedRequest(
new DateTimeImmutable('-4 hours')
)
);
$this->assertFeedContains([
['id' => 1, 'content' => 'first'],
['id' => 2, 'content' => 'second']
], $feed);
}
private function addPost($id, $content, $createdAt)
{
$this->postRepository->add(new Post(
new PostId($id),
new Body($content),
new DateTimeImmutable($createdAt)
));
}
private function assertFeedContains($expected, $feed)
{
foreach ($expected as $index => $contents) {
$this->assertArraySubset($contents, $feed[$index]);
$this->assertNotNull($feed[$index]['created_at']);
}
}
}