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)

Tivoli (RM)

source: www.romaest.org

Tivoli (RM)

Tivoli (RM)

Tivoli (RM)

Me everyday:

What is a framework?

An essential supporting structure of a building, vehicle, or object.

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

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.

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

 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

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

Email

- 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!

Frameworkless PHP with DDD

By Damiano Petrungaro

Frameworkless PHP with DDD

  • 1,703