piotrplenik / clean-code-php

:bathtub: Clean Code concepts adapted for PHP
MIT License
12.26k stars 2.28k forks source link

reusable value-objects w/ better type safety #182

Open sebastianstucke87 opened 3 years ago

sebastianstucke87 commented 3 years ago

I didn't really understand the point of traits until recently and now I can't live without them. And since I didn't find anything about traits on here, I wanted to share my findings. Is this something you are interested in?


In this first example, the two arguments workEmail and personalEmail are switched. But according to the signature of updateEmail, everything is in order since both arguments are of type string:

final class User
{
    public function updateEmail(string $workEmail, string $personalEmail): void
    {
        /* ... */
    }
}

$user = new User();
$user->updateEmail('ripper666@aolmail.com', 'john_doe@acme.com');

Here we created the value-object EmailTrait to wrap any email-value. But instead of a class, the value-object is a trait. We re-use the trait in WorkEmailand PersonalEmail. Both have the same behavior as EmailTrait but have their own type WorkEmail and PersonalEmail respectively.

trait EmailTrait
{
    private string $email;

    private function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException(/* ... */);
        }

        $this->email = $email;
    }

    public static function fromString(string $email): self
    {
        return new self($email);
    }

    public function toString(): string
    {
        return $this->email;
    }
}

final class WorkEmail
{
    use EmailTrait;
}

final class PersonalEmail
{
    use EmailTrait;
}

Now in this second example, the signature of updateEmail is absolutely clear about its argument types and can't be misused:

final class User
{
    public function updateEmail(WorkEmail $work, PersonalEmail $personal): void
    {
        /* ... */
    }
}

$work = WorkEmail::fromString('john_doe@acme.com');
$personal = PersonalEmail::fromString('jonny1337@aolmail.com');

$user = new User();
$user->updateEmail($work, $personal);

This is especially helpful, when there are a lot of primitive values that behave essentially the same, but represent different things in the domain (IDs, postal addresses, event-objects etc.).

coelhoricardo commented 3 years ago

Hello, You can achieve the same result with inheritance. Can you explain why you prefer to use traits ? Thanks

llaville commented 3 years ago

@sebastianstucke87 Because you didn't find any article about traits, here us one that could help to understand more features about traits. Overriding & Extending a PHP Trait Method | Andy Carter https://andy-carter.com/blog/overriding-extending-a-php-trait-method

peter-gribanov commented 3 years ago

Using traits in Value Objects is an indicator that you are using Value Objects incorrectly. A Value Object is a Minimum Viable Universal Unit, not a Unique Unit. It is not good to create a unique Value Object for each variable (DRY). An exception can be Value Objects for identifiers for which the uniqueness of the type is important, and the code is almost always identical.

final class Email
{
    private string $email;

    public function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException(/* ... */);
        }

        $this->email = $email;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function __toString(): string
    {
        return $this->email;
    }
}

final class User
{
    public function updateEmail(Email $work, Email $personal): void
    {
        /* ... */
    }
}

$user->updateEmail(
    new Email('ohn_doe@acme.com'),
    new Email('jonny1337@aolmail.com'),
);

For more control over the data structure, you can use the Value Object to describe a group of object properties.

final class UserContactData
{
    private Email $work;
    private Email $personal;

    public function __construct(Email $work, Email $personal)
    {
        $this->work = $work;
        $this->personal = $personal;
    }

    public function getWorkEmail(): Email
    {
        return $this->work;
    }

    public function getPersonalEmail(): Email
    {
        return $this->personal;
    }
}

final class User
{
    private UserContactData $contact_data;

    public function changeContactData(UserContactData $contact_data): void
    {
        $this->contact_data = $contact_data
    }
}

$user->changeContactData(new UserContactData(
    new Email('ohn_doe@acme.com'),
    new Email('jonny1337@aolmail.com'),
));

If you are using the Doctrine, then you can describe it through Embeddables.

peter-gribanov commented 3 years ago

The topic of traits has already been raised in another issue #130. So far, there are no adequate examples of using traits. I would also like to see a good example of using traits here.