7

5

9

Laravel loves Enterprise

Can you find the solution?

PREMISE

Laravel is an interesting tool, but it works well only for prototypes or small projects.
Is that true?
For the big one, just use Symfony or Laminas (aka Zend)

PREMISE

My answer is NO
But discipline and knowledge are necessary, otherwise you will have a mess pretty soon
Laravel can be a very interesting option and it is mature enough in order to achieve a long living and complex project
In this talk we will discuss about some strategies and design useful to avoid some common mistakes

Christian Nastasi

IT Training Manager @Facile.it
20+ years developing stuff 
Nerd T-shirt lover

WHO AM I?

- Worked on a legacy project (10+ years old)
- Led and coached the team
- Saw what a legacy code can become
- Refactored part of that legacy codebase
- Built new stuff on that project

WHAT DID I DO IN THE PAST (ALMOST) 3 YEARS

We are not covering all the practices and solutions needed for working on a legacy project
The topic is pretty big, and in order to give some value, I focused the talk in small and limited set of problems.

WHAT'S NOT THE TOPIC OF THE TALK?

There are some bad patterns that are recurrent and need attention in order to avoid an increase in our technical debt.
We are going to simulate an evolving green field project.
I will give you some design hints to keep your architecture clean and maintainable.

WHAT'S THE TOPIC OF THE TALK?

This talk is based on my personal experience and studies
May contains traces of humour
The code snippets showed in this talk are intentionally simple and it omits sometimes parts for educational purpose

DISCLAIMER

CONTEXT

No formal definition found, but for me it has:
A very long life (years)
Contains domain specific business logics
Could be very structured
It is a live project: 
it continue to change and evolve
There is one or more teams working on it

WHAT'S AN ENTERPRISE PROJECT?

- Often it is partially refactored (Frankenstein architecture)
- Lack of time means shortcuts (Growth of wrong dependencies and technical debt)
- It grows in complexity within the time (cost of maintanance exponential high)
- Often, domain logics, framework and persistence are tightly bounded
- Change requests / evolutions to the legacy are potentially disruptive
- There is no or low test coverage

COMMON ISSUES

- Nobody knows 100% of the codebase
High technical debt
High maintenance cost
High evolution cost
Low confidence in changes

IN SHORT TERMS

BUT, AS THEY SAY, FACTS ARE BETTER THAN WORDS

THE USE CASE

Features requested:
- Add a book into the library
- Search for a book

We have to develop an application for a library.

FEATURE:

Add a book into the library

class AddBookController 
{
   public function __invoke (Request $request): JsonResponse 
   {
      $validatedBookData = $request->validate([
      	'isbn'    => 'required|...', // simplified
        'title'   => 'required|min:2',
        'authors' => 'required|array|...' // simplified,
        'publishedAt' => 'required|date|...' // simplified
      ])
   
      $book = Book::create($validatedBookData);
         
      return response()->json($book, 201)
   }
}

A SIMPLE IMPLEMENTATION

Get the input
& Validate

Persist

Response

CHANGE REQUEST:

Add a book into the library

 Using the command line

Duplicated code

EASY, RIGHT?

SOLUTION

Don't repeat yourself

final class AddBook
{
    public function __invoke (array $data): void 
    {
        $validator = Validator::make($data, [
            'isbn'    => 'required|...', // simplified
            'title'   => 'required|min:2',
            'authors' => 'required|array|...' // simplified,
            'publishedAt' => 'required|date|...' // simplified
        ]);
      
        if ($validator->fails()) {
            throw new InvalidBookException($validator->errors()->toArray());
        }
   
        $book = Book::create($validator->validated());
    }
}

JUST MOVE THE LOGIC OUTSIDE

RECAP

Don't repeat yourself

Try to centralize your business logic

Apply the single responsibility principle

FEATURE:

Search for a book

SEARCH FOR A BOOK

First implementation

I would like that our users are able to find a book using words contained in the content of the book
We tried, but with the amout of books in our database a full text search is too slow with the actual technology stack that we have.
Then change the stack!

CHANGE REQUEST

  • We have a change request / evolution that are potentially disruptive in the actual technology stack
  • The database used right now are not the best choice for that use case, we should change it, at least for the search

 

  • We are using an ORM that aren't compatible with the technology we should use. Changing it means to touch the code in a several places, with the risk of introducing side effects

 

  • We are too coupled with the framework

