1 / 66
PHP Rewrite
PHP Rewrite
Do the right thing
Do the right thing
IPC Berlin 2024
IPC Berlin 2024
2 / 66
Prelude
3 / 66
Do the right thing,
You got to do the right thing.
I hope you take heed to the message I brought
In another word, the lesson I taught.
bring
bring
tell
tell
4 / 66
About Ralf Eggert
About Ralf Eggert
CEO of Travello GmbH (2005+)
CEO of Travello GmbH (2005+)
PHP & Web Developer (1998+)
PHP & Web Developer (1998+)
ZF fan boy (2006+)
ZF fan boy (2006+)
ZF author & trainer (2008+)
ZF author & trainer (2008+)
Our mission:
Our mission:
We modernize legacy projects.
We modernize legacy projects.
5 / 66
Project Overview and Challenges
Project Overview and Challenges
Introducing New Methodologies
Introducing New Methodologies
Technical Implementation
Technical Implementation
Results and Learnings
Results and Learnings
Q&A
Q&A
Agenda
Agenda
6 / 66
Disclaimer
Disclaimer
Please note: This presentation is based on our
Please note: This presentation is based on our
personal experiences and views.
personal experiences and views.
For confidentiality reasons, all examples used are
For confidentiality reasons, all examples used are
adapted to a fictional migration project involving a
adapted to a fictional migration project involving a
travel community platform.
travel community platform.
It is not meant to be a one-size-fits-all solution, so you
It is not meant to be a one-size-fits-all solution, so you
don't need to throw away your application immediately.
don't need to throw away your application immediately.
At least please wait until the end of this presentation.
At least please wait until the end of this presentation.
7 / 66
Project Overview and Challenges
8 / 66
8 / 66
About the project
Launched in 2000 (not 2007).
Connecting travelers for planning
and sharing their travel experiences.
Key Features: Interactive forum,
photo upload and written travel tipps.
Only community driven content.
My first really big project.
9 / 66
9 / 66
Evolution of the Project
Started in 2000 with custom "pirado"
framework.
Rapid growth from 2000 to 2006,
adding new features and building
community.
Upgraded to Zend Framework 1 in
2007 for better scalability and
features.
10 / 66
10 / 66
Challenges Over the Years
Architecture struggled as features
kept on added regularly.
Complexity caused frequent bugs
and longer dev times.
After 2010, development stopped
due to other priorities.
A ZF2 rewrite failed.
Threw away all tests in 2014
because of lacking test
maintenance.
11 / 66
11 / 66
State Before rewrite
Still based on outdated technology
PHP 5.4 and ZF1.
Maintenance almost stopped except
website is down.
Project is still operational
but full of bugs.
12 / 66
12 / 66
Architecture Overview
Application built as a monolith on
Zend Framework 1.
No modular separation; components
tightly coupled.
All controllers in one directory.
All models in another directory.
No domain separation.
No dependency injection.
13 / 66
13 / 66
Code Complexity
Largest controller weighs 652 KB
and has 17,274 lines of code.
Some controller actions surpass
thousands of lines (mouse wheel
legacy correlation).
Largest model class with 62 KB,
polluted with copy-pasted logic.
Overloaded controllers and models
make testing very complicate.
14 / 66
14 / 66
Framework Coupling
Model & form classes extend ZF1
classes directly.
Controllers packed with lots of copy-
pasted business logic.
Code mess between controller,
models, and services.
Dojo Toolkit integration complicates
backend operations
15 / 66
15 / 66
Failed ZF2 Rewrite
Migration to Zend Framework 2
started but never finished.
Other priorities slowed down the
migration to ZF2.
ZF2 was outdated before the rewrite
project was finished.
16 / 66
16 / 66
Team Structure
Initially developed by one person for
many years.
New team members struggle with
the complex legacy code.
So original developer is still heavily
involved.
Team growth did not work out.
17 / 66
17 / 66
The Need for Change
System cannot keep up with
increasing technical debt.
Need new features to meet market
and user demands.
Essential to update tech for better
security and performance.
18 / 66
18 / 66
Project Summary
Launched in 2000, grew by adding
new features.
Switched to ZF1 in 2007 which is
outdated now.
ZF2 rewrite failed.
Struggles with bugs.
Maintenance has almost stopped.
Still based on old PHP 5.4.
19 / 66
Introducing New Methodologies
20 / 66
New Methodologies
Modular approach to simplify
development and updates.
Enhance team skills with new
development practices.
Adding the Big 3 DDD, Event
Sourcing and TDD.
21 / 66
Infrastructure Update
Upgrade technologies for
performance and security.
Build modular systems for flexibility.
Develop a core application largely
independent of a full-stack
framework to prevent vendor lock-in.
Use QA tools like phpstan,
Code Sniffer, and PHPUnit.
Implement CI/CD for efficiency.
22 / 66
Train the team
Promote learning new methods.
Encourage pair and mob
programming to improve quality.
Standardize processes for
development efficiency.
Every team member should build up
knowledge in the project.
23 / 66
23 / 66
Domain-Driven Design
Focus software development on core
business needs.
Use ubiquitous language to prevent
misunderstandings.
Develop a model that evolves with
the business.
Aim for flexible and relevant
software.
24 / 66
24 / 66
Key Concepts of DDD
Entities are defined by identity.
Value objects focus on attributes.
Aggregates group related objects
as a unit.
Repositories abstract data access
and retrieval.
Domain events capture important
changes in domain state.
25 / 66
25 / 66
Implementation of DDD
Layered DDD architecture isolates
business, UI, and data layers.
Bounded Contexts clarify domain
boundaries between subsystems.
Continuous refinement of the model.
Continuous collaboration with
domain experts is key.
26 / 66
Event Sourcing
All changes are stored as events.
System state can be recreated by
replaying atomic events.
Provides complete audit and change
history via events.
Makes system modifications easier.
27 / 66
Implementing Events
An event represents a meaningful
domain changes.
Events stored in chronological order.
Events are immutable once
recorded.
Events are stored in a dedicated
event store database.
28 / 66
Handling Events
Rebuild state from event history.
You can handle events
asynchronously for efficiency.
You can update read models through
projections.
Handle side effects like sending
mails or logging stuff.
29 / 66
29 / 66
Test-Driven Development
Writing tests before code.
Red: Write failing test for new
feature or functionality.
Green: Write just enough code to
make the test pass.
Refactor: Clean up the new code,
maintaining test pass status.
30 / 66
30 / 66
Writing good Tests
Tests should be clear and specific.
Ensure tests are fast and can run
independently.
Focus each test on a single aspect.
Refactor tests alongside code to
keep them relevant.
31 / 66
31 / 66
test pyramid
Implement a mix of test types.
Focus on numerous small, fast, and
independent unit tests.
Integration tests verify interactions
between components.
End-to-end tests ensure the system
works as intended.
Detroit: Unit Test first (bottom-up).
London: E2E-Test first (top-down).
32 / 66
32 / 66
summary
Upgrade tools & team knowledge to
enhance efficiency & collaboration.
Domain-Driven Design streamlines
complex processes.
Event Sourcing tracks changes for
better accuracy.
Test-Driven Development reduces
bugs and improves code.
33 / 66
Technical Implementation
34 / 66
34 / 66
Bounded Contexts
Separate system into distinct
subsystems to force modularization.
Each context operates
independently.
Each context has his own database.
Contexts commuicate via APIs.
35 / 66
Example: Bounded Contexts
travel_community/
├── user/
│ ├── src/
│ │ ├── User.php
│ │ ├── UserRepository.php
│ │ ├── Event/
│ │ │ ├── UserCreatedEvent.php
│ │ │ └── UserUpdatedEvent.php
│ │ └── ValueObject/
│ │ ├── UserId.php
│ │ └── EmailAddress.php
│ └── tests/
├── forum/
├── photos/
├── travel-tips/
└── travel-booking/
36 / 66
36 / 66
Value Objects
Immutable by design.
Constructed via factory methods.
Focuses on attributes.
Uses UUID internally.
37 / 66
Value object example
namespace CommunityUserValueObject;
final readonly class UserId {
private function __construct(private UUID $id) {
$this->id = $id;
}
public static function generate(): self {
return new self(UUID::generate());
}
public static function fromString(string $id): self {
return new self(UUID::fromString($id));
}
public function asString(): string {
return $this->id->asString();
}
}
38 / 66
38 / 66
Entities
Identified by identity (value object).
Factory methods for generation.
Private constructor: Restricts object
creation to the factory methods.
No setter methods for properties.
Specific methods for state changes,
e.g. activation, password change,
address update, etc.
39 / 66
Entity example
<?php
namespace CommunityUser;
final class User {
private bool $isActive = false;
private DateTimeImmutable $activatedOn = null;
private function __construct(readonly private UserId $userId,
private EmailAddress $email, private string $name) {}
public static function create(EmailAddress $email, string $name): self {
return new self(UserId::generate(), $email, $name);
}
public function activate(): void {
$this->isActive = true;
$this->activatedOn = new DateTimeImmutable;
}
}
40 / 66
Why Entity setters suck
<?php
namespace CommunityUserService;
final readonly class ActivationService {
public function __construct(private UserRepository $repository) {}
public function activateUser(UserId $userId): void {
$user = $this->repository->find($userId);
$user->setActive(true);
$user->setActivatedOn(new DateTimeImmutable);
}
public function activateUser(UserId $userId): void {
$user = $this->repository->find($userId);
$user->activate();
}
}
41 / 66
41 / 66
Repositories
Decouples business logic from
the data storage.
Flexible storages can work with
databases, memory, or APIs
seamlessly.
Eases testing and enables easy
mocking for data operations.
42 / 66
Repository example
<?php
namespace CommunityUser;
interface UserRepositoryInterface {
public function find(UserId $userId): ?User;
public function save(User $user): void;
}
final readonly class UserRepository implements UserRepositoryInterface {
public function __construct(private UserStorageInterface $storage) {}
public function find(UserId $userId): ?User {
Return $this->storage->fetchById($userId);
}
public function save(User $user): void {
$this->storage->saveUser($user);
}
}
43 / 66
43 / 66
Events
Events represent state changes.
Immutable once stored.
Constructed with factory methods.
Include necessary identifiers like
event id or user id.
44 / 66
Event example
<?php
namespace CommunityUserEvent;
final class UserRegistered implements EventInterface {
private function __construct(
private EventId $eventId, private UserId $userId, private EmailAddress $email,
private string $name
) {}
public static function from(EventId $eventId, UserId $userId, EmailAddress $email,
string $name): self {
return new self($eventId, $userId, $email, $name);
}
public function getEventId(): EventId {
return $this->eventId;
}
// more getters
}
45 / 66
45 / 66
Event Store
The Event Store records all system
events centrally.
It stores events in the order they
occur to maintain chronology.
The store can reconstruct the
system state by replaying events.
It provides mechanisms to retrieve
events based on specific criteria.
46 / 66
Event Store example
<?php
namespace CommunityUserEvent;
class InMemoryEventStore implements EventStoreInterface {
private array $events = [];
public function append(EventInterface $event): void {
$this->events[] = $event;
}
public function allEvents(): array {
return $this->events;
}
}
47 / 66
47 / 66
Event Handler
Event handlers process events to
update read models or trigger side
effects (e.g. send an email).
They track processed events in a log
to avoid duplication.
Handlers use dead letter queues for
error handling to manage failures.
Can pass events to an event bus for
async processing.
48 / 66
Event Handler example
<?php
namespace CommunityUserEvent;
class UserEventHandler {
private array $processedEventsLog = [];
public function __construct(private readonly array $sideEffects = []) {}
public function handle(array $events): void {
foreach ($events as $event) {
if (in_array($event->getEventId(), $this->processedEventsLog)) {
continue;
}
$this->processEvent($event);
$this->processedEventsLog[] = $event->getEventId();
}
}
[...]
49 / 66
Event Handler example
[...]
private function processEvent(EventInterface $event): void {
try {
match ($event::class) {
UserRegistered::class => $this->handleRegisteredEvent($event),
default => throw new UnknownEventException('Unknown event type')
};
} catch (Exception $e) {
throw HandleEventException::fromException($e);
}
}
private function handleRegisteredEvent(UserRegistered $event): void
{
$this->sideEffects[UserRegistered::class]->execute($event);
}
}
50 / 66
Side Effect example
<?php
namespace CommunityUserSideEffect;
class SendActivationMail implements SideEffectInterface {
public function __construct(private Mailer $mailer) {}
public function execute(UserRegistered $event): void {
$email = $event->getEmail();
$name = $event->getName();
$subject = "Activate Your Account";
$message = "Hi {$name}, please activate your account by clicking here.";
$this->mailer->send($email, $subject, $message);
}
}
51 / 66
51 / 66
Implementing tdd
Train the team in TDD basics
through workshops.
Start with pair programming to learn.
Use TDD first on small projects to
gain confidence.
Schedule regular refactoring to
practice TDD.
Promote testing as a key part of
development.
52 / 66
52 / 66
Unit Tests
Use PHPUnit for PHP unit tests.
Minimize usage of mocks and
stubs in tests.
Generate readable test
documentation with TestDox.
Ensure full code coverage.
Keep tests simple for easy
maintenance.
53 / 66
Unit Test example
<?php
namespace CommunityTestsUserEvent;
#[CoversClass(UserRegistered::class)]
#[UsesClass(UserId::class)]
#[UsesClass(EmailAddress::class)]
class UserRegisteredTest extends TestCase
{
#[TestDox('It creates a UserRegistered event and validates properties.')]
public function testUserRegisteredEventProperties(): void
{
$eventId = EventId::generate();
$userId = UserId::fromString('user-123');
$email = EmailAddress::fromString('test@example.com');
$name = 'John Doe';
$event = UserRegistered::from($eventId, $userId, $email, $name);
$this->assertEquals($eventId, $event->getEventId());
$this->assertSame($userId, $event->getUserId());
$this->assertEquals('test@example.com', $event->getEmail()->toString());
$this->assertEquals($name, $event->getName());
}
}
54 / 66
54 / 66
Integration Tests
Use PHPUnit for integration tests.
Test against a test database.
Only use mocks and stubs in
exceptional cases.
Use TestDox and code coverage.
Keep tests simple for easy
maintenance.
55 / 66
Integration Test example
<?php
namespace CommunityTestsUserEvent;
#[CoversClass(UserRepository::class)]
#[UsesClass(UserStorage::class)]
#[UsesClass(User::class)]
class UserRepositoryIntegrationTest extends TestCase
{
private PDO $pdo;
private UserRepository $repository;
protected function setUp(): void {
$this->pdo = new PDO('sqlite::memory:');
$this->repository = new UserRepository(new UserStorage($this->pdo));
}
protected function tearDown(): void {
$this->pdo = null;
}
56 / 66
Integration Test example
#[TestDox('It stores and retrieves users correctly via the database.')]
public function testStoreAndRetrieveUser(): void {
$userId = UserId::generate();
$email = EmailAddress::fromString('test@example.com');
$user = new User($userId, $email, 'John Doe');
$this->repository->save($user);
$fetchedUser = $this->repository->find($userId);
$this->assertNotNull($fetchedUser);
$this->assertEquals($user->getEmail(), $fetchedUser->getEmail());
$this->assertEquals($user->getName(), $fetchedUser->getName());
}
}
57 / 66
57 / 66
End-to-end Tests
Use specialized tools over PHPUnit
for complex E2E tests.
Behat is good for behavior-driven
PHP projects.
Codeception handles PHP
acceptance and functional tests well.
Cypress is a good choice for testing
JavaScript frontend interactions.
58 / 66
58 / 66
Summary
Introduced modern coding practices.
Applied DDD for better modularity.
Used Event Sourcing for accurate
history tracking.
Implemented TDD for reliability.
Ensured thorough testing from
unit to E2E.
59 / 66
Results and Learnings
60 / 66
60 / 66
Project Improvements
Increased modularity through
Domain-Driven Design.
Track every state change of the domain
with Event Sourcing.
Reduced bugs and improved
performance with TDD.
Improved code quality metrics from
refactoring sessions.
61 / 66
61 / 66
Advanced Practices
Why integrate TDD, Event Sourcing, and
DDD in same migration project?
TDD a must for us right from the start.
We needed event sourcing combined with
DDD to be able to track events.
Built expertise through training and
team workshops.
Improved team skills with
pair and mob programming.
62 / 66
62 / 66
Key Learnings
Learned valuable lessons from new
development practices.
Plan to expand DDD and TDD across
more projects.
Aim to enhance technology and
processes further.
Focus on broadening team expertise in
advanced methods.
63 / 66
63 / 66
Current Project Status
Making good progress, not yet complete
but on track.
Developers have embraced the new
technologies and methodologies.
Established a strong pair programming
culture enhancing teamwork.
Team grew together and shares now
common knowledge base.
Everyone is optimistic about moving away
from the legacy system.
64 / 66
Q&A
65 / 66
Any Questions?
Any Questions?
66 / 66
Thanks
Thanks
ralf@travello.de
ralf@travello.de
www.travello.de
www.travello.de
Our mission:
Our mission:
We modernize legacy projects.
We modernize legacy projects.

