Making a ValueObject NULL-prove

in Testing and Coding a ValueObject we used “clone” to make the ValueObject and it’s internally stored objects immutable. The code for the constructor and the getter was quite simple:

/**
 * @var DateTime
 */
protected $dateTime;

/**
 * @param DateTime $dateTime
 */
public function __construct($dateTime) {
    $this->dateTime = clone $dateTime;
}

Unfortunately this leads to an exception, if you try to set NULL. Furthermore it doesn’t take care of wrong parameters like e.g. an integer, array or objects different from DateTime. To make this ValueObject “NULL-prove” and check the argument’s type, we generate an according test first.

/**
 * @test
 */
public function setAndGetNullWorks() {
    $myValueObject = $this->objectManager->get(MyValueObject, NULL);
    $this->assertNull($myValueObject->getDateTime());
}

/**
 * @test
 * @expectedException InvalidArgumentException
 * @expectedExceptionCode 1412287860
 */
public function constructWithNonDateTimeThrowsException() {
    $myValueObject = $this->objectManager->get(MyValueObject, 42);
}

The first test just calls the constructor with $dateTime=NULL and expects the same “NULL”, if it reads this value back using the getDateTime() method. Using the simple clone implementation above this will fail, because “clone NULL” does not work and will throw an exception.

The second test tries to send an int to the constructor. Please note the annotations: We expect the InvalidArgumentException to be thrown with a specific exception code (1412287860). This will fail as well, if we try to clone the int value.

You can use the constructor with an explicitly named Class like this:

public function __construct(DateTime $dateTime) {
    // ....
}

But this won’t let you use “NULL”, because “NULL” is not accepted as a DateTime object.

Conclusion

Do not use dedicated Class references in parameter lists and use class / type checks inside your value objects, if you

  • want to make the parameter optional (thus it might be “NULL”) or
  • want to use parameters that might be of different classes and/or types (“mixed”).

Do use dedicated Class references in parameter lists, if you

  • want to make the parameter mandatory and
  • want the parameter to be an object of a dedicated class.

You have to ask yourself…

Of course you have to ask yourself how a method shall react in cases like the once above. In my case I accept NULL parameters and I want to get an exception, if the parameter is not a DateTime object. It’s a very good idea to first think about your expectations and to write a unit test reflecting exactly those expectations and not thinking about the actual implementation in the first place. Do the implementation afterwards to meet the functional test, but don’t write a test to cover your planned technical implementation.

The implementation:

One of a variety of solutions to cover my tests above is the following implementation for the constructor:

/**
 * @param DateTime $dateTime
 */
public function __construct($dateTime) {
    if(is_null($dateTime)) {
        $this->dateTime = NULL;
    } elseif($dateTime instanceof DateTime) {
        $this->dateTime = clone $dateTime;
    } else {
        throw new InvalidArgumentException('MyValueObject::__construct must retrieve a DateTime object', 1412287860);
    }
}

One remark regarding the exception code: It’s pretty equal which exception code you use. I totally recommend to use exception codes and to use unique exception codes for every position in the source code where an exception is thrown. Reason: It’s very easy to find those exception locations by searching for the exception code using a simple full text search. This makes support and debugging much more easier.

If you want to generate a unique exception code, just use the current system time stamp when writing your code (e.g. “date +%s” for Unix/Linux systems). I always define the exception code in the test that expects the exception to be thrown, because this is the very first place where the exception code is used in the “@expectedExceptionCode” annotation. The actual implementation just copies this value to meet the test case’s expectation.

Leave a Reply