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