PHP Frameworks: I want to break free (IPC Berlin 2024)
eggertralf
48 views
84 slides
May 31, 2024
Slide 1 of 84
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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 ...
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 on these frameworks, based on real-world examples and practical considerations. The goal is to equip developers with the skills and knowledge to create more flexible and future-proof web applications. We'll explore the importance of maintaining autonomy in a rapidly changing tech landscape and how to make informed decisions in PHP development.
This talk is aimed at encouraging a more independent approach to using PHP frameworks, moving towards a more flexible and future-proof approach to PHP development.
Size: 1.75 MB
Language: en
Added: May 31, 2024
Slides: 84 pages
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!
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');
?>
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;
}
}
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(),
]);
}
}
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.
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.
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.
??????
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