Do PHP Frameworks still make Sense?
Let’s go Frameworkless to focus on the Domain!
Who am I?
Damiano Petrungaro
Italy
source: www.vidiani.com
Tivoli (RM)
source: www.livitaly.com
Tivoli (RM)
source: www.romaest.org
Tivoli (RM)
source: www.tivolitouring.com
Tivoli (RM)
source: www.confinelive.it
Tivoli (RM)
source: www.greenparkmadama.it
Me everyday:
What is a framework?
An essential supporting structure of a building, vehicle, or object.
source: en.oxforddictionaries.com
Laravel
Symfony
Slim
CakePHP
Yii
Codeigniter
Zend -> Expressive
Importance of the frameworks
-
Push for innovation
-
Don't reinvent the wheel
-
Define new standards
-
Easier for new joiners
Using a framework has a cost.
And it costs a lot.
Legacy Monolith
today: big ball of mud
yesterday: wow it's amazing!
It's all about dependency
The state of relying on or being controlled by someone or something else.
Framework = Dependency
source: en.oxforddictionaries.com
Frameworkless Movement
The Frameworkless Movement is a group of developers interested in developing applications without frameworks. We don't hate frameworks, nor we will ever create campaigns against frameworks, but we perceive the misuse of frameworks as a lack of knowledge regarding technical debt and we acknowledge the availability of alternatives to frameworks, namely using dedicated libraries, standard libraries, programming languages and operating systems.
source: frameworkless-manifesto
Dependencies are bad
Applying a methodology as
Domain Driven Design
How to avoid them:
The domain is not a dependency!
DDD to the rescue
It enforces us to model the domain first.
Then adapts the framework to it.
Strategic Design
Tactical Design
Strategic Design
Ubiquitous language
Bounded contexts
- Partnership
- Shared Kernel
- Customer-Supplier Development
- Conformist
...
Tactical Patterns
Value Object
Entity
Aggregate
Repository
Domain Event
Domain Service
Strategic Design
Ubiquitous language
source: Domain Driven Design
By using the model-based language pervasively and not being satisfied until it flows, we approach a model that is complete and comprehensible, made up of simple elements that combine to express complex ideas.
...
Communication is important
Bounded contexts
source: Implementing DDD
Why context matters?
Office
Shower
Be naked:
Nude Beach
Not-Nude Beach
Context may looks similar but...
Be naked in:
The context is important in code
- Partnership
- Shared Kernel
- Customer-Supplier Development
- Conformist
- Anticorruption layer
- Open Host Service
- Published Language
- Separate ways
- Big ball of mud
Tactical Design
Entities & Aggregate(Root)
It's a mutable object with an identity, it can be represented in different ways.
It's comparable to other entities by its ID.
Laravel way
<?php
namespace App;
// ...
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
Symfony way
namespace App\Entity;
// ...
/**
* @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
*/
class Product
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="integer")
*/
private $price;
public function getId()
{
return $this->id;
}
// ... getter and setter methods
}
A model
<?php
declare(strict_types=1);
namespace Acme\Article;
// ...
final class Article
{
/**
* @var ArticleID
*/
private $articleID;
/**
* @var AuthorID
*/
private $authorID;
/**
* @var AuthorDetails
*/
private $authorDetails;
/**
* @var Title
*/
private $title;
/**
* @var Body
*/
private $body;
/**
* @var DateTimeImmutable|null
*/
private $publishDate;
/**
* @var DateTimeImmutable
*/
private $creationDate;
/**
* @var DateTimeImmutable|null
*/
private $lastUpdateDate;
private function __construct(
ArticleID $articleID,
Title $title,
Body $body,
AuthorID $authorID,
AuthorDetails $authorDetails,
DateTimeImmutable $creationDate,
?DateTimeImmutable $publishDate,
?DateTimeImmutable $lastUpdateDate
) {
$this->articleID = $articleID;
$this->title = $title;
$this->body = $body;
$this->authorID = $authorID;
$this->authorDetails = $authorDetails;
$this->publishDate = $publishDate;
$this->creationDate = $creationDate;
$this->lastUpdateDate = $lastUpdateDate;
}
public static function create(
ArticleID $articleID,
Title $title,
Body $body,
AuthorID $authorID,
AuthorDetails $authorDetails,
DateTimeImmutable $creationDate
): self {
return new self($articleID, $title, $body, $authorID, $authorDetails, $creationDate);
}
public function id(): ArticleID
{
return $this->articleID;
}
public function title(): Title
{
return $this->title;
}
public function body(): Body
{
return $this->body;
}
public function authorID(): AuthorID
{
return $this->authorID;
}
public function authorDetails(): AuthorDetails
{
return $this->authorDetails;
}
public function publishDate(): ?DateTimeImmutable
{
return $this->publishDate;
}
public function creationDate(): DateTimeImmutable
{
return $this->creationDate;
}
public function lastUpdateDate(): ?DateTimeImmutable
{
return $this->lastUpdateDate;
}
}
Value Object
It's an immutable object representing a value in a domain.
It's comparable to other value objects by its own values.
A value object
<?php
declare(strict_types=1);
namespace Acme\Article\ValueObject;
// ...
final class Title
{
public const MIN_LENGTH = 5;
public const MAX_LENGTH = 100;
/**
* @var string
*/
private $value;
/**
* @throws InvalidTitle
*/
public function __construct(string $value)
{
$value = \trim($value);
$length = \mb_strlen($value);
if ($length < self::MIN_LENGTH) {
throw new TitleTooShort($value);
}
if ($length >= self::MAX_LENGTH) {
throw new TitleTooLong($value);
}
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
public function equals(self $title): bool
{
return $this->value === (string)$title;
}
private function __clone()
{
}
}
Repository
It's an abstraction layer to provide access to all the entities and the value objects related to an aggregate.
Laravel way
<?php
namespace App\Http\Controllers;
// ...
class UserController extends Controller
{
/**
* Show the profile for the given user.
*
* @param int $id
* @return View
*/
public function show($id)
{
return view('user.profile', ['user' => User::findOrFail($id)]);
}
/**
* Update the specified resource in storage.
*
* @param int $id
* @return Response
*/
public function update($id)
{
$user = User::findOrFail($id);
// Validation rules ...
$user->update(Request::all());
return view('user.profile', compact('user'));
}
}
Symfony way
<?php
namespace App\Controller;
// ...
class ProductController extends AbstractController
{
/**
* @Route("/product", name="product")
*/
public function index()
{
// you can fetch the EntityManager via $this->getDoctrine()
// or you can add an argument to your action: index(EntityManagerInterface $entityManager)
$entityManager = $this->getDoctrine()->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Using a repository
<?php
declare(strict_types=1);
namespace Acme\Article\UseCase\GetArticle;
// ...
final class GetArticleHandler
{
/**
* @var ArticleRepository
*/
private $articleRepository;
public function __construct(ArticleRepository $articleRepository)
{
$this->articleRepository = $articleRepository;
}
/**
* @throws ArticleDoesNotExist
* @throws ImpossibleToRetrieveArticle
*/
public function __invoke(GetArticleCommand $command): Article
{
return $this->articleRepository->getById($command->articleID());
}
}
Repository implementation
<?php
declare(strict_types=1);
namespace App\Integration\Article\Repository;
// ...
final class ArticleQueryBuilderRepository implements ArticleRepository
{
// ...
public function __construct(
DatabaseManager $database,
ArticleMapper $mapper,
Instrumentation $instrumentation
) {
$this->database = $database;
$this->mapper = $mapper;
$this->instrumentation = $instrumentation;
}
public function getById(ArticleID $articleID): Article
{
try {
$article = $this->database
->table(self::TABLE_NAME)
->select()->where('id', '=', (string) $articleID)
->first();
} catch (QueryException $e) {
$this->instrumentation->articleNotRetrieved($e, $articleID);
throw new ImpossibleToRetrieveArticles($e);
}
if (null === $article) {
$this->instrumentation->articleNotFound($articleID);
throw new ArticleDoesNotExist($articleID);
}
$this->instrumentation->articleFound($articleID);
return ($this->mapper)($article->toArray());
}
}
Let's use a framework now
It's like you are using a library
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
// ...
final class GetArticleController extends Controller
{
// ...
public function __construct(GetArticleHandler $handler, Mapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}
public function __invoke(GetArticleRequest $request):Response
{
// GetArticleRequest already handle HTTP validation using ValueObject behind the scene.
// So if the `id` is invalid won't het there.
$id = $request->route()->parameter('id');
$command = new GetArticleCommand(ArticleID::fromUUID($id));
try {
$article = ($this->handler)($command);
} catch(ArticleDoesNotExist $e){
// ...
}catch(ImpossibleToRetrieveArticle $e){
// ...
} //...
$response = ($this->mapper)($article);
return response()->json($response);
}
}
HTTP
<?php
namespace App\Console\Commands;
// ...
final class GetArticleCLI extends Command
{
// ...
public function __construct(GetArticleHandler $handler, Mapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
parent::__construct();
}
public function handle(): void
{
$id = $this->argument('id');
try {
$command = new GetArticleCommand(ArticleID::fromUUID($id));
} catch (InvalidID $e) {
// ...
}
try {
$article = ($this->handler)($command);
} catch(ArticleDoesNotExist $e){
// ...
}catch(ImpossibleToRetrieveArticle $e){
// ...
} //...
$this->output->text(($this->fromArticleMapper)($article));
}
}
CLI
Diagram of the architecture
Value Object
Aggregate
Command
Aggregate
Command
Aggregate
Req
Res
Input
Output
Repo
Use
Case
HTTP
CLI
DB
List-Array
Read
Diagram of the architecture
Aggregate
Command
Command
Req
Res
Input
Output
Repo
Use
Case
HTTP
CLI
DB
List-Array
Write
What happens when the fw is old?
Value Object
Aggregate
Command
Aggregate
Command
Aggregate
Req
Res
Input
Output
Repo
Use
Case
HTTP
CLI
DB
List-Array
Controller or CLI
ORM
Recap
-
Applying DDD will give us superpower
-
CQRS
-
Event Sourcing
-
Service Oriented Architecture
-
-
Update your framework/libraries more easily
-
New joiners learn the domain more easily
-
Less breaking changes across the system
-
No more monolithic legacy codebase
-
It's impossible I know, but please, let me dream
-
Thank you all!
Feedbacks: http://bit.ly/ipc-dam
LinkedIn: /damianopetrungaro
Slides: http://bit.ly/php-fw-ddd
Frameworkless PHP with DDD
By Damiano Petrungaro
Frameworkless PHP with DDD
- 1,725