Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reusable value-objects w/ better type safety #182

Open
sebastianstucke87 opened this issue Nov 12, 2020 · 4 comments
Open

reusable value-objects w/ better type safety #182

sebastianstucke87 opened this issue Nov 12, 2020 · 4 comments

Comments

@sebastianstucke87
Copy link

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('[email protected]', '[email protected]');

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 WorkEmail and 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('[email protected]');
$personal = PersonalEmail::fromString('[email protected]');

$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
Copy link

coelhoricardo commented Nov 13, 2020

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

@llaville
Copy link

@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
Copy link
Contributor

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('[email protected]'),
    new Email('[email protected]'),
);

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('[email protected]'),
    new Email('[email protected]'),
));

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

@peter-gribanov
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants