Skip to content
Advertisement

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 !

Advertisement

Answer

Yes, specializing collections like this is often the example given for the usefulness of “generic” or “templated” types. Rather than extending the base class, you would specialise it with a type parameter, giving something like this:

class GenericBag<T>
{
    // ...

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

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

        return $this;
    }
    
    // ...
}

class GrandMa
{
    public function giveCookie(GenericBag<Cookie> $bag)
    {
        bag->set('abc', new Cookie());
    }
}

Unfortunately, those don’t exist in PHP, and are unlikely to any time soon because there are some fundamental problems with how they would fit into the existing language.

The best you can do in the meantime is to use some machine readable documentation which can be read by various static analysis tools and IDEs. Here for instance is Psalm’s documentation for it; a lot of other tools support the same syntax.

So the above example would be:

/** @template T */
class GenericBag
{
    // ...

    /**
     * @param string $key
     * @param T|null $fallback
     * @return T
     */
    public function get(string $key, $fallback = null)
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

   /**
     * @param string $key
     * @param T $value
     * @return GenericBag<T>
     */
    public function set(string $key, $value) : self
    {
        $this->bag[$key] = $value;

        return $this;
    }
    
    // ...
}

class GrandMa
{
    /**
      * @param GenericBag<Cookie> $bag
      */
    public function giveCookie(GenericBag $bag)
    {
        bag->set('abc', new Cookie());
    }
}
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement