PHP Frameworks: I want to break free (IPC Berlin 2024)

eggertralf 48 views 84 slides May 31, 2024
Slide 1
Slide 1 of 84
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
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76
Slide 77
77
Slide 78
78
Slide 79
79
Slide 80
80
Slide 81
81
Slide 82
82
Slide 83
83
Slide 84
84

About This Presentation

In this presentation, we examine the challenges and limitations of relying too heavily on PHP frameworks in web development. We discuss the history of PHP and its frameworks to understand how this dependence has evolved. The focus will be on providing concrete tips and strategies to reduce reliance ...


Slide Content

1 / 84
PHP FrameworksPHP Frameworks
I want to break freeI want to break free
IPC Berlin 2024IPC Berlin 2024

2 / 84
Prelude

3 / 84
I want to break free
I want to break free
I want to break free from your lies
You're so self-satisfied, I don't need you
I've got to break free
tiesties
I want to break free from your lies

4 / 84
About Ralf EggertAbout Ralf Eggert
CEO of Travello GmbH (2005+)CEO of Travello GmbH (2005+)
PHP & Web Developer (1999+)PHP & Web Developer (1999+)
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 / 84
The evolution of PHP and its frameworksThe evolution of PHP and its frameworks
My personal PHP framework journeyMy personal PHP framework journey
Strategies to reduce framework dependencyStrategies to reduce framework dependency
Q&AQ&A
AgendaAgenda

6 / 84
DisclaimerDisclaimer
Please note: This presentation is based on my Please note: This presentation is based on my
personal experiences and views. personal experiences and views.
I am aware that a web application can hardly be I am aware that a web application can hardly be
implemented without a framework. implemented without a framework.

7 / 84
The Evolution of PHP and its Frameworks

8 / 848 / 84
PHP 3 in a nutshell
Release Year: 1998
Introduced very limited OOP
features (no inheritance, visibility)
Need for structured code
in larger applications
Early libraries: PHPlib, PEAR.

9 / 84
PHP 4 in a nutshell
Release Year: 2000
Improved, but limited OOP features
Complex web apps required more
structured solutions
Forced framework adoption for
better structure, and maintainability
First framework generation: Mojavi,
Prado, Seagull & CakePHP

10 / 8410 / 84
PHP 5 in a nutshell
Release Year: 2004
Major OOP enhancements including
visibility, interfaces, and exceptions.
New OOP capabilities motivated the
second generation of frameworks
Zend Framework, Symfony, Yii and
CodeIgniter founded a new era of
MVC frameworks for PHP

11 / 84
PHP 6 in a nutshell
Release Year: never
No new improvements to
the PHP ecosystem.
Had no impact on any
PHP framework.
Nevertheless, several books have
been written about it

12 / 84
PHP 7 in a nutshell
Release Year: 2015
Major performance improvements
with the new Zend Engine 3.
Composer transitioned frameworks
from silos to component libraries.
Symfony 4, Laravel 5.5, ZF3,
CakePHP 4 required PHP 7.
Micro-frameworks: Zend Expressive,
Symfony Silex, Laravel Lumen.

13 / 84
PHP 8 in a nutshell
Release Year: 2020
Emphasis on performance
improvement and code clarity.
Frameworks like Symfony 5,
Laminas and Laravel 8 use new
PHP 8 features for cleaner, more
expressive code.

14 / 84
My personal
PHP Framework Journey

15 / 84
PHP 3: The dark ages
From 1998 till 2000.
Before PHP, I created HTML Pages
and used some CGI Perl scripts.
Working with PHP 3 was much
easier than working with Perl.
Everything in one file. Yeah!

16 / 84
PHP 3: example
<?php
include('config.php');
$conn = mysql_connect($dbHost, $dbUser, $dbPassword);
mysql_select_db($dbName, $conn);
$query = "SELECT id, title, teaser FROM news ORDER BY date ASC ";
$result = mysql_query($query, $conn);
?>
<html>
<head><title><?php echo $pageTitle; ?></title></head>
<body>
<h1>Latest News</h1>
<?php while ($row = mysql_fetch_assoc($result)): ?>
<h2><?php echo $row['title']; ?></h2>
<p><?php echo $row['teaser']; ?></p>
<?php endwhile; ?>
<?php mysql_close($conn); ?>
<? include('footer.php'); ?>
</body>
</html>