TYPICAL PROBLEMS

SOLUTION

Repository Pattern

interface BookRepository
{
    public function add (array $data): void; 
    
    public function findByIsbn(string $isbn): ?array;
    
    /** .... */
}
class EloquentBookRepository implements BookRepository
{
    public function add (array $data): void {
        /* Input Validation */
        
        Book::create($data);
    } 
    
    public function findByIsbn(string $isbn): ?array {
    	return Book::find($isbn)->toArray();
    }
    
    /** .... */
}

BOOK REPOSITORY

RECAP

Coupling business logic and  infrastructure is risky

Depend on abstractions, not on implementations

WHY SHOULD I REINVENT THE WHEEL?

Wasted DB Access

Possible inconsistent rule between logics

PROBLEM

​Wasted db access

Some validation rule requires check on database
We waste time and resources doing the same operations every time per session
Those rules are duplicated

PROBLEM

​Wasted db access

SOLUTION

Caching

final class CachedBookRepository implements BookRepository
{
    public function __construct(
        private BookRepository $bookRepository,
        private BookCache $bookCache
    ) {}
    
    public function add (array $data):void 
    {
        $this->bookRepository->add($data);
    } 
    
    public function findByIsbn(string $isbn):?array 
    {
        if (!$bookCache->has($isbn)) {
            $book = $this->bookRepository->findByIsbn($isbn);
            
            $bookCache->set($book);
        }
        
        return $bookCache->get($isbn);
    }
}

CACHED REPOSITORY

BOOK CACHE INTERFACE

interface BookCache
{
    public function has(string $isbn):bool;
    public function get(string $isbn):array;
    public function set(array $book):void;
}

CACHING IS NOT A SILVER BULLET

Ask to yourself:

How frequently the information changes?

How often I need that information?

RECAP

Try to optimize I/O access when possible

Hide those optimizations behind abstractions

PROBLEM

Inconsistency

We cannot trust the inputs, because we don't know from where the logic will be used in the future
We can't be sure that we are using the same validation rules everywhere
The validation rules are duplicated
Inconsistent data
Inconsistent behaviour

PROBLEM

Inconsistency

BUT BEFORE CONTINUING, LET'S TALK ABOUT PRIMITIVES, TYPES AND DATA MODELLING IN GENERAL

What's an age?
What's a phone number?
Integer
> 17
< 99
Only italian numbers
Prefix + 8 o 9 digits
Start with +
a number
a string
So I have to validate every time, everywhere, to be sure that the value is correct

PRIMITIVES: THE ROOT OF ALL EVILS

There's a bug.
Can you spot it?
class VeryUsefulClass 
{
   public function doSomething (int &$age): void {
      if ($age = 18) {
         // do your stuff
      }
      else if ($age > 18) {
         // do other stuff
      }
   }
}
There's nothing that can prevent this situation
Are we sure?
What if we can define our own types?

WATCH THIS

author
title
ISBN
publishing date

WHAT'S A BOOK?

13 digits

Have a specific format

"978-3-16148410-0"

"The Hitchhiker's Guide to the Galaxy"

At least 2 characters

[ "Douglas Adams" ]

At least one

Exists into the authors registry

At least 2 characters each

"September 27, 1995"

Valid date

Past the first book printed

Not too far into the future

WHAT'S A BOOK?

Self validating
Immutable
Describes values / concepts of your domain
Protect you from the past yourself
Comparable
If you have an instance of it, then you are pretty sure that your data is correct

SOLUTION

Value Objects

final class Isbn
{    
    private function __construct(public readonly string $value) 
    {
    	$this->assertIsThirteenCharacters($this->value);
        $this->assertHasAValidFormat($this->value);
    }
    
    private function assertIsThirteenCharacters (string $value):void 
    {
        strlen($value) == 13 or throw InvalidIsbn::tooShort($value);
    } 
    
    private function assertHasAValidFormat (string $value):void 
    { 
        // ISBN specific rules 
    }
    
    public function __toString ():string 
    { 
        return $this->value; 
    }
    
    public function equalsTo (Isbn $isbn):bool 
    {
        return $isbn->value === $this->value;
    }
    
    public static function fromString(string $isbn): Isbn 
    {
        return new Isbn($isbn);
    }
}

SOLUTION

Value Objects

final class Book
{    
    public function __construct (
       public readonly Isbn $isbn,
       public readonly Title $title,
       public readonly Authors $authors,
       public readonly PublishingDate $publishedAt
    ) {}
    
