13
\$\begingroup\$

I'm still learning about domain driven design, and I have created my first entity class User.

Is this fully compliant with the DDD principles? Particularly my setters because I have business rules in them. Any other suggestions would be highly appreciated as well.

namespace Models\Entities;

class User
{
    private $id;
    private $firstName;
    private $lastName;
    private $email;
    private $password;
    private $registerDate;

    public function setId($id)
    {
        if (!$this->id) {
            if (is_int($id) && $id >= 1) {
                $this->id = $id;

                return $this;
            }

            throw new \InvalidArgumentException('ID must be a positive integer.');
        }

        throw new \InvalidArgumentException('ID has already been set.');
    }

    public function getId()
    {
        return $this->id;
    }

    public function setFirstName($firstName)
    {
        if (is_string($firstName) && !empty($firstName) && ctype_alpha($firstName)) {
            $length = $this->stringLength($firstName);

            if ($length >= 1 && $length <= 35) {
                $this->firstName = $firstName;

                return $this;
            }
        }

        throw new \InvalidArgumentException('First name must be an alpha string inclusively between 1 and 35 characters.');
    }

    public function getFirstName()
    {
        return $this->firstName;
    }

    public function setLastName($lastName)
    {
        if (is_string($lastName) && !empty($lastName) && ctype_alpha($lastName)) {
            $length = $this->stringLength($lastName);

            if ($length >= 1 && $length <= 35) {
                $this->lastName = $lastName;

                return $this;
            }
        }

        throw new \InvalidArgumentException('Last name must be an alpha string inclusively between 1 and 35 characters.');
    }

    public function getLastName()
    {
        return $this->lastName;
    }

    public function getFullName()
    {
        return $this->firstName . ' ' . $this->lastName;
    }

    public function setEmail($email)
    {
        if (is_string($email) && !empty($email)) {
            $sanitized = filter_var($email, FILTER_SANITIZE_EMAIL);
            $isValid   = filter_var($sanitized, FILTER_VALIDATE_EMAIL);
            $isValid   = $isValid ? $this->stringLength($email) <= 254 : false;

            if ($email === $sanitized && $isValid) {
                $this->email = $email;

                return $this;
            }
        }

        throw new \InvalidArgumentException('Email must be a string in this format: "[local]@[domain].[ext]".');
    }

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

    public function setPassword($password)
    {
        if (is_string($password) && !empty($password)) {
            $length = $this->stringLength($password);

            if ($length >= 6 && $length <= 60) {
                $this->password = $password;

                return $this;
            }
        }

        throw new \InvalidArgumentException('Password must be a string inclusively between 6 and 60 characters.');
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function setRegisterDate($date)
    {
        if (is_int($date) && !empty($date)) {
            // TO DO: date validation unix timestamp is_int

            $this->registerDate = $date;

            return $this;
        }

        throw new \InvalidArgumentException('Register date must be an integer and valid unix timestamp.');
    }

    public function getRegisterDate()
    {
        return $this->registerDate;
    }

    private function stringLength($input)
    {
        $encoding = mb_detect_encoding($input);
        $length   = mb_strlen($input, $encoding);

        return $length;
    }
}
\$\endgroup\$
6
\$\begingroup\$

Your class can absolutely be considered like a DDD domain entity. However, I would suggest some improvements:

  • DDD likes to use an ObjectValue for the unique identifiers, you could use one instead of a simple integer.
  • You could separate the validation logic from the entity (not specific to DDD).
  • You should not have a getPassword() method but a verifyPassword($password) instead. Don't expose the password in plain text.
  • You could also separate the stringLength() into its own string utility class.
  • Regarding the use of a Uniw timestamp, keep in mind that DateTime is a better option since it embeds the time zone. Timestamps don't, which is always confusing when handling dates from all over the world...
\$\endgroup\$
3
\$\begingroup\$

Like Keven stated:

bring a utility function like the calculation of the length of a string in a separate class with other methods similar to this one.

For example:

class Utility
{
    public static function getStringLength($input)
    {
        $encoding = mb_detect_encoding($input);
        $length = mb_strlen($input, $encoding);
        return $length;
    }
}

//Usage:
echo Utility::getStringLength("CodeReview");

Same goes for methods that check for a certain range, or check for integer values:

public static function isInRange($i, $min, $max)
{
    return ($i >= $min && $i <= $max);
}

//Usage:
echo Utility::isInRange($someUnknownIntVar, 10, 20);

For your code example I've created following class:

class Utility
{
    public static function isNonEmptyString($input)
    {
        return (!empty($input) && is_string($input));
    }

    public static function getStringLength($input)
    {
        $encoding = mb_detect_encoding($input);
        $length = mb_strlen($input, $encoding);
        return $length;
    }

    public static function isInt($i)
    {
        return (!empty($i) && is_int($i));
    }

    public static function isPositiveInt($i)
    {
        return (self::isInt($i) && $i > 0);
    }

    public static function isInRange($i, $min, $max)
    {
        return ($i >= $min && $i <= $max);
    }

    public static function isValidEmail($email)
    {
        $sanitized = filter_var($email, FILTER_SANITIZE_EMAIL);
        $isValid = filter_var($sanitized, FILTER_VALIDATE_EMAIL);
        return $isValid;
    }
}

Also mentioned is that an entity should not actually containt any logic. Define another class specifically for validation on the properties. You can add validation methods to the Utility class if you want but I'd keep them separated. This way the Utility class can be used in other projects without having to modify them. Here's an example of a Validator class:

class Validator
{
    public static function isValidName($name)
    {
        $isValidName = (Utility::isNonEmptyString($name) && ctype_alpha($name));
        $isValidName = $isValidName ? Utility::isInRange(Utility::getStringLength($name), 1, 35) : false;
        return $isValidName;
    }

    public static function isValidPassWord($pass)
    {
        $isValidPass = Utility::isNonEmptyString($pass);
        $isValidPass = $isValidPass ? Utility::isInRange(Utility::getStringLength($pass), 6, 60) : false;
        return $isValidPass;
    }

    public static function isValidEmailAddress($email)
    {
        $isValidEmail = Utility::isValidEmail($email);
        $isValidEmail = $isValidEmail ? Utility::isInRange(Utility::getStringLength($email), 0, 254) : false;
        return $isValidEmail;
    }
}

Now, instead of this:

public function setEmail($email)
{
    if (is_string($email) && !empty($email)) {
        $sanitized = filter_var($email, FILTER_SANITIZE_EMAIL);
        $isValid   = filter_var($sanitized, FILTER_VALIDATE_EMAIL);
        $isValid   = $isValid ? $this->stringLength($email) <= 254 : false;

        if ($email === $sanitized && $isValid) {
            $this->email = $email;

            return $this;
        }
    }

    throw new \InvalidArgumentException('Email must be a string in this format: "[local]@[domain].[ext]".');
}

You'll get this clean code (which goes for all your properties like password, name, ...):

public function setEmail($email)
{
    if (Validator::isValidEmailAddress($email)) {
        $this->email = $email;
        return $this;
    }
    throw new \InvalidArgumentException('Email must be a string in this format: "[local]@[domain].[ext]".');
}
\$\endgroup\$
2
\$\begingroup\$

Yes, it can be considered a DDD entity, but all validation logic in the setters could be moved to an invariants checking method, that could clean the methods, bringing space to implement more interesting business logic.

public boolean checkInvariants() {

    //Check first name

    //Check lastname

    //Check e-mail 

    //And so on...

}

It also could have more expressive methods aligned with the ubiquitous language, not only getters and setters, something like:

public String fullName() { return firstName + " " + lastName; }

public void doLogin() { ... }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.