oliverdavies.uk/content/node.d7f7c11b-8ff6-4543-9945-fd500d9a80f9.yml

202 lines
8.5 KiB
YAML

uuid:
- value: d7f7c11b-8ff6-4543-9945-fd500d9a80f9
langcode:
- value: en
type:
- target_id: daily_email
target_type: node_type
target_uuid: 8bde1f2f-eef9-4f2d-ae9c-96921f8193d7
revision_timestamp:
- value: '2025-05-11T09:00:53+00:00'
revision_uid:
- target_type: user
target_uuid: b8966985-d4b2-42a7-a319-2e94ccfbb849
revision_log: { }
status:
- value: true
uid:
- target_type: user
target_uuid: b8966985-d4b2-42a7-a319-2e94ccfbb849
title:
- value: 'Refactoring to value objects'
created:
- value: '2022-10-03T00:00:00+00:00'
changed:
- value: '2025-05-11T09:00:53+00:00'
promote:
- value: false
sticky:
- value: false
default_langcode:
- value: true
revision_translation_affected:
- value: true
path:
- alias: /daily/2022/10/03/refactoring-value-objects
langcode: en
body:
- value: |
<p>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.</p>
<pre><code class="language-php">return Collection::make($stationNodes)
-&gt;map(fn (NodeInterface $station): string =&gt; $station-&gt;get('field_station_code')-&gt;getString())
-&gt;values();
</code></pre>
<p>There are two issues with this code.</p>
<p>First, whilst I'm implicitly saying that it accepts a certain type of node, because of the <code>NodeInterface</code> 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.</p>
<p>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.</p>
<h2 id="introducing-a-value-object">Introducing a value object</h2>
<p>This is the value object that I created.</p>
<p>It accepts the original node but checks to ensure that the node is the correct type. If not, an Exception is thrown.</p>
<p>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.</p>
<pre><code class="language-php">namespace Drupal\mymodule\ValueObject;
use Drupal\node\NodeInterface;
final class Station implements StationInterface {
private NodeInterface $node;
private function __construct(NodeInterface $node) {
if ($node-&gt;bundle() != 'station') {
throw new \InvalidArgumentException();
}
$this-&gt;node = $node;
}
public function getStationCode(): string {
return $this-&gt;node-&gt;get('field_station_code')-&gt;getString();
}
public static function fromNode(NodeInterface $node): self {
return new self($node);
}
}
</code></pre>
<h2 id="refactoring-to-use-the-value-object">Refactoring to use the value object</h2>
<p>This is what my code now looks like:</p>
<pre><code class="language-php">return Collection::make($stationNodes)
-&gt;map(fn (NodeInterface $node): StationInterface =&gt; Station::fromNode($node))
-&gt;map(fn (StationInterface $station): string =&gt; $station-&gt;getStationCode())
-&gt;values();
</code></pre>
<h1>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD:website/source/_daily_emails/2022-10-03.md</h1>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p>b9cea6d (chore: replace Sculpin with Astro):website/src/pages/daily-emails/2022-10-03.md
I've added an additional <code>map</code> 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.</p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>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.</p>
format: full_html
processed: |
<p>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.</p>
<pre><code class="language-php">return Collection::make($stationNodes)
-&gt;map(fn (NodeInterface $station): string =&gt; $station-&gt;get('field_station_code')-&gt;getString())
-&gt;values();
</code></pre>
<p>There are two issues with this code.</p>
<p>First, whilst I'm implicitly saying that it accepts a certain type of node, because of the <code>NodeInterface</code> 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.</p>
<p>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.</p>
<h2 id="introducing-a-value-object">Introducing a value object</h2>
<p>This is the value object that I created.</p>
<p>It accepts the original node but checks to ensure that the node is the correct type. If not, an Exception is thrown.</p>
<p>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.</p>
<pre><code class="language-php">namespace Drupal\mymodule\ValueObject;
use Drupal\node\NodeInterface;
final class Station implements StationInterface {
private NodeInterface $node;
private function __construct(NodeInterface $node) {
if ($node-&gt;bundle() != 'station') {
throw new \InvalidArgumentException();
}
$this-&gt;node = $node;
}
public function getStationCode(): string {
return $this-&gt;node-&gt;get('field_station_code')-&gt;getString();
}
public static function fromNode(NodeInterface $node): self {
return new self($node);
}
}
</code></pre>
<h2 id="refactoring-to-use-the-value-object">Refactoring to use the value object</h2>
<p>This is what my code now looks like:</p>
<pre><code class="language-php">return Collection::make($stationNodes)
-&gt;map(fn (NodeInterface $node): StationInterface =&gt; Station::fromNode($node))
-&gt;map(fn (StationInterface $station): string =&gt; $station-&gt;getStationCode())
-&gt;values();
</code></pre>
<h1>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD:website/source/_daily_emails/2022-10-03.md</h1>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p>b9cea6d (chore: replace Sculpin with Astro):website/src/pages/daily-emails/2022-10-03.md
I've added an additional <code>map</code> 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.</p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>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.</p>
summary: null
field_daily_email_cta: { }