    public function equalsTo (Book $book):bool 
    {
        return $book->isbn->equalsTo($this->isbn);
    }
    
    public static function create (
        string $isbn, 
        string $title, 
        array $authors, 
        string $publishedAt
    ): Book {
        return new Book (
            Isbn::fromString($isbn),
            Title::fromString($title),
            Authors::fromArray($array),
            PublishingDate::fromString($publishedAt)
        );
    }
    
    public function toArray ():array 
    {
    	return [
           'isbn' => (string) $this->isbn,
           'title' => (string) $this->title,
           'authors' => $this->authors->toArray(),
           'publishAt' => (string) $this->publishedAt
        ];
    }
}

SOLUTION

Entities

WHAT'S NEXT?

Welcome into the DDD realm

Aggregates
Domain Events
Hierarchic Errors
Factories
Use cases / services
Bounded contexts
...

RECAP

Using a strong type system, prevent you from bugs and data inconsistency

interface BookRepository
{
    public function add (Book $book):void;
    
    public function findByIsbn (Isbn $isbn):?Book;
}

Repository

LET'S REFACTOR: ADD BOOK

final class AddBook
{
   public function __construct (
      private readonly BookRepository $bookRepository,
      private readonly EventDispatcher $dispatcher
   ) {}

   public function __invoke (Book $book): void 
   {
      $this->bookRepository->add ($book);
      
      $this->dispatcher->dispatch (new BookAdded ($book));
   }
}

Service

LET'S REFACTOR: ADD BOOK

class AddBookRequest
{
   public function rules():array
   {
      return [
         'isbn'        => 'required', 
         'title'       => 'required', 
         'authors'     => 'required', 
         'publishedAt' => 'required',
     ];
   }
   
   /* ... */
}

Request

LET'S REFACTOR: ADD BOOK

Controller

class AddBookController 
{
   public function __construct (private readonly AddBook $addBook) {}

   public function __invoke (AddBookRequest $request): JsonResponse 
   {
      $book = Book::create(...$request->validated()); // It's our entity
      
      ($this->addBook)($book);
         
      return response()->json($book->toArray(), 201)
   }
}

LET'S REFACTOR: ADD BOOK

final class GetBookByIsbn
{
   public function __construct (
      private readonly BookRepository $bookRepository
   ) {}

   public function __invoke (Isbn $isbn): Book|never
   {
      $book = $this->bookRepository->findByIsbn ($isbn);
      
      return $book ?? throw new BookNotFound ($isbn);
   }
}

Service

LET'S REFACTOR: GET BOOK

interface BookCache
{
    public function has (Isbn $isbn):bool;
    
    public function get (Isbn $isbn):?Book;
    
    public function set (Book $book):void;
}

Cache

LET'S REFACTOR: GET BOOK

class GetBookController 
{
   public function __construct (
      private readonly GetBookByIsbn $getBookByIsbn
   ) {}

   public function __invoke (string $isbnAsString): JsonResponse 
   {
      $isbn = Isbn::fromString ($isbnAsString);
      
      $book = ($this->getBookByIsbn)($isbn); // Throws BookNotFound
         
      return response()->json($book->toArray(), 200)
   }
}

Controller

LET'S REFACTOR: GET BOOK

THE BIG PICTURE

Don't put your business logic into the entry points

Inject the logic from outside instead

PUTTING ALL TOGETHER

Follow the rule: one logic, one place

Don't put the same logic in two places

PUTTING ALL TOGETHER

Don't let your business logic knows about frameworks, libraries or infrastructures

Use abstractions in your business logics and let the concrete class knows about the implementation details.

PUTTING ALL TOGETHER

Avoid repeating heavy I/O operations

Use caching policies when it make sense

PUTTING ALL TOGETHER

Build your types instead.

Every class is a type

Don't use primitives

PUTTING ALL TOGETHER

AND ALSO

SOLID

Fig Initiative: PSRs

Static code analysis: psalm, phpstan, code-sniffer, ...

Test coverage: unit, functional, integration, E2E

Layered Architectures: Hexagonal, Clean, Onion

Monorepo php with Composer

Event source & CQRS

...

THANKS

Questions?

WE ARE HIRING

CONTACTS

christian.nastasi@facile.it
https://github.com/cnastasi
https://github.com/yadddl
https://joind.in/talk/a863a