17 / 84
PHP 4: My 1st framework
From 2001 till 2005.
Worked half a year on my first PHP
framework »pirado«.
»pirado« had only 2 users.
Built a travel community on top of it.
Had some separation of concerns.

18 / 84
PHP 4: example
class NewsModel {
function News($dbHost, $dbUser, $dbPassword, $dbName) {
$this->conn = mysql_connect($dbHost, $dbUser, $dbPassword);
mysql_select_db($dbName, $this->conn);
}

function getNews() {
$query = "SELECT id, title, teaser FROM news ORDER BY date ASC";
$result = mysql_query($query, $this->conn);
$news = array();
while ($row = mysql_fetch_assoc($result)) {
$news[] = $row;
}
return $news;
}
}

19 / 84
PHP 4: example
<?php
require('libs/Smarty.class.php');
include('config.php');
require('NewsModel.class.php');
$newsModel = new NewsModel($dbHost, $dbUser, $dbPassword, $dbName);
$newsList = $newsModel->getNews();
$smarty = new Smarty;
$smarty->assign('news', $newsList);
$smarty->display('news.tpl');
?>

20 / 84
PHP 4: example
{include file="header.tpl"}
<h1>Latest News</h1>
<ul>
{foreach from=$news item=newsItem}
<h2>{$newsItem.title}</h2>
<p>{$newsItem.teaser}</p>
{/foreach}
</ul>
{include file="footer.tpl"}

21 / 84
PHP 5: ZF1 Fan boy
From 2006 till 2012.
Built some large projects with ZF1.
Wrote a best selling book and a
couple of articles about it.
Held some training courses and
presentations on ZF1.
Clear MVC patterns, but controllers /
models tended to grow too l

22 / 84
ZF1: Model example
require_once 'Zend/Db/Table/Abstract.php';
class NewsModel extends Zend_Db_Table_Abstract {
protected $_name = 'news'; // Name der Datenbanktabelle
public function getNews() {
$select = $this->select();
$select->from($this->_name, array( 'id', 'title', 'teaser'));
$select->order('date' );
return $this->fetchAll($select);
}
}

23 / 84
ZF1: controller example
require_once 'Zend/Controller/Action.php';
class IndexController extends Zend_Controller_Action {
public function indexAction() {
$newsModel = new NewsModel();
$news = $newsModel->getNews();
$this->view->news = $news;
}
}

24 / 84
ZF1: View example
<h1>Latest News</h1>
<?php foreach ($this->news as $newsItem): ?>
<h2><?php echo $this->escape($newsItem->title); ?></h2>
<p><?php echo $this->escape($newsItem->teaser); ?></p>
<?php endforeach; ?>

25 / 84
PHP 5: ZF2 Coach & Author
From 2012 till 2016.
Struggled with migration to ZF2.
Wrote another book and many, many
articles about it.
Held a lot of training courses (> 30)
and presentations on ZF2.
Clear MVC pattern, and controllers /
models tended to be smalle

26 / 84
ZF2: Model example
namespace News\Model;
use Zend\Db\TableGateway\TableGatewayInterface;
class NewsRepository {
protected $tableGateway;
public function __construct(TableGatewayInterface $tableGateway) {
$this->tableGateway = $tableGateway;
}
public function getAllNews() {
$select = $this->tableGateway->select()
$select->from($this->_name, array( 'id', 'title', 'teaser'));
$select->order('date' );
return $this->tableGateway->fetchAll($select );
}
}

27 / 84
ZF2: Controller example
namespace News\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use News\Model\NewsRepository;
class NewsController extends AbstractActionController {
private $repository;
public function __construct(NewsRepository $ repository) {
$this->repository = $ repository;
}
public function indexAction() {
return new ViewModel([
'news' => $this->repository-> getAllNews(),
]);
}
}

28 / 84
ZF2: View example
<h1>Latest News</h1>
<?php foreach ($this->news as $newsItem): ?>
<h2><?php echo $this->escapeHtml($newsItem->title); ?></h2>
<p><?php echo $this->escapeHtml($newsItem->teaser); ?></p>
<?php endforeach; ?>

