frankvanhest/value-objects

Value Objects interfaces and abstracts to create value objects

v4.0.0 2024-01-26 10:38 UTC

README

This package provides abstracts classes and interface to create Value Objects based on the four scalar type supported by PHP.

Installation

composer require frankvanhest/value-objects

What is a Value Object

A Value Object is a wrapper for primitive types such as a string. It usually has specific domain knowledge, and it will make sure that the value is asserted accordingly to that domain knowledge. The characteristics of a Value Object are:

  • it is immutable
  • it wraps primitive data
  • it represents domain specific knowledge
  • it has a name which give it a meaningful purpose
  • it is equals to another instance when the value is the same, but the instance itself is not

Whenever an instance of a Value Object is passed around your application, you can be sure that the value inside is what you expect it to be. For example, if you have a Value Object Email, you can be sure that the value is a correctly formatted string according to what an e-mail address should be.

Purpose

The purpose of this package is to provide a set of interfaces, abstracts and factories to get you started with value objects. It should not be considered as a silver bullet. You should always be use the best tools for your application. That being said, what could be reasons to use this package. Like stated before, if you are new to value objects this package will get you started and provide a learning opportunity to get a better understanding of value objects. Another reason is that you'll don't need to write the same basic code for your value objects, but again do not use it as a silver bullet. When you encounter a value object that cannot or cannot easily be created by using this package, do not try to do so. Write the complete implementation yourself. When you are more experienced with value objects and programming in general, this package is probably not for you. That is of course that is entirely up to you.

Installation

A most of the packages for PHP, the installation is done by using Composer.

composer require frankvanhest/value-objects

Usage

This package only provided the means to create your own Value Objects. The four main primitives types string, integer, float and boolean are included. Each one has its own interface, abstract and factory (except for boolean) available. With each factory you can add a corresponding value modifier. For instance, you want to create a Value Object for Money. You can use the interface FloatValueObject and implement according to your own need. To simplify things, you can also use the abstract class FloatValueObject. See the following example.

use FrankVanHest\ValueObjects\Abstracts\FloatValueObject;

final readonly class Money extends FloatValueObject
{
    protected function assert(float $value): void
    {
        if ($value < 0) {
            throw new \InvalidArgumentException('Only a value greater than zero is allowed');
        }
    }
}

As you can see, you only have to implement the assertion for your Value Object according to your domain knowledge. In this case the only constraint is that the value should be greater than zero.

We got convenient methods like fromFloat, toFloat and equals available.

What if I want to be able to initialize Money from a string. Just implement the interface StringValueObject and we get:

use FrankVanHest\ValueObjects\Abstracts\FloatValueObject;
use FrankVanHest\ValueObjects\Interfaces\StringValueObject;

final readonly class Money extends FloatValueObject implements StringValueObject
{
    protected function assert(float $value): void
    {
        if ($value < 0) {
            throw new \InvalidArgumentException('Only a value greater than zero is allowed');
        }
    }
    
    public function toString(): string
    {
        return sprintf('The value of Money is %f', $this->asFloat());
    }
    
    public static function fromString(string $value): static
    {
        if (!is_numeric($value)) {
            throw new \InvalidArgumentException('Only numeric values are allowed');
        }
        
        return new static((float)$value);
    }
}

If you want to modify the value before the value object is created you can provide a value modified when using the factory to create the Money object.

use FrankVanHest\ValueObjects\Interfaces\FloatValueModifier;

final readonly class DivideBy implements FloatValueModifier
{
    public function __construct(private float $divideBy)
    {    
    }
    
    public function modify(float $value): float
    {
        return $value / $this->divideBy;
    }
}
use FrankVanHest\ValueObjects\Factories\FloatValueObjectFactory;

$money = FloatValueObjectFactory::create(Money::class, 100.50, new DivideBy(10));

As stated before when it comes to comparing two instances of Value Objects it only compares the value itself, not if the Value Objects are the same instance. To clarify this, look at the following examples based the Money class.

$moneyA = Money::fromFloat(10.25);
$moneyB = Money::fromFloat(10.25);
$moneyC = Money::fromFloat(1.50);

$moneyA->equals($moneyA); // Returns true
$moneyA->equals($moneyB); // Returns true
$moneyA->equals($moneyC); // Returns false
$moneyA->equals(null); // Returns false

Contribution

Feel free to submit an issue of pull request. When it comes to a pull request I'm curious of how it improves this packages of which problem it may solve.

There are some requirements:

  • Use PSR-12 for code styling
  • Write a test for every meaningful change

License

See LICENCE