$30 off During Our Annual Pro Sale. View Details »

Laravel Design Patterns 2.0

Laravel Design Patterns 2.0

Design patterns are commonly defined as solutions to recurring design problems. Frameworks like Laravel use Design Patterns throughout the codebase to keep structure and maintainability. Last year I talked about the factory, manager, strategy and provider design pattern. The patterns are all used within the framework. In this talk we will explore another set of Design Patterns that are used in Laravel. Once we have a basis of the design patterns we go into some practical examples.

Bobby Bouwmann

July 24, 2019
Tweet

More Decks by Bobby Bouwmann

Other Decks in Programming

Transcript

  1. Laravel Design Pa.erns 2.0

    View Slide

  2. @bobbybouwmann

    View Slide

  3. View Slide

  4. View Slide

  5. Let's talk design pa/erns

    View Slide

  6. What are design pa.erns?
    • Common occurring problems
    • Building blocks
    • Generic reusable solu6on for a problem

    View Slide

  7. Laravel Design Pa.erns
    • Factory Pa+ern
    • Builder (Manager) Pa+ern
    • Strategy Pa+ern
    • Provider Pa+ern
    • Repository Pa+ern
    • Iterator Pa+ern
    • Singleton Pa+ern
    • Presenter Pa+ern
    • Bridge Pa+ern

    View Slide

  8. Laravel Design pa.erns 1.0
    • Factory Pa+ern
    • Builder (Manager) Pa+ern
    • Strategy Pa+ern
    • Provider Pa+ern

    View Slide

  9. Laravel Design pa.erns 2.0
    • Singleton Pa-ern
    • Observer Pa-ern
    • Bridge Pa-ern

    View Slide

  10. Singleton Pa,ern
    Crea%onal Pa+ern

    View Slide

  11. View Slide

  12. View Slide

  13. Singleton Pa,ern
    Singleton is a crea0onal design pa,ern that lets you
    ensure that a class has only one instance, while
    providing a global access point to this instance.

    View Slide

  14. But Why Singleton Pa1ern?
    • Save system resources
    • Share data

    View Slide

  15. class PizzaOven
    {
    private static $instance = null;
    private $oven;
    private function __construct() {
    $this->oven = new Oven();
    }
    public static function getInstance() {
    if(!self::$instance) {
    self::$instance = new PizzaOven();
    }
    return self::$instance;
    }
    public function getOven() {
    return $this->oven;
    }
    }

    View Slide

  16. $pizzaOvenOne = PizzaOven::getInstance();
    $pizzaOvenTwo = PizzaOven::getInstance();
    $this->assertSame(
    $pizzaOvenOne,
    $pizzaOvenTwo
    ); // true
    $this->assertSame(
    $pizzaOvenOne->getOven(),
    $pizzaOvenTwo->getOven()
    ); // true

    View Slide

  17. // Illuminate\Database\DatabaseServiceProvider.php
    public function register()
    {
    // The connection factory is used to create the actual connection instances on
    // the database. We will inject the factory into the manager so that it may
    // make the connections while they are actually needed and not of before.
    $this->app->singleton('db.factory', function ($app) {
    return new ConnectionFactory($app);
    });
    // The database manager is used to resolve various connections, since multiple
    // connections might be managed. It also implements the connection resolver
    // interface which may be used by other components requiring connections.
    $this->app->singleton('db', function ($app) {
    return new DatabaseManager($app, $app['db.factory']);
    });
    }

    View Slide

  18. // Illuminate\Database\DatabaseManager.php
    public function __construct($app, ConnectionFactory $factory)
    {
    $this->app = $app;
    $this->factory = $factory;
    $this->reconnector = function ($connection) {
    $this->reconnect($connection->getName());
    };
    }

    View Slide

  19. // Illuminate\Container\Container.php
    public function singleton($abstract, $concrete = null): void
    {
    $this->bind($abstract, $concrete, true);
    }
    public function bind($abstract, $concrete = null, $shared = false): void
    {
    if (! $concrete instanceof Closure) {
    $concrete = $this->getClosure($abstract, $concrete);
    }
    // IMPORTANT LINE HERE
    $this->bindings[$abstract] = compact('concrete', 'shared');
    if ($this->resolved($abstract)) {
    $this->rebound($abstract);
    }
    }

    View Slide

  20. protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
    $abstract = $this->getAlias($abstract);
    // if this is an instance managed as singleton we return it right away
    if (isset($this->instances[$abstract])) {
    return $this->instances[$abstract];
    }
    $concrete = $this->getConcrete($abstract);
    // Instantiate an instance of the concrete type registered for the binding
    $object = $this->isBuildable($concrete, $abstract)
    ? $this->build($concrete)
    : $this->make($concrete);
    // If the type is registered as a singleton we'll want to cache the instance in memory
    if ($this->isShared($abstract)) {
    $this->instances[$abstract] = $object;
    }
    return $object;
    }

    View Slide

  21. // Illuminate\Log\LogServiceProvider
    $this->app->singleton('log', function () {
    return new LogManager($this->app);
    });
    $this->assertSame(
    app()->make('log'),
    app()->make('log')
    ); // true
    // Some random controller
    public function index(ChannelManager $channelManager): void
    {
    $this->assertSame(
    $channelManager,
    app()->make(ChannelManager::class)
    ); // true
    }

    View Slide

  22. Singleton Pa,ern
    Singleton is a crea0onal design pa,ern that lets you
    ensure that a class has only one instance, while
    providing a global access point to this instance.

    View Slide

  23. Observer Pa*ern
    Behavioral Pa,ern

    View Slide

  24. View Slide

  25. View Slide

  26. Observer Pa*ern
    Define a one-to-many dependency between objects
    so that when one object changes state,
    all its dependents listeners (subscribers) are no4fied
    and updated automa>cally.

    View Slide

  27. • Observable:
    • The subject
    • The thing that can be observed
    • The thing that undergoes the changes
    • Keeps a collec6on of observers to no/fy
    • Observer:
    • The listener
    • The thing that can observe
    • The thing that is interested in the changes
    • Accepts the observable to handle the state change

    View Slide

  28. View Slide

  29. class StartProcessOrder implements Observable
    {
    /** @var Collection */
    private $observers;
    public function add(Observer $observer): void
    {
    $this->observers->push($observer);
    }
    public function notify(): void
    {
    foreach ($this->observers as $observer) {
    $observer->update($this);
    }
    }
    }

    View Slide

  30. class SendDeliveryTime implements Observer
    {
    public function update(Observerable $observable): void
    {
    $this->smsService->send($observable->getOrder());
    }
    }
    class NotifyBaker implements Observer
    {
    public function update(Observerable $observable): void
    {
    $this->ticketPrinter->print($observable->getOrder());
    }
    }

    View Slide

  31. class SendDeliveryTime implements Observer
    {
    public function update(StartProcessOrder $startProcessOrder): void
    {
    echo 'Send SMS with delivery time';
    $this->smsService->send($startProcessOrder->getDeliveryTime());
    }
    }
    class NotifyBaker implements Observer
    {
    public function update(StartProcessOrder $startProcessOrder): void
    {
    echo 'Start printing order for baker';
    $this->ticketPrinter->print($startProcessOrder->getOrder());
    }
    }

    View Slide

  32. // Create an observable
    $orderProces = new StartProcessOrder();
    // Add observer to the observable
    $orderProces->add(new SendDeliveryTime());
    $orderProces->add(new NotifyBaker());
    // Notify all observers
    $orderProces->notify();
    // Output: Send SMS with delivery time
    // Output: Start printing order for baker

    View Slide

  33. // App\Providers\EventServiceProvider.php
    use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
    class EventServiceProvider extends ServiceProvider
    {
    protected $listen = [
    Registered::class => [
    SendEmailVerificationNotification::class,
    ],
    ];
    }

    View Slide

  34. // Illuminate\Foundation\Support\Providers\EventServiceProvider.php
    class EventServiceProvider extends ServiceProvider
    {
    protected $listen = [];
    public function boot(): void
    {
    $events = $this->listens();
    foreach ($events as $event => $listeners) {
    foreach (array_unique($listeners) as $listener) {
    Event::listen($event, $listener);
    }
    }
    }
    public function listens(): array
    {
    return $this->listen;
    }
    }

    View Slide

  35. // Illuminate\Events\Dispatcher
    public function listen($events, $listener)
    {
    foreach ((array) $events as $event) {
    if (Str::contains($event, '*')) {
    $this->setupWildcardListen($event, $listener);
    } else {
    $this->listeners[$event][] = $this->makeListener($listener);
    }
    }
    }

    View Slide

  36. // Illuminate\Events\Dispatcher
    public function listen($events, $listener)
    {
    foreach ((array) $events as $event) {
    if (Str::contains($event, '*')) {
    $this->setupWildcardListen($event, $listener);
    } else {
    $this->listeners[$event][] = $this->makeListener($listener);
    }
    }
    }

    View Slide

  37. // Illuminate\Foundation\helpers.php
    function event(...$args)
    {
    return app('events')->dispatch(...$args);
    }
    // Fire of the event somewhere in your code
    // e.g controllers, jobs, etc.
    event(new Registered($user));

    View Slide

  38. event(new Registered($user));
    // Illuminate\Events\Dispatcher.php
    public function dispatch($event, $payload = [])
    {
    // When the given "event" is actually an object we will assume it is an event
    // object and use the class as the event name and this event itself as the
    // payload to the handler, which makes object based events quite simple.
    [$event, $payload] = $this->parseEventAndPayload(
    $event, $payload
    );
    foreach ($this->getListeners($event) as $listener) {
    $listener($event, $payload);
    }
    }

    View Slide

  39. Observer Pa*ern
    Define a one-to-many dependency between objects
    so that when one object changes state,
    all its dependents listeners (subscribers) are no4fied
    and updated automa>cally.

    View Slide

  40. Bridge Pa*ern
    Structural Pa*ern

    View Slide

  41. View Slide

  42. View Slide

  43. View Slide

  44. Bridge Pa*ern
    Decouple an abstrac-on from its implementa-on so
    that the two can vary independently.

    View Slide

  45. View Slide

  46. View Slide

  47. class Product
    {
    private $promotion;
    private $price;
    private $name;
    public function __construct(Promotion $promotion, float $price, string $name)
    {
    $this->promotion = $promotion;
    $this->price = $price;
    $this->name = $name;
    }
    public function getPrice(): float
    {
    return $this->promotion->applyDiscount($this->price);
    }
    }

    View Slide

  48. class Pizza extends Product
    {
    public function getPricePerPiece(): float
    {
    return round($this->getPrice() / 8, 2);
    }
    }
    class ChickenWings extends Product
    {
    public function getPriceWithSauce(): float
    {
    return round($this->getPrice() + 0.65, 2);
    }
    public function getPriceWithoutSauce(): float
    {
    return round($this->getPrice(), 2);
    }
    }

    View Slide

  49. abstract class Promotion
    {
    public function applyDiscount(float $price): float
    }
    class BlackFriday extends Promotion
    {
    public function applyDiscount(float $price): float
    {
    // Apply 40% discount + make sure price ends with .99
    return ceil($price * 0.6) - 0.01;
    }
    }
    class ValentinesDay extends Promotion
    {
    public function applyDiscount(float $price): float
    {
    return $price - 0.25;
    }
    }

    View Slide

  50. // Using product as a base
    $product = new Product(new BlackFriday(), 6.00, 'Pizza');
    $product->getPrice(); // 3.99
    $product = new Product(new ValentinesDay(), 6.00, 'Pizza');
    $product->getPrice(); // 5.75
    // Using individual products (abstractions)
    $product = new ChickenWings(new BlackFriday(), 12.50, 'Wings');
    $product->getPriceWithSauce(); // 8.64
    $product->getPriceWithoutSauce(); // 7.99
    $product = new ChickenWings(new ValentinesDay(), 12.50, 'Wings');
    $product->getPriceWithSauce(); // 12.90
    $product->getPriceWithoutSauce(); // 12.25

    View Slide

  51. class CreateUsersTable extends Migration
    {
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
    });
    }
    public function down()
    {
    Schema::dropIfExists('users');
    }
    }

    View Slide

  52. View Slide

  53. // database/migrations/2014_10_12_000000_create_users_table.php
    Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    });
    // Illuminate\Database\Schema\Builder.php
    public function __construct(Connection $connection)
    {
    $this->connection = $connection;
    $this->grammar = $connection->getSchemaGrammar();
    }

    View Slide

  54. // Illuminate\Database\Schema\Builder
    public function dropAllTables()
    {
    throw new LogicException('This database driver does not support dropping all tables.');
    }
    // Illuminate\Database\Schame\MySqlBuilder
    public function dropAllTables()
    {
    $tables = $this->getAllTables();
    $this->disableForeignKeyConstraints();
    $this->connection->statement(
    $this->grammar->compileDropAllTables($tables)
    );
    $this->enableForeignKeyConstraints();
    }

    View Slide

  55. // Illuminate\Database\Schema\SQLiteBuilder
    public function dropAllTables()
    {
    if ($this->connection->getDatabaseName() !== ':memory:') {
    return $this->refreshDatabaseFile();
    }
    $this->connection->select($this->grammar->compileEnableWriteableSchema());
    $this->connection->select($this->grammar->compileDropAllTables());
    $this->connection->select($this->grammar->compileDisableWriteableSchema());
    $this->connection->select($this->grammar->compileRebuild());
    }

    View Slide

  56. // Illuminate\Database\Schema\Grammars\MySqlGrammar
    public function compileDropAllTables($tables)
    {
    return 'drop table '.implode(',', $this->wrapArray($tables));
    }
    // Illuminate\Database\Schema\Grammars\SQLiteGrammar
    public function compileDropAllTables()
    {
    return "delete from sqlite_master where type in ('table', 'index', 'trigger')";
    }
    // Illuminate\Database\Schema\Grammars\PostgresGrammar
    public function compileDropAllTables($tables)
    {
    return 'drop table "'.implode('","', $tables).'" cascade';
    }

    View Slide

  57. Bridge Pa*ern
    Decouple an abstrac-on from its implementa-on so
    that the two can vary independently.

    View Slide

  58. Looks like Adapter Pa0ern right?
    • Bridge pa+ern is designed up-front to let the abstrac7on and the
    implementa7on vary independently
    • Adapter makes things work a>er they're designed, make
    unrelated classes work together

    View Slide

  59. Wrapping up

    View Slide

  60. Singleton Pa,ern

    View Slide

  61. Observer Pa*ern

    View Slide

  62. Bridge Pa*ern

    View Slide

  63. The silver bullet

    View Slide

  64. View Slide

  65. THANK YOU

    View Slide

  66. THANK YOU

    View Slide