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;
 }
}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jul 22, 2014 at 11:46
\$\endgroup\$

3 Answers 3

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...
answered Jul 23, 2014 at 8:10
\$\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]".');
}
answered Jul 23, 2014 at 10:02
\$\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() { ... }
answered Nov 20, 2017 at 18:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.