php - DRY and Typing Specialization - TagMerge
4DRY and Typing SpecializationDRY and Typing Specialization

DRY and Typing Specialization

Asked 1 years ago
0
4 answers

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());
    }
}

Source: link

0

@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!

Source: link

0

Let’s look at some code snippets to understand why:
<?php

interface Product
{
    public function displayPrice();
}

class PlasticDuck implements Product
{
    /** @var int */
    private $price;

    public function __construct(int $price)
    {
        $this->price = $price;
    }

    public function displayPrice()
    {
        echo sprintf("The price of this plastic duck is %d euros!", $this->price);
    }
}

$plasticDuck = new PlasticDuck(2);
$plasticDuck->displayPrice();
But you attacked Dave’s expertise: he’s angry. He wants to win the argument. Soon enough, he finds another piece of code you’ve written; he comes back to your desk, slapping it in your face:
<?php

class CsvValidation
{
    public function validateProduct(array $product)
    {
        if (!isset($product['color'])) {
            throw new \Exception('Import fail: the product attribute color is missing');
        }

        if (!isset($product['size'])) {
            throw new \Exception('Import fail: the product attribute size is missing');
        }

        if (!isset($product['type'])) {
            throw new \Exception('Import fail: the product attribute type is missing');
        }
    }
}
Suddenly, your fingers type on your keyboard, at the speed of light, the following code:
<?php

class CsvValidation
{
    private $productAttributes = [
        'color',
        'size',
        'type',
    ];

    public function validateProduct(array $product)
    {
        foreach ($this->productAttributes as $attribute) {
            if (!isset($product[$attribute])) {
                throw new \Exception(sprintf('Import fail: the product attribute %s is missing', $attribute));
            }
        }
    }
}
I’ve written above that repetition of knowledge is always a violation of the DRY principle. This only apply when the same knowledge is repeated. Let’s take an example:
<?php


/** Shipment from the warehouse to the customer */
class Shipment
{
     public $deliveryTime = 4; //in days

     public function calculateDeliveryDay(): DateTime
     {
         return new \DateTime("now +{$this->deliveryTime} day");
     }
}

/** Order return of a customer */
class OrderReturn
{
    public $returnLimit = 4; //in days

    public function calculateLastReturnDay(): DateTime
    {
         return new \DateTime("now +{$this->returnLimit} day");
    }
}

Source: link

0

Here’s what it means in practice:
require 'dry-validation' require 'dry-types' module Types include Dry::Types.module Age = Strict::Int.constrained(gt: 18) end UserSchema = Dry::Validation.Form do key(:name).required key(:age).required(Types::Age) end UserSchema.(name: 'Jane', age: 17).messages # { age: ["must be greater than 18"] }
Simple example:
require 'dry-validation' require 'dry-types' module Types include Dry::Types.module module DataImport StrippedString = Strict::String.constructor { |value| value.to_s.strip } end end UserImport = Dry::Validation.Schema do configure { config.input_processor = :sanitizer } key(:name).required(Types::DataImport::StrippedString) key(:email).required(Types::DataImport::StrippedString) end UserSchema.(name: ' Jane ', email: 'jane@doe.org ').to_h # { name: "Jane", email: "jane@doe.org" }
dry-validation infers both validation rules and value constructors from your types, notice that Types::DataImport::StrippedString is a strict string, which means a proper validation rule is set too:
UserSchema.(name: ' Jane ', email: nil).messages # { email: ["must be filled", "must be String"] }
When defining a high-level rule, you can now delegate validation to an external schema. Why would you want to do that? Let’s say you have a nested data structure, and parts of that structure should be validated using different schemas based on other values from that data structure. In example:
CreateCommandSchema = Dry::Validation.Schema do key(:name).required key(:email).required end UpdateCommandSchema = Dry::Validation.Schema do key(:id).required(:int?) key(:data).schema(CreateCommandSchema) end CommandSchema = Dry::Validation.Schema do key(:command).maybe(:str?, :inclusion?: %w(Create Update)) key(:args).maybe(:hash?) rule(create_command: [:command, :args]) do |command, args| command.eql?('Create').then(args.schema(CreateCommandSchema)) end rule(update_command: [:command, :args]) do |command, args| command.eql?('Update').then(args.schema(UpdateCommandSchema)) end end CommandSchema.(command: 'Oops').messages.inspect # { command: ["must be one of: Create, Update"], args: ["is missing"] } CommandSchema.( command: 'Create', args: { name: 'Jane', email: nil } ).messages # { args: { email: ["must be filled"] } } CommandSchema.( command: 'Update', args: { id: 1, data: { name: nil, email: 'jane@doe.org' } } ).messages # { data: { name: ["must be filled"] } }
You can now specify that a value must be a number, in case of Form schemas a proper coercion is applied:
UserSchema = Dry::Validation.Form do key(:age).required(:number?, :int?) end UserSchema.(age: '1').to_h # { age: 1 } UserSchema.(age: 'one').messages # { age: ["must be a number"] } UserSchema.(age: '1.5').messages # { age: ["must be an integer"] }

Source: link

Recent Questions on php

    Programming Languages