Refactoring to value objects

Here's a snippet of some Drupal code that I wrote last week. It's responsible for converting an array of nodes into a Collection of one of it's field values.

return Collection::make($stationNodes)
->map(fn (NodeInterface $station): string => $station->get('field_station_code')->getString())
->values();

There are two issues with this code.

First, whilst I'm implicitly saying that it accepts a certain type of node, because of the NodeInterface typehint this could accept any type of node. If that node doesn't have the required field, the code will error - but I'd like to know sooner if an incorrect type of node is passed and make it explicit that only a certain type of node can be used.

Second, the code for getting the field values is quite verbose and is potentially repeated in other places within the codebase. I'd like to have a simple way to access these field values that I can reuse anywhere else. If the logic for getting these particular field values changes, then I'd only need to change it in one place.

Introducing a value object

This is the value object that I created.

It accepts the original node but checks to ensure that the node is the correct type. If not, an Exception is thrown.

I've added a helper method to get the field value, encapsulating that logic in a reusable function whilst making the code easier to read and its intent clearer.

namespace Drupal\mymodule\ValueObject;

use Drupal\node\NodeInterface;

final class Station implements StationInterface {

private NodeInterface $node;

private function __construct(NodeInterface $node) {
if ($node->bundle() != 'station') {
throw new \InvalidArgumentException();
}

$this->node = $node;
}

public function getStationCode(): string {
return $this->node->get('field_station_code')->getString();
}

public static function fromNode(NodeInterface $node): self {
return new self($node);
}

}

Refactoring to use the value object

This is what my code now looks like:

return Collection::make($stationNodes)
->map(fn (NodeInterface $node): StationInterface => Station::fromNode($node))
->map(fn (StationInterface $station): string => $station->getStationCode())
->values();

I've added an additional map to convert the nodes to the value object, but the second map can now use the new typehint - ensuring better type safety and also giving us auto-completion in IDEs and text editors. If an incorrect node type is passed in, then the Exception will be thrown and a much clearer error message will be shown.

Finally, I can use the helper method to get the field value, encapsulating the logic within the value object and making it intention clearer and easier to read.