PHP Rewrite: Do the right thing (IPC Berlin 2024)

  • 1.
    1 / 66 PHPRewrite PHP Rewrite Do the right thing Do the right thing IPC Berlin 2024 IPC Berlin 2024
  • 2.
  • 3.
    3 / 66 Dothe right thing, You got to do the right thing. I hope you take heed to the message I brought In another word, the lesson I taught. bring bring tell tell
  • 4.
    4 / 66 AboutRalf Eggert About Ralf Eggert CEO of Travello GmbH (2005+) CEO of Travello GmbH (2005+) PHP & Web Developer (1998+) PHP & Web Developer (1998+) ZF fan boy (2006+) ZF fan boy (2006+) ZF author & trainer (2008+) ZF author & trainer (2008+) Our mission: Our mission: We modernize legacy projects. We modernize legacy projects.
  • 5.
    5 / 66 ProjectOverview and Challenges Project Overview and Challenges Introducing New Methodologies Introducing New Methodologies Technical Implementation Technical Implementation Results and Learnings Results and Learnings Q&A Q&A Agenda Agenda
  • 6.
    6 / 66 Disclaimer Disclaimer Pleasenote: This presentation is based on our Please note: This presentation is based on our personal experiences and views. personal experiences and views. For confidentiality reasons, all examples used are For confidentiality reasons, all examples used are adapted to a fictional migration project involving a adapted to a fictional migration project involving a travel community platform. travel community platform. It is not meant to be a one-size-fits-all solution, so you It is not meant to be a one-size-fits-all solution, so you don't need to throw away your application immediately. don't need to throw away your application immediately. At least please wait until the end of this presentation. At least please wait until the end of this presentation.
  • 7.
    7 / 66 ProjectOverview and Challenges
  • 8.
    8 / 66 8/ 66 About the project Launched in 2000 (not 2007). Connecting travelers for planning and sharing their travel experiences. Key Features: Interactive forum, photo upload and written travel tipps. Only community driven content. My first really big project.
  • 9.
    9 / 66 9/ 66 Evolution of the Project Started in 2000 with custom "pirado" framework. Rapid growth from 2000 to 2006, adding new features and building community. Upgraded to Zend Framework 1 in 2007 for better scalability and features.
  • 10.
    10 / 66 10/ 66 Challenges Over the Years Architecture struggled as features kept on added regularly. Complexity caused frequent bugs and longer dev times. After 2010, development stopped due to other priorities. A ZF2 rewrite failed. Threw away all tests in 2014 because of lacking test maintenance.
  • 11.
    11 / 66 11/ 66 State Before rewrite Still based on outdated technology PHP 5.4 and ZF1. Maintenance almost stopped except website is down. Project is still operational but full of bugs.
  • 12.
    12 / 66 12/ 66 Architecture Overview Application built as a monolith on Zend Framework 1. No modular separation; components tightly coupled. All controllers in one directory. All models in another directory. No domain separation. No dependency injection.
  • 13.
    13 / 66 13/ 66 Code Complexity Largest controller weighs 652 KB and has 17,274 lines of code. Some controller actions surpass thousands of lines (mouse wheel legacy correlation). Largest model class with 62 KB, polluted with copy-pasted logic. Overloaded controllers and models make testing very complicate.
  • 14.
    14 / 66 14/ 66 Framework Coupling Model & form classes extend ZF1 classes directly. Controllers packed with lots of copy- pasted business logic. Code mess between controller, models, and services. Dojo Toolkit integration complicates backend operations
  • 15.
    15 / 66 15/ 66 Failed ZF2 Rewrite Migration to Zend Framework 2 started but never finished. Other priorities slowed down the migration to ZF2. ZF2 was outdated before the rewrite project was finished.
  • 16.
    16 / 66 16/ 66 Team Structure Initially developed by one person for many years. New team members struggle with the complex legacy code. So original developer is still heavily involved. Team growth did not work out.
  • 17.
    17 / 66 17/ 66 The Need for Change System cannot keep up with increasing technical debt. Need new features to meet market and user demands. Essential to update tech for better security and performance.
  • 18.
    18 / 66 18/ 66 Project Summary Launched in 2000, grew by adding new features. Switched to ZF1 in 2007 which is outdated now. ZF2 rewrite failed. Struggles with bugs. Maintenance has almost stopped. Still based on old PHP 5.4.
  • 19.
    19 / 66 IntroducingNew Methodologies
  • 20.
    20 / 66 NewMethodologies Modular approach to simplify development and updates. Enhance team skills with new development practices. Adding the Big 3 DDD, Event Sourcing and TDD.
  • 21.
    21 / 66 InfrastructureUpdate Upgrade technologies for performance and security. Build modular systems for flexibility. Develop a core application largely independent of a full-stack framework to prevent vendor lock-in. Use QA tools like phpstan, Code Sniffer, and PHPUnit. Implement CI/CD for efficiency.
  • 22.
    22 / 66 Trainthe team Promote learning new methods. Encourage pair and mob programming to improve quality. Standardize processes for development efficiency. Every team member should build up knowledge in the project.
  • 23.
    23 / 66 23/ 66 Domain-Driven Design Focus software development on core business needs. Use ubiquitous language to prevent misunderstandings. Develop a model that evolves with the business. Aim for flexible and relevant software.
  • 24.
    24 / 66 24/ 66 Key Concepts of DDD Entities are defined by identity. Value objects focus on attributes. Aggregates group related objects as a unit. Repositories abstract data access and retrieval. Domain events capture important changes in domain state.
  • 25.
    25 / 66 25/ 66 Implementation of DDD Layered DDD architecture isolates business, UI, and data layers. Bounded Contexts clarify domain boundaries between subsystems. Continuous refinement of the model. Continuous collaboration with domain experts is key.
  • 26.
    26 / 66 EventSourcing All changes are stored as events. System state can be recreated by replaying atomic events. Provides complete audit and change history via events. Makes system modifications easier.
  • 27.
    27 / 66 ImplementingEvents An event represents a meaningful domain changes. Events stored in chronological order. Events are immutable once recorded. Events are stored in a dedicated event store database.
  • 28.
    28 / 66 HandlingEvents Rebuild state from event history. You can handle events asynchronously for efficiency. You can update read models through projections. Handle side effects like sending mails or logging stuff.
  • 29.
    29 / 66 29/ 66 Test-Driven Development Writing tests before code. Red: Write failing test for new feature or functionality. Green: Write just enough code to make the test pass. Refactor: Clean up the new code, maintaining test pass status.
  • 30.
    30 / 66 30/ 66 Writing good Tests Tests should be clear and specific. Ensure tests are fast and can run independently. Focus each test on a single aspect. Refactor tests alongside code to keep them relevant.
  • 31.
    31 / 66 31/ 66 test pyramid Implement a mix of test types. Focus on numerous small, fast, and independent unit tests. Integration tests verify interactions between components. End-to-end tests ensure the system works as intended. Detroit: Unit Test first (bottom-up). London: E2E-Test first (top-down).
  • 32.
    32 / 66 32/ 66 summary Upgrade tools & team knowledge to enhance efficiency & collaboration. Domain-Driven Design streamlines complex processes. Event Sourcing tracks changes for better accuracy. Test-Driven Development reduces bugs and improves code.
  • 33.
    33 / 66 TechnicalImplementation
  • 34.
    34 / 66 34/ 66 Bounded Contexts Separate system into distinct subsystems to force modularization. Each context operates independently. Each context has his own database. Contexts commuicate via APIs.
  • 35.
    35 / 66 Example:Bounded Contexts travel_community/ ├── user/ │ ├── src/ │ │ ├── User.php │ │ ├── UserRepository.php │ │ ├── Event/ │ │ │ ├── UserCreatedEvent.php │ │ │ └── UserUpdatedEvent.php │ │ └── ValueObject/ │ │ ├── UserId.php │ │ └── EmailAddress.php │ └── tests/ ├── forum/ ├── photos/ ├── travel-tips/ └── travel-booking/
  • 36.
    36 / 66 36/ 66 Value Objects Immutable by design. Constructed via factory methods. Focuses on attributes. Uses UUID internally.
  • 37.
    37 / 66 Valueobject example namespace CommunityUserValueObject; final readonly class UserId { private function __construct(private UUID $id) { $this->id = $id; } public static function generate(): self { return new self(UUID::generate()); } public static function fromString(string $id): self { return new self(UUID::fromString($id)); } public function asString(): string { return $this->id->asString(); } }
  • 38.
    38 / 66 38/ 66 Entities Identified by identity (value object). Factory methods for generation. Private constructor: Restricts object creation to the factory methods. No setter methods for properties. Specific methods for state changes, e.g. activation, password change, address update, etc.
  • 39.
    39 / 66 Entityexample <?php namespace CommunityUser; final class User { private bool $isActive = false; private DateTimeImmutable $activatedOn = null; private function __construct(readonly private UserId $userId, private EmailAddress $email, private string $name) {} public static function create(EmailAddress $email, string $name): self { return new self(UserId::generate(), $email, $name); } public function activate(): void { $this->isActive = true; $this->activatedOn = new DateTimeImmutable; } }
  • 40.
    40 / 66 WhyEntity setters suck <?php namespace CommunityUserService; final readonly class ActivationService { public function __construct(private UserRepository $repository) {} public function activateUser(UserId $userId): void { $user = $this->repository->find($userId); $user->setActive(true); $user->setActivatedOn(new DateTimeImmutable); } public function activateUser(UserId $userId): void { $user = $this->repository->find($userId); $user->activate(); } }
  • 41.
    41 / 66 41/ 66 Repositories Decouples business logic from the data storage. Flexible storages can work with databases, memory, or APIs seamlessly. Eases testing and enables easy mocking for data operations.
  • 42.
    42 / 66 Repositoryexample <?php namespace CommunityUser; interface UserRepositoryInterface { public function find(UserId $userId): ?User; public function save(User $user): void; } final readonly class UserRepository implements UserRepositoryInterface { public function __construct(private UserStorageInterface $storage) {} public function find(UserId $userId): ?User { Return $this->storage->fetchById($userId); } public function save(User $user): void { $this->storage->saveUser($user); } }
  • 43.
    43 / 66 43/ 66 Events Events represent state changes. Immutable once stored. Constructed with factory methods. Include necessary identifiers like event id or user id.
  • 44.
    44 / 66 Eventexample <?php namespace CommunityUserEvent; final class UserRegistered implements EventInterface { private function __construct( private EventId $eventId, private UserId $userId, private EmailAddress $email, private string $name ) {} public static function from(EventId $eventId, UserId $userId, EmailAddress $email, string $name): self { return new self($eventId, $userId, $email, $name); } public function getEventId(): EventId { return $this->eventId; } // more getters }
  • 45.
    45 / 66 45/ 66 Event Store The Event Store records all system events centrally. It stores events in the order they occur to maintain chronology. The store can reconstruct the system state by replaying events. It provides mechanisms to retrieve events based on specific criteria.
  • 46.
    46 / 66 EventStore example <?php namespace CommunityUserEvent; class InMemoryEventStore implements EventStoreInterface { private array $events = []; public function append(EventInterface $event): void { $this->events[] = $event; } public function allEvents(): array { return $this->events; } }
  • 47.
    47 / 66 47/ 66 Event Handler Event handlers process events to update read models or trigger side effects (e.g. send an email). They track processed events in a log to avoid duplication. Handlers use dead letter queues for error handling to manage failures. Can pass events to an event bus for async processing.
  • 48.
    48 / 66 EventHandler example <?php namespace CommunityUserEvent; class UserEventHandler { private array $processedEventsLog = []; public function __construct(private readonly array $sideEffects = []) {} public function handle(array $events): void { foreach ($events as $event) { if (in_array($event->getEventId(), $this->processedEventsLog)) { continue; } $this->processEvent($event); $this->processedEventsLog[] = $event->getEventId(); } } [...]
  • 49.
    49 / 66 EventHandler example [...] private function processEvent(EventInterface $event): void { try { match ($event::class) { UserRegistered::class => $this->handleRegisteredEvent($event), default => throw new UnknownEventException('Unknown event type') }; } catch (Exception $e) { throw HandleEventException::fromException($e); } } private function handleRegisteredEvent(UserRegistered $event): void { $this->sideEffects[UserRegistered::class]->execute($event); } }
  • 50.
    50 / 66 SideEffect example <?php namespace CommunityUserSideEffect; class SendActivationMail implements SideEffectInterface { public function __construct(private Mailer $mailer) {} public function execute(UserRegistered $event): void { $email = $event->getEmail(); $name = $event->getName(); $subject = "Activate Your Account"; $message = "Hi {$name}, please activate your account by clicking here."; $this->mailer->send($email, $subject, $message); } }
  • 51.
    51 / 66 51/ 66 Implementing tdd Train the team in TDD basics through workshops. Start with pair programming to learn. Use TDD first on small projects to gain confidence. Schedule regular refactoring to practice TDD. Promote testing as a key part of development.
  • 52.
    52 / 66 52/ 66 Unit Tests Use PHPUnit for PHP unit tests. Minimize usage of mocks and stubs in tests. Generate readable test documentation with TestDox. Ensure full code coverage. Keep tests simple for easy maintenance.
  • 53.
    53 / 66 UnitTest example <?php namespace CommunityTestsUserEvent; #[CoversClass(UserRegistered::class)] #[UsesClass(UserId::class)] #[UsesClass(EmailAddress::class)] class UserRegisteredTest extends TestCase { #[TestDox('It creates a UserRegistered event and validates properties.')] public function testUserRegisteredEventProperties(): void { $eventId = EventId::generate(); $userId = UserId::fromString('user-123'); $email = EmailAddress::fromString('test@example.com'); $name = 'John Doe'; $event = UserRegistered::from($eventId, $userId, $email, $name); $this->assertEquals($eventId, $event->getEventId()); $this->assertSame($userId, $event->getUserId()); $this->assertEquals('test@example.com', $event->getEmail()->toString()); $this->assertEquals($name, $event->getName()); } }
  • 54.
    54 / 66 54/ 66 Integration Tests Use PHPUnit for integration tests. Test against a test database. Only use mocks and stubs in exceptional cases. Use TestDox and code coverage. Keep tests simple for easy maintenance.
  • 55.
    55 / 66 IntegrationTest example <?php namespace CommunityTestsUserEvent; #[CoversClass(UserRepository::class)] #[UsesClass(UserStorage::class)] #[UsesClass(User::class)] class UserRepositoryIntegrationTest extends TestCase { private PDO $pdo; private UserRepository $repository; protected function setUp(): void { $this->pdo = new PDO('sqlite::memory:'); $this->repository = new UserRepository(new UserStorage($this->pdo)); } protected function tearDown(): void { $this->pdo = null; }
  • 56.
    56 / 66 IntegrationTest example #[TestDox('It stores and retrieves users correctly via the database.')] public function testStoreAndRetrieveUser(): void { $userId = UserId::generate(); $email = EmailAddress::fromString('test@example.com'); $user = new User($userId, $email, 'John Doe'); $this->repository->save($user); $fetchedUser = $this->repository->find($userId); $this->assertNotNull($fetchedUser); $this->assertEquals($user->getEmail(), $fetchedUser->getEmail()); $this->assertEquals($user->getName(), $fetchedUser->getName()); } }
  • 57.
    57 / 66 57/ 66 End-to-end Tests Use specialized tools over PHPUnit for complex E2E tests. Behat is good for behavior-driven PHP projects. Codeception handles PHP acceptance and functional tests well. Cypress is a good choice for testing JavaScript frontend interactions.
  • 58.
    58 / 66 58/ 66 Summary Introduced modern coding practices. Applied DDD for better modularity. Used Event Sourcing for accurate history tracking. Implemented TDD for reliability. Ensured thorough testing from unit to E2E.
  • 59.
    59 / 66 Resultsand Learnings
  • 60.
    60 / 66 60/ 66 Project Improvements Increased modularity through Domain-Driven Design. Track every state change of the domain with Event Sourcing. Reduced bugs and improved performance with TDD. Improved code quality metrics from refactoring sessions.
  • 61.
    61 / 66 61/ 66 Advanced Practices Why integrate TDD, Event Sourcing, and DDD in same migration project? TDD a must for us right from the start. We needed event sourcing combined with DDD to be able to track events. Built expertise through training and team workshops. Improved team skills with pair and mob programming.
  • 62.
    62 / 66 62/ 66 Key Learnings Learned valuable lessons from new development practices. Plan to expand DDD and TDD across more projects. Aim to enhance technology and processes further. Focus on broadening team expertise in advanced methods.
  • 63.
    63 / 66 63/ 66 Current Project Status Making good progress, not yet complete but on track. Developers have embraced the new technologies and methodologies. Established a strong pair programming culture enhancing teamwork. Team grew together and shares now common knowledge base. Everyone is optimistic about moving away from the legacy system.
  • 64.
  • 65.
    65 / 66 AnyQuestions? Any Questions?
  • 66.
    66 / 66 Thanks Thanks ralf@travello.de ralf@travello.de www.travello.de www.travello.de Ourmission: Our mission: We modernize legacy projects. We modernize legacy projects.