29
loading...
This website collects cookies to deliver better user experience
PHP 8.1 adds a feature that might seem like a small detail, but one that I think will have a significant day-by-day impact on many people.
$someDependencyToBeInjected = FactoryClass::create();
$someService = new SomeServiceClass($someDependencyToBeInjected);
// service class to apply business logic
// most standard
class DefaultLeadRecordService implements LeadRecordService
{
public function __construct(
private LeadQueryService $queryService,
private LeadCommandHandler $commandHandler
) {
}
}
// infrastructure class to match the interface -- a DBAL concrete class
// sneakily allowing a "default" value, but also open to Dependency Injection
// but not that great
class DbalLeadQueryService implements LeadQueryService
{
public function __construct(private ?Connection $connection = null)
{
if (!$this->connection) {
$this->connection = Core::getConnection();
}
}
}
// instantiation would be something like -- given you have $connection already instantiated
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
$service = new \Blog\Application\DefaultLeadRecordService(
new \Blog\Infrastructure\DbalLeadQueryService($connection),
new \Blog\Infrastructure\DbalLeadCommandHandler($connection),
);
// if we allow construct get default value
$service = new \Blog\Application\DefaultLeadRecordService(
new \Blog\Infrastructure\DbalLeadQueryService(),
new \Blog\Infrastructure\DbalLeadCommandHandler(),
);
class DefaultLeadRecordService implements LeadRecordService
{
public function __construct(
private LeadQueryService $queryService = new DbalLeadQueryService(),
private LeadCommandHandler $commandHandler = new DbalLeadCommandHandler()
) {
}
}
// we see there is still room for new features here
// still not that great
class DbalLeadQueryService implements LeadQueryService
{
public function __construct(private ?Connection $connection = null)
{
// waiting when `new initializers` feature allows static function as default parameters
if (!$this->connection) {
$this->connection = Core::getConnection();
}
}
}
$service = new \Blog\Application\DefaultLeadRecordService();
<?php
namespace Test\Unit\Blog\Application;
use Blog\Application\DefaultLeadRecordService;
use Blog\Domain\LeadCommandHandler;
use Blog\Domain\LeadQueryService;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class DefaultLeadRecordServiceTest extends TestCase
{
private const EMAIL = '[email protected]';
private LeadQueryService|MockObject $leadQueryServiceMock;
private LeadCommandHandler|MockObject $leadCommandHandlerMock;
private DefaultLeadRecordService $service;
protected function setUp(): void
{
parent::setUp();
$this->leadQueryServiceMock = $this->getMockBuilder(LeadQueryService::class)->getMock();
$this->leadCommandHandlerMock = $this->getMockBuilder(LeadCommandHandler::class)->getMock();
$this->service = new DefaultLeadRecordService($this->leadQueryServiceMock, $this->leadCommandHandlerMock);
}
public function testCanAdd()
{
$this->leadQueryServiceMock
->expects(self::once())
->method('getByEmail')
->with(self::EMAIL)
->willReturn(false);
$this->leadCommandHandlerMock
->expects(self::once())
->method('add')
->with(self::EMAIL)
->willReturn(1);
$result = $this->service->add(self::EMAIL);
self::assertEquals(1, $result);
}
public function testAddExistentReturnsFalse()
{
$this->leadQueryServiceMock
->expects(self::once())
->method('getByEmail')
->with(self::EMAIL)
->willReturn(['email' => self::EMAIL]);
$this->leadCommandHandlerMock
->expects(self::never())
->method('add');
$result = $this->service->add(self::EMAIL);
self::assertFalse($result);
}
public function testCanGetAll()
{
$unsorted = [
['email' => '[email protected]'],
['email' => '[email protected]'],
['email' => '[email protected]'],
];
$this->leadQueryServiceMock
->expects(self::once())
->method('getAll')
->willReturn($unsorted);
$fetched = $this->service->getAll();
$expected = $unsorted;
asort($expected);
self::assertEquals($expected, $fetched);
}
}
$this->databaseFilePath = '/tmp/test-' . time();
and, thanks to the Dbal library, we can be confident that operations could work for any database.<?php
namespace Test\Integration\Blog\Infrastructure;
use Blog\Infrastructure\DbalLeadQueryService;
use Doctrine\DBAL\Connection;
use Faker\Factory;
use Faker\Generator;
use Test\TestCase;
class DbalLeadQueryServiceTest extends TestCase
{
private string $databaseFilePath;
private Generator $faker;
private Connection $connection;
private DbalLeadQueryService $service;
public function testCanGetAll()
{
$this->addEmail($email1 = $this->faker->email());
$this->addEmail($email2 = $this->faker->email());
$this->addEmail($email3 = $this->faker->email());
$expected = [
['email' => $email1],
['email' => $email2],
['email' => $email3],
];
$fetched = $this->service->getAll();
self::assertEquals($expected, $fetched);
}
protected function setUp(): void
{
parent::setUp();
$this->faker = Factory::create();
$this->createLeadTable();
$this->service = new DbalLeadQueryService($this->connection());
}
protected function tearDown(): void
{
parent::tearDown();
$this->dropDatabase();
}
private function connection(): Connection
{
if (!isset($this->connection)) {
$this->databaseFilePath = '/tmp/test-' . time();
$config = new \Doctrine\DBAL\Configuration();
$connectionParams = [
'url' => "sqlite:///{$this->databaseFilePath}",
];
$this->connection = DriverManager::getConnection($connectionParams, $config);
}
return $this->connection;
}
private function dropDatabase()
{
@unlink($this->databaseFilePath);
}
private function createLeadTable(): void
{
$this->connection()->executeQuery('CREATE TABLE IF NOT EXISTS leads ( email VARCHAR )');
}
private function addEmail(string $email): int
{
return $this->connection()->insert('leads', ['email' => $email]);
}
}