'DRY and Typing Specialization

while I was learning PHP, I thought of a (simple?) problem that I could not solve "properly". Here it is:

  • I would like to create multiple "specialized containers"
  • I would like to avoid duplicated code

For example:

<?php

interface BagInterface
{

    public function has(string $key) : bool;
    public function get(string $key, mixed $fallback) : mixed;
    public function set(string $key, mixed $value) : self;
    public function del(string $key) : void;
    public function all() : array;
    public function filter(callable $callback) : array;

}

abstract class AbstractBag implements BagInterface
{

    private array $bag;

    public function has(string $key) : bool
    {
        return array_key_exists($key, $this->bag);
    }

    public function get(string $key, mixed $fallback = null) : mixed
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

    public function set(string $key, mixed $value) : self
    {
        $this->bag[$key] = $value;

        return $this;
    }

    public function del(string $key) : void
    {
        unset($this->bag[$key]);
    }

    public function all() : array
    {
        return $this->bag;
    }

    public function filter(callable $callback) : array
    {
        return array_filter($this->bag, $callback, ARRAY_FILTER_USE_BOTH);
    }

}

So, I could then create "specialized" bag:

<?php

class CookieBag extends AbstractBag
{

    public function get(string $key, ?Cookie $fallback = null) : ?Cookie
    {
        return parent::get($key, $fallback);
    }

    public function set(string $key, Cookie $cookie) : self
    {
        return parent::set($key, $cookie);
    }

}

class CandyBag extends AbstractBag
{

    public function get(string $key, ?Candy $fallback = null) : ?Candy
    {
        return parent::get($key, $fallback);
    }

    public function set(string $key, Candy $candy) : self
    {
        return parent::set($key, $candy);
    }

}

I understood that it's not possible in PHP, as it is breaking the Liskov Substitution Principle.

For example:

<?php

class GrandMa
{
    public function giveCookie(BagInterface $bag)
    {
        // Will be fine, BagInterface said "mixed"
        // But break LSP, error if $bag is a not a CookieBag
        bag->set('abc', new Cookie());
    }
}

So, I read multiple post on the same "problem", and none of them provided a clear solution, few mentioned the Observer Pattern, but I do not really see how to apply it. Maybe I am too tired / blinded by the C++ template approach...

Does anyone have any advise, example, or better approach ?

Thanks !



Solution 1:[1]

@IMSoP Thanks for you answer! So now, I know there is no "template-like" way to achieve this in PHP. Your solution looks great to avoid code-duplication, but I do not like the idea of relying on "add-ons".

So, I endup with the following "solution", which seems to be more "safe" (at least for me).

<?php

interface BagInterface
{

    public function has(string $key) : bool;
    public function get(string $key, mixed $fallback) : mixed;
    public function set(string $key, mixed $value) : self;
    public function delete(string $key) : self;
    
    // ... Rest of Generic Functions Declarations 

}

class Bag implements BagInterface
{

    private array $items = [];

    public function has(string $key) : bool
    {
        return \array_key_exists($key, $this->items);
    }

    public function get(string $key, mixed $fallback = null) : mixed
    {
        return $this->has($key) ? $this->items[$key] : $fallback;
    }

    public function set(string $key, mixed $value) : self
    {
        $this->items[$key] = $value;

        return $this;
    }

    public function delete(string $key) : self
    {
        unset($this->items[$key]);

        return $this;
    }

    // ... Rest of Generic Functions Definitions
    
}

And then, for "specialization", I have used "Adapter Pattern ?" (I tried to add some logic to make it more "realistic").

<?php

interface CookieBagInterface
{

    public function has(string $name) : bool;
    public function get(string $name) : ?CookieInterface;
    // Changed set(...) to add(...), because I do not need $key
    public function add(CookieInterface $cookie) : self;
    public function delete(string $name) : self;

    // ... Rest of Specialized Functions Declarations

}

class CookieBag implements CookieBagInterface
{

    public function __construct(
        private BagInterface $bag,
    )
    {
        // Do nothing
    }

    public function has(string $name) : bool
    {
        return $this->bag->has($name);
    }

    public function get(string $name) : ?CookieInterface
    {
        return $this->bag->get($name, null);
    }

    public function add(CookieInterface $cookie) : self
    {
        $added = \setcookie($cookie->getName(), $cookie->getValue(), [
            'expires'  => $cookie->getExpires(),
            'path'     => $cookie->getPath(),
            'domain'   => $cookie->getDomain(),
            'secure'   => $cookie->isSecure(),
            'httponly' => $cookie->isHttpOnly(),
            'samesite' => $cookie->getSameSitePolicy(),
        ]);

        if ($added)
        {
            $this->bag->set($cookie->getName(), $cookie);
        }

        return $this;
    }

    public function delete(string $name) : self
    {
        $cookie = $this->get($name);

        if ($cookie === null)
        {
            return $this;
        }

        $removed = \setcookie($cookie->getName(), '', 1);

        if ($removed)
        {
            $this->bag->delete($name);
        }

        return $this;
    }

    // ... Rest of Specialized Functions Definitions

}

Benefits:

  • It works
  • I can modify methods (for example: Bag::set() / CookieBag::add(), do not need $key)
  • I can add "logic?" to methods (for example: CookieBag::add() and CookieBag::delete() use \setcookie())

Downsides:

  • I have to re-declare (interface) and re-define (class) each needed
  • I have to pass my "Generic" Bag to the constructor (or should I break Dependency Inversion Principle and add $bag = new Bag() to the constructor?)

Thanks!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 KuroBayashi