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

eggertralf 16 views 66 slides May 31, 2024
Slide 1
Slide 1 of 66
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66

About This Presentation

This talk addresses the challenges of modernizing a PHP application that has been under development since 2007 and was initially tightly coupled with its MVC framework. This tight integration led to various problems, including an increase in bugs and prolonged timeframes for implementing changes. We...


Slide Content

1 / 66
PHP RewritePHP Rewrite
Do the right thingDo the right thing
IPC Berlin 2024IPC 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.
bringbring
telltell

4 / 66
About Ralf EggertAbout 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 ChallengesProject Overview and Challenges
Introducing New MethodologiesIntroducing New Methodologies
Technical ImplementationTechnical Implementation
Results and LearningsResults and Learnings
Q&AQ&A
AgendaAgenda

6 / 66
DisclaimerDisclaimer
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 / 668 / 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 / 669 / 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 / 6610 / 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 / 6611 / 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 / 6612 / 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 / 6613 / 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 / 6614 / 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 / 6615 / 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 / 6616 / 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 / 6617 / 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 / 6618 / 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 / 6623 / 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 / 6624 / 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 / 6625 / 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 / 6629 / 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 / 6630 / 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 / 6631 / 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 / 6632 / 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 / 6634 / 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 / 6636 / 66
Value Objects
Immutable by design.
Constructed via factory methods.
Focuses on attributes.
Uses UUID internally.

37 / 66
Value object example
namespace Community\User\ValueObject;
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 / 6638 / 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 Community\User;
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 Community\User\Service;
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 / 6641 / 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 Community\User;
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 / 6643 / 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 Community\User\Event;
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 / 6645 / 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 Community\User\Event;
class InMemoryEventStore implements EventStoreInterface {
private array $events = [];
public function append( EventInterface $event): void {
$this->events[] = $event;
}
public function allEvents(): array {
return $this->events;
}
}

47 / 6647 / 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 Community\User\Event;
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 Community\User\SideEffect;
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 / 6651 / 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 / 6652 / 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 Community\Tests\User\Event;
#[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('[email protected]');
$name = 'John Doe';
$event = UserRegistered::from($eventId, $userId, $email, $name);
$this->assertEquals($eventId, $event->getEventId());
$this->assertSame($userId, $event->getUserId());
$this->assertEquals('[email protected]', $event->getEmail()->toString());
$this->assertEquals( $name, $event->getName());
}
}

54 / 6654 / 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 Community\Tests\User\Event;
#[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('[email protected]');
$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 / 6657 / 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 / 6658 / 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 / 6660 / 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 / 6661 / 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 / 6662 / 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 / 6663 / 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
ThanksThanks
[email protected]@travello.de
www.travello.de www.travello.de
Our mission:Our mission:
We modernize legacy projects.We modernize legacy projects.