Freelancer software engineer
include 'db_config.php';
if (!$connection = mysql_connect($hostname, $username, $password)) {
die('Erreur de connexion : ' . mysql_error());
}
mysql_select_db($dbname, $connection);
if (!isset($_SESSION['userLoggedIn'])) exit;
if (isset($_POST['name'] && $_POST['description'])) {
$name = mysql_real_escape_string($_GET['name']);
$description = mysql_real_escape_string($_POST['description']);
$query = "INSERT INTO map (name, description) " .
"VALUES ('$name', '$description')";
if (!$result = mysql_query($query, $connection)) {
die('Invalid query: ' . mysql_error());
}
}
mysql_close($connection);
It prevents the need to reinvent the wheel by solving technical problems
Symfony and its ecosystem have made the development of PHP apps easier
It helped me to ship CRUD applications more quickly
I mainly contributed to developing the resource bundle
Entity Manager
Database
Doctrine entity
HTTP Call
Serializer
Form
Request
Controller
We needed more than just a framework and libraries to solve our problems
Akeneo PIM looks like a big CRUD app, but it isn't
Controller
Form / Serializer
Anemic model
HTTP Call
Request
ORM
Database
Entities, Value objects and Aggregates
help to design rich domain objects
Value object
Entity
Identity-less
Attribute based equality
Immutable
Behavior-rich
Self-validating
Enforce invariant
Focus on behavior (not data)
Has an identity
Identity based equality
An aggregate is a cluster of objects (entities and value objects) that together represent a domain concept
Map
Marker
Media
Id
Name
Location
Name
Path
aggregate root
entity
entity
Value object
Value object
Name
Name
Id
Marker List
Map Gallery
Maker Gallery
Value object
Value object
Primitive
Value object
Map
Marker
Media
Cartographer
MarkerCategory
ID
ID
ID
final class Map
{
public function __construct(
private readonly string $mapId,
private Name $name,
private MarkerList $markers,
) {
}
public static function create(string $mapId, string $name): self
{
//...
}
public function addMarker(
string $markerId, string $name, float $latitude, float $longitude
): void {
//...
}
public function shareMap(string $cartographerId): void {
//...
}
}
final class MarkerList
{
private readonly array $markers;
public function __construct(Marker ...$markers) {
$this->markers = $markers;
}
public static function empty(): MarkerList
{
return new self();
}
public function add(Marker $marker): MarkerList
{
return new self($marker, ...$this->markers);
}
public function remove(string $markerId): MarkerList
{
return new self(...array_filter(
$this->markers,
fn (Marker $marker) => !$marker->equal($markerId),
));
}
}
addMarker
Get and dispatch
Map
records
MarkerAdded
final class MarkerAdded
{
public function __construct(
private readonly string $mapId,
private readonly string $markerId,
private readonly Name $name,
private readonly Location $location,
) {}
}
final class Map
{
public array $events = [];
// ...
public function addMarker(string $markerId, string $name, /** Etc. */): void
{
$name = new Name($name);
$location = new Location($latitude, $longitude);
$this->markers = $this->markers->add(
new Marker($markerId, $name, $location, $addedAt)
);
$this->events[] = new MarkerAdded($this->mapId, $markerId, $name, $location);
}
}
Controller
Form / Serializer
Anemic model
HTTP Call
Request
ORM
Database
A repository behaves like a collection of aggregates that aims to hide the complexity of storing an object’s state
interface Maps
{
/**
* @throws \LogicException
* @throws UnknownMap
*/
public function get(string $mapId): Map;
/**
* @throws \LogicException
*/
public function add(Map $map): void;
}
Persistence model
Anemic model
Domain model
Rich model
Store
Query
Domain model
Persistence model
final class DoctrineMaps implements Maps
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}
public function add(Map $map): void
{
try {
$this->entityManager->persist($map);
$this->entityManager->flush();
} catch (ORMInvalidArgumentException | ORMException $e) {
throw new \LogicException('Cannot persist the map', 0, $e);
}
}
// ...
}
final class DoctrineMaps implements Maps
{
// ...
public function get(string $id): Map
{
try {
$map = $this->entityManager->find(Map::class, $id);
if (null === $map) {
throw new UnknownMap::fromId(id);
}
return $map;
} catch (ORMInvalidArgumentException | ORMException $e) {
throw new \LogicException('Cannot retreive the map', 0, $e);
}
}
}
Persistence
model
Store
Query
Convert
Domain model
final class DoctrineMaps implements Maps
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}
public function add(Map $map): void
{
try {
$doctrineMap = $this->entityManager->find(
DoctrineMap::class,
(string) $map->id
);
if (!$doctrineMap) {
$doctrineMap = new DoctrineMap();
$this->entityManager->persist($doctrineMap);
}
$map->mapTo($doctrineMap);
$this->entityManager->flush();
} catch (ORMInvalidArgumentException|ORMException $e) {
throw new \LogicException('Cannot persist the map', 0, $e);
}
}
// ...
}
final class DoctrineMaps implements Maps
{
// ...
public function get(Id $id): Map
{
try {
$doctrineMap = $this->entityManager->find(
DoctrineMap::class,
(string) $id
);
if (!$doctrineMap) {
throw UnknownMap::fromId($id);
}
return Map::fromState($doctrineMap);
} catch (ORMInvalidArgumentException|ORMException $e) {
throw new \LogicException('Cannot retrieve the map', 0, $e);
}
}
// ...
}
Controller
Form / Serializer
Anemic model
HTTP Call
Request
ORM
Database
The logic for the use case is implemented in the controller
What happens if you need to handle the same use case through a web controller and a CLI?
It can end with 'Service' or 'Manager' classes
Commands and command handlers help to handle use cases
A command represents the user’s intent, while the command handler performs the actions needed to achieve the use case
A command must be valid to be handled
A command handler only handles a single command
Create command
Handle command
Validate command
Dispatch Event
validator
form & serializer
messenger
final class AddMarkerToMap
{
public function __construct(
public readonly string $mapId,
public readonly string $markerId,
public readonly string $name,
public readonly float $latitude,
public readonly float $longitude,
) {
}
}
final class AddMarkerToMapHandler
{
public function __construct(private readonly Maps $maps) {}
public function __invoke(AddMarkerToMap $addMarkerToMap): void
{
$map = $this->maps->get($addMarkerToMap->mapId);
$map->addMarker(
$addMarkerToMap->markerId,
$addMarkerToMap->name,
$addMarkerToMap->latitude,
$addMarkerToMap->longitude
);
$this->maps->add($map);
}
}
Controller
Repository
Anemic model
HTTP Call
Request
Serializer
Response
You need getters to access an object’s state, but they can break encapsulation
Why use serializers for tasks that your database can handle?
Multiple queries for complex read use cases
Greg Young's Blog screenshot (only available on archive.org)
CQRS is simply the creation of two objects where there was previously only one. The separation occurs based upon whether the methods are a command or a query
Greg Young
Map Service
(write)
addMarker
moveMarker
Map Service
addMarker
moveMarker
openMap
findMyMap
Map Service
(read)
openMap
findMyMap
Write side
Read side
Query
Command
Command Handler
Aggregate
Query Function
Result
Read Model
final class DoctrineOpenMap implements OpenMap
{
// Constructor
public function __invoke(string $mapId): Map
{
$sql = "<<<SQL
SELECT map.map_id, map.name, COALESCE(
json_agg(marker) FILTER (WHERE marker.marker_id IS NOT NULL),
'[]'
) AS markers
FROM map
LEFT JOIN marker ON map.map_id = marker.map_id
WHERE map.map_id = :map_id
GROUP BY map.map_id
SQL";
try {
$statement = $this->connection->executeQuery($sql, ['map_id' => $mapId]);
$map = $statement->fetchAssociative();
} catch (DBALException $e) {
throw new LogicException("SQL query failed {$e->getMessage()}", 0, $e);
}
if (false === $map) {
throw UnknownMap::fromId($mapId);
}
return Map::fromArray($map);
}
}
class Map
{
public function __construct(
public readonly string $mapId,
public readonly string $name,
public readonly array $markers,
) {
}
public static function fromArray(array $map): self
{
return new self(
$map['map_id'],
$map['name'],
array_map(
fn(array $marker) => Marker::fromArray($marker),
json_decode($map['markers'], true)
)
);
}
}
class Marker
{
public function __construct(
public readonly string $markerId,
public readonly string $name,
public readonly float $latitude,
public readonly float $longitude,
) {
}
}
It is useful to collect data from multiple aggregates
It is better to work with smaller business-oriented objects #myTwoCents
We can do more with CQRS, but do you really need that?
or the Port & Adapter Architecture
Domain
Secondary ports
Primary ports
Adapter
Port
Port
Adapter
Port
Adapter
Port
Adapter
The domain is the area where we solve our business problems no matter the technical constraints
No framework, business focused
No Input Output (tools)
No framework doesn't mean no libraries
Test with shoot feedback loop
Domain
command handlers
Entities
Value objects
Domain services
Thanks to Secondary Ports
Domain (no IO)
Infrastructure (IO)
Port
Adapter
Adapter
Domain (no IO)
Infrastructure (IO)
Interface
Concrete class
Concrete class
Domain (no IO)
Infrastructure (IO)
Maps
PosgresqlMaps
InMemoryMaps
We can easily postpone the decision on which tools to use
Thanks to Primary Ports
Domain
Primary ports
Port
Web
CLI
Secondary ports
Adapter
Port
Adapter
class WebAddMarkerToMap extends AbstractController
{
// constructor
#[Route('/maps', methods: ['PUT'])]
public function __invoke(Request $request): Response
{
$command = $this->serializer->deserialize(
$request->getContent(),
AddMarkerToMap::class,
'json',
);
if (count($errors = $this->validator->validate($command)) > 0) {
return new Response(
$this->serializer->serialize($errors, 'json'),
400
);
}
$this->entityManager->wrapInTransaction(
fn () => ($this->createMapHandler)($command)
);
return new Response();
}
}
class CliAddMarkerToMap extends Command
{
// constructor
protected function configure(): void
{
$this->addArgument('name', InputArgument::REQUIRED, 'Marker name');
// Etc.
}
protected function execute(
InputInterface $input, OutputInterface $output
): int {
$command = new AddMarkerToMap($input->getArgument('name'), /** etc. */);
if (count($this->validator->validate($command)) > 0) {
return Command::FAILURE;
}
$this->entityManager->wrapInTransaction(
fn () => ($this->createMapHandler)($command)
);
return Command::SUCCESS;
}
}
Yes, with a Command Bus
Logging
middleware
Validation
middleware
Transactional
middleware
Handle command
middleware
Command
Command bus
messenger
class WebAddMarkerToMap extends AbstractController
{
// constructor
#[Route('/maps', methods: ['PUT'])]
public function __invoke(Request $request): Response
{
$command = $this->serializer->deserialize(
$request->getContent(),
AddMarkerToMap::class,
'json',
);
try {
$this->bus->dispatch($command);
} catch (ValidationFailedException $e) {
return new Response(
$this->serializer->serialize($e->getViolations(), 'json'),
400
);
}
return new Response();
}
}
Domain
Port
Port
Web
Adapter
Input
Output
Integration tests
(cover the infra)
End to End tests
Unit tests
(cover the domain and more)
Fast
Slow
Dev
Regression
Controller
Handler
Aggregate
Repository
Command
APP_ENV=test
APP_ENV=testio
Primary adapters
Secondary adapters
Domain
Controller
Handler
Aggregate
Repository
Command
tests without IO
tests with IO
Long feedback loop
Fast feedback loop
Pyramide des tests ?
// it uses the WebTestCase
describe('user interface - web - create a map', function () {
it('creates a map from the web end point', function () {
/** @var KernelBrowser $client */
$client = static::createClient();
$client->request('POST', '/maps', content: json_encode([
'name' => 'My map',
]));
expect($client->getResponse()->getStatusCode())->toEqual(200);
});
});
describe('map use cases', function () {
test('a cartographer adds marker to a map', function () {
$mapId = Uuid::v4();
$maps = new InMemoryMaps([Map::whatever(id: $mapId, markers: [])]);
$markerId = Uuid::v4();
$name = 'Sunset';
$latitude = 43.4833;
$longitude = -1.5167;
(new AddMarkerToMapHandler($maps))(
new AddMarkerToMap(
$mapId, $markerId, $name, $latitude, $longitude
)
);
expect($maps->get(new Id($mapId)))->toEqual(
Map::whatever(id: $mapId, markers: [
Marker::whatever($markerId, $name, $latitude, $longitude)
])
);
});
});
describe('infra - maps', function () {
it('persists and retrieves a map with marker', function ($serviceName) {
$this->container->get(Database::class)->truncateAllTables();
$maps = $this->container->get($serviceName);
$map = Map::whatever(Uuid::v4(), markers: [
Marker::whatever(Uuid::v4()),
Marker::whatever(Uuid::v4()),
]);
$maps->add($map);
expect($maps->get($map->id))->toEqual($map);
})->with([
InMemoryMaps::class,
DoctrineMaps::class,
]);
});
Acceptance tests
tests without IO
tests with IO
Controller
Handler
Aggregate
Repository
Unit tests
Integration tests
Controller
Handler
Aggregate
Repository
Command
Acceptance tests
Integration tests
Unit tests
Unit tests
tests without IO
tests with IO
Doctrine entities do not specifically describe business problems
Doctrine entities are persistence models
Doctrine entities are data structures (POJO)
Problème 1
The repository design pattern is often misunderstood
Persistence model versus Domain model
A repository helps to store the state of domain objects
Request
Repository
Doctrine entity
Serialiser
Controller
Ce n'est pas quelque chose dédié à la lecture
Serializer
Form
Request
Controller
Entity Manager
Doctrine entity
Entity Manager
Serializer
Form
Request
Controller
Doctrine entity
Entity Manager
Doctrine entity
Serializer
Form
Request
Controller
The answer is no!