29 / 84
PHP 7: ZF3 Enthusiast
From 2016 till 2019.
Still sad about the decline of the ZF.
Wrote my last book and a couple
of articles about it.
Held a couple of training courses
and presentations on ZF3.
Prefer middleware (Expressive)
to MVC.

30 / 84
ZF3: Repository example
namespace News\Model\Repository;
use News\Model\Storage\NewsStorageInterface ;
class NewsRepository implements NewsRepository Interface {
protected $newsStorage;
public function __construct(NewsStorageInterface $ newsStorage) {
$this->newsStorage = $ newsStorage;
}
public function getAllNews() {
return $this->newsStorage->fetchAllNews();
}
}

31 / 84
ZF3: Storage example
namespace News\Model\Storage;
use Zend\Db\TableGateway\TableGatewayInterface;
class NewsStorage implements NewsStorageInterface {
protected $tableGateway;
public function __construct(TableGatewayInterface $tableGateway) {
$this->tableGateway = $tableGateway;
}
public function fetchAllNews() {
$select = $this->tableGateway->select()
$select->from($this->_name, array( 'id', 'title', 'teaser'));
$select->order('date' );
return $this->tableGateway->fetchAll($select );
}
}

32 / 84
ZF3: Middleware example
namespace News\Handler;
class NewsHandler {
private $newsRepository;
private $template;
public function __construct(
NewsRepository $newsRepository, TemplateRendererInterface $template
) {
$this->newsRepository = $newsRepository;
$this->template = $template;
}
public function handle(ServerRequestInterface $request): ResponseInterface {
$news = $this->newsRepository->getAllNews();
return new HtmlResponse(
$this->template->render('news::list', ['news' => $news])
);
}
}

33 / 84
PHP 8: Laminas took over
Since 2020.
Mainly continues Zend Framework.
Supported by Linux Foundation
Clear separation into Laminas
Components, Mezzio and MVC
No more versioning of the
framework, but

34 / 8434 / 84
My ideal framework...
... allows for modularity and decoupling.
... helps to keep business logic separate
from framework code.
... offers flexibility in component
selection.
... provides robust middleware support.
... does not force me to run updates after
its release cycle.

35 / 84
Strategies to Reduce
Framework Dependency

36 / 84
Structured monoliths

37 / 84
Example: Structured Monoliths
├── module/
│ └── App/
│ ├── src/
│ │ ├── Controllers/
│ │ │ ├── ArticleController.php
│ │ │ ├── CartController.php
│ │ │ ├── HomeController.php
│ │ │ └── UserController.php
│ │ ├── Models/
│ │ │ ├── Article.php
│ │ │ ├── Cart.php
│ │ │ └── User.php
│ │ └── Services/
│ │ ├── ArticleService.php
│ │ ├── CartService.php
│ │ └── UserService.php
│ └── tests/
├── public/
└── vendor/

38 / 8438 / 84
Structured Monoliths
Frameworks usually provide
predefined structures.
Beginners tend to put all files in the
App bundle/module/whatever.
This quickly leads to monolithic
applications.

39 / 84
Example: Embrace Modularity
├── module/
│ ├── App/
│ │ └── src/
│ │ └── Controllers/
│ │ └── HomeController.php
│ ├── Article/
│ │ ├── src/
│ │ │ ├── Controllers/
│ │ │ │ └── ArticleController.php
│ │ │ ├── Models/
│ │ │ │ ├── Article.php
│ │ │ │ └── ArticleRepository.php
│ │ │ └── Services/
│ │ │ └── ArticleService.php
│ │ └── tests/
│ ├── Cart/
│ └── User/
├── public/
└── vendor/

40 / 8440 / 84
Embrace Modularity
Break application into independent
modules from the start.
Organize controllers, models, services
and other classes in separate folders of
each module.
Keep tests for modules in dedicated test
folder of each module.
Easily extend application with new
modules / bundles / whatever.

41 / 84
Fat Controller / Handler

42 / 84
Example: Fat Controller / Handler
namespace Shop\Http\Controllers;
final readonly class OrderController extends Controller {
public function create(Request $request) {
$product = Product::find($request->input('product_id'));
if ($product->stock < $request->input('quantity')) {
return response()->json(['error' => 'Not enough stock'], 400);
}
$order = new Order();
$order->product_id = $request->input('product_id');
$order->quantity = $request->input('quantity');
$order->total_price = $product->price * $request->input('quantity');
$order->save();
$product->stock -= $request->input('quantity');
$product->save();
return response()->json(['message' => 'Order created successfully'], 201);
}
}

43 / 8443 / 84
Fat controllers
Controllers handle too much logic.
This also applies to handlers in
middleware frameworks
Testing of a fat controller is difficult.
Domain and controller logic is mixed.
Domain logic in controllers
cannot be reused.
The code is complex and hard to
understand.

44 / 84
Example: Keep controllers / handlers slim
namespace Shop\Services;
final readonly class OrderService {
public function createOrder($productId, $quantity) {
$product = Product::find($productId);
if ($product->stock < $quantity) {
throw new OutOfStockException('Not enough stock');
}
$order = new Order();
$order->product_id = $productId;
$order->quantity = $quantity;
$order->total_price = $product->price * $quantity;
$order->save();
$product->stock -= $quantity;
$product->save();
return $order;
}
}

45 / 84
Example: Keep controllers / handlers slim
namespace Shop\Http\Controllers;
final readonly class OrderController extends Controller {
public function __construct(private OrderService $orderService) {}
public function create(Request $request) {
try {
$order = $this->orderService->createOrder(
$request->input('product_id'),
$request->input('quantity')
);
return response()->json(['message' => 'Order created successfully'], 201);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}

46 / 8446 / 84
Keep controllers slim
Move domain logic to service classes.
Easier to test controllers and domain
services logic separately.
Clear distinction between controller and
domain logic.
Domain logic in services can be reused.
Slimmer controllers / handlers are easier
to maintain and understand.

47 / 84
Mixed services

48 / 84
Example: Mixed Services
namespace Shop\Services;
final readonly class OrderService {
public function createOrder($productId, $quantity) {
$product = Product::find($productId);
if ($product->stock < $quantity) {
Notification::send($order->user, new OutOfStock($ product));
throw new OutOfStockException('Not enough stock');
}
$order = new Order(); // order generation hidden
$order->save();
$product->stock -= $quantity;
$product->save();
Event::dispatch('order.created', $order);
Notification::send($order->user, new OrderShipped($order));
return $order;
}
}

49 / 8449 / 84
Mixed Services
Framework-specific code is mixed
with domain logic.
Hard to test domain logic
independently.
Tight coupling between application
and domain logic.
Difficult to maintain and extend.

50 / 84
Example: Split your Services
namespace Shop\Domain\Services;
final readonly class DomainOrderService {
public function createOrder($productId, $quantity) {
$product = Product::find($productId);
if ($product->stock < $quantity) {
throw new OutOfStockException('Not enough stock');
}
$order = new Order(); // order generation hidden
$order->save();
$product->stock -= $quantity;
$product->save();
return $order;
}
}

51 / 84
Example: Split your Services
namespace Shop\Services;
final readonly class ApplicationOrderService {
public function __construct(private DomainOrderService $domainOrderService) {}
public function createOrder($productId, $quantity) {
try {
$order = $this->domainOrderService->createOrder($productId, $quantity);
} catch (OutOfStockException $e) {
Notification::send($order->user, new OutOfStock($product));
throw $e;
}
Event::dispatch('order.created', $order);
Notification::send($order->user, new OrderShipped($order));
return $order;
}
}

52 / 8452 / 84
Split your Services
Separate framework-specific code from
the pure domain logic.
Use domain services for core business
logic.
Use application services for
framework-specific tasks.
Improves maintainability and testability.

53 / 84
Use ORM directly

54 / 84
Example: Use ORM directly
namespace Job\Domain\Service;
use Doctrine\ORM\EntityManagerInterface;
use Job\Domain\Job;
final readonly class DomainJobService {
public function __construct(private EntityManagerInterface $entityManager) {}
public function getJobById(int $id): ?Job {
return $this->entityManager->getRepository(Job::class)->find($id);
}
public function saveJob(Job $job): void {
$this->entityManager->persist($job);
$this->entityManager->flush();
}
}

55 / 8455 / 84
Use ORM directly
Domain logic is mixed with
infrastructure logic.
Hard to test domain logic
independently.
Tight coupling between domain logic
and usage of the ORM.
Difficult to replace ORM if needed.

56 / 84
Example: Own your interfaces
namespace Job\Domain\Repository;
use Job\Domain\Job;
interface JobRepositoryInterface {
public function findById(int $id): ?Job;
public function save(Job $job): void;
}

57 / 84
Example: Own your interfaces
namespace Job\Infrastructure\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Job\Domain\Job;
final readonly class DoctrineJobRepository implements JobRepositoryInterface {
public function __construct(private EntityManagerInterface $entityManager) {}
public function findById(int $id): ?Job {
return $this->entityManager->getRepository(Job::class)->find($id);
}
public function save(Job $job): void {
$this->entityManager->persist($job);
$this->entityManager->flush();
}
}

58 / 84
Example: Own your interfaces
namespace Job\Domain\Service;
use Job\Domain\Repository\JobRepositoryInterface;
use Job\Domain\Job;
final readonly class DomainJobService {
public function __construct(private JobRepositoryInterface $ jobRepository) {}
public function getJobById(int $id): ?Job {
return $this->jobRepository->findById($id);
}
public function saveJob(Job $job): void {
$this->jobRepository->save($job);
}
}

59 / 8459 / 84
Own your Interfaces
Separate your domain logic from the
infrastructure logic.
Use custom interfaces for your domain
classes, e.g. repository / services.
Implement interfaces using frameworks
ORM-specific classes.
Improves flexibility and testability.

60 / 84
Validate in Framework only

61 / 84
Example: Validate in framework only
namespace Cms\InputFilter;
use Laminas\InputFilter\InputFilter;
use Laminas\InputFilter\InputFilterInterface;
final readonly class ArticleInputFilter extends InputFilter {
public function init() {
$this->add([
'name' => 'title',
'required' => true,
'filters' => [['name' => 'StringTrim']],
'validators' => [['name' => 'NotEmpty']]
]);
$this->add([
'name' => 'description',
'required' => true,
'filters' => [['name' => 'StringTrim']],
'validators' => [['name' => 'NotEmpty']]
]);
}
}

62 / 84
Example: Validate in framework only
namespace Cms\Service;
use App\InputFilter\ArticleInputFilter;
use App\Service\ArticleDomainService;
use Laminas\InputFilter\InputFilterInterface;
final readonly class ArticleApplicationService {
private $inputFilter;
private $domainService;
public function __construct(private ArticleInputFilter $inputFilter,
private ArticleDomainService $domainService) {}
public function createArticle(array $data) {
$this->inputFilter->setData($data);
if (!$this->inputFilter->isValid()) {
throw new \Exception('Invalid data');
}
$this->domainService->saveArticle( $this->inputFilter->getValues());
}
}

63 / 8463 / 84
Validate In Framework
Validation is tightly coupled to the
framework.
Hard to reuse validation logic outside
the framework.
When replacing the framework, all
rules must be migrated.
If application validation is not pro-
cessed, inconsistent entities occur.

64 / 84
Example: Validate in your domain
namespace App\Entity;
class Article {
private $title;
private $description;
public function __construct(string $title, string $description) {
$this->ensureTitleIsValid($title);
$this->ensureDescriptionIsValid($description);
$this->title = $title;
$this->description = $description;
}
private function ensureTitleIsValid(string $title): void {
if (empty($title)) {
throw new \InvalidArgumentException('Title cannot be empty');
}
}
private function ensureDescriptionIsValid(string $description): void {
if (empty($description)) {
throw new \InvalidArgumentException('Description cannot be empty');
}
}
}

65 / 8465 / 84
Validate in your domain
Add validation logic to the domain
entities as an addition to pure
application validation.
Ensure entities validate their own state.
The validation rules are now redundant,
but this increases both security and the
user experience.

66 / 84
Extend the Framework

67 / 84
Example: Extend the Framework
namespace Shop\Controller\Plugin;
use Laminas\Mvc\Controller\Plugin\AbstractPlugin;
class DiscountPlugin extends AbstractPlugin {
public function calculateDiscount($price) {
if ($price > 100) {
return $price * 0.90;
}
return $price;
}
}

68 / 84
Example: Extend the Framework
namespace Shop\Controller;
use Laminas\Mvc\Controller\AbstractActionController;
class ProductController extends AbstractActionController {
public function showAction() {
$price = 150;
$discountedPrice = $this->discountPlugin()->calculateDiscount($price);
return ['price' => $discountedPrice];
}
}

69 / 8469 / 84
Extend the framework
Business logic is tightly coupled
with framework code.
Hard to reuse business logic
outside the framework.
Difficult to test business logic
independently.
Reduces code maintainability.

70 / 84
Example: Isolate Business Logic
namespace Shop\Service;
final readonly class DiscountCalculator {
public function calculate($price) {
if ($price > 100) {
return $price * 0.90;
}
return $price;
}
}

71 / 84
Example: Isolate Business Logic
namespace Shop\Controller;
use Laminas\Mvc\Controller\AbstractActionController;
use Shop\Service\DiscountCalculator;
final readonly class ProductController extends AbstractActionController {
public function __construct(private DiscountCalculator $discountCalculator) {}
public function showAction() {
$price = 150;
$discountedPrice = $this->discountCalculator->calculate($price);
return ['price' => $discountedPrice];
}
}

72 / 84
Example: Isolate Business Logic
namespace Shop\Domain;
class Product {
private $price;
public function __construct($price) {
$this->price = $price;
}
public function getDiscount() {
if ($this->price > 100) {
return $this->price * 0.90;
}
return $this->price;
}
public function getPrice() {
return $this->price;
}
}

73 / 8473 / 84
Isolate Business Logic
Move business logic to separate
service classes.
Inject services into controllers for
better separation.
Further improve by placing logic in
domain entities.
Increases flexibility, reusability, and
testability.

74 / 84
Forced Release cycles

75 / 84
Example: Laravel
https://packagist.org/packages/laravel/framework/stats Laravel 4 released 2013

76 / 84
Example: Symfony HTTP Foundation
https://packagist.org/packages/symfony/http-foundation/stats Symfony 2 released 2011

77 / 8477 / 84
Forced Release Cycles
Forces updates to remain secure
and feature-rich.
Requires resources to be up to date.
Leaves projects behind that
can't keep up on time.
Gradually increases technical debt
and maintenance costs.
Good especially for agencies,
freelancer and framework
maintainers.
??????

78 / 84
Example: Robust Testing Practices
class ApplicationOrderServiceTest extends TestCase {
public function testCreateOrder(): void {
$productId = 1;
$quantity = 2;
$order = $this-generateOrder();
$this->domainOrderServiceMock
->expects($this->once())
->method('createOrder')
->with($productId, $quantity)
->willReturn($order);
Event::fake();
Notification::fake();
[...]
}
}

79 / 84
Example: Robust Testing Practices
class ApplicationOrderServiceTest extends TestCase {
public function testCreateOrder(): void {
[...]
$result = $this->applicationOrderService->createOrder($productId, $quantity);
$this->assertSame($order, $result);
Event::assertDispatched('order.created', function ($event, $payload) use ($order) {
return $payload === $order;
});
Notification::assertSentTo(
[$order->user],
OrderShipped::class,
function ($notification, $channels, $notifiable) use ($order) {
return $notifiable->email === $order->user->email;
}
);
}
}

80 / 8480 / 84
Robust Testing Practices
Ensure code reliability through testing.
Use unit tests for isolated components.
Use integration tests for component
interactions.
Use e2e tests for system verification.
Prefer test-driven development.

81 / 84
Outro & Q&A

82 / 84
7 Strategies to Reduce Dependency7 Strategies to Reduce Dependency
Embrace modularity and decouplingEmbrace modularity and decoupling
Keep controllers and handlers slimKeep controllers and handlers slim
Split application and domain servicesSplit application and domain services
Own your interfacesOwn your interfaces
Add validation to your domainAdd validation to your domain
Separate your business logic from framework codeSeparate your business logic from framework code
Implement robust testing practicesImplement robust testing practices

83 / 84
Any Questions?Any Questions?

84 / 84
ThanksThanks
[email protected]@travello.de
www.travello.de www.travello.de
Our mission:Our mission:
We modernize legacy projects.We modernize legacy projects.