1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
|
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Minimalist Journal - Theme for Hugo</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Arvo:400,700">
<link rel="stylesheet" href="https://ianrodrigues.dev/css/highlight.css">
<link rel="stylesheet" href="/build/theme.css">
</head>
<body class="font-serif bg-gray-200 border-t-4 border-blue-500 antialiased">
<div class="w-full p-6 md:w-2/3 md:px-0 md:mx-auto xl:w-2/5">
<header class="mb-12">
<div class="mb-8 md:flex md:items-center">
<img class="hidden md:block w-20 rounded-full mr-6" src="https://www.oowlish.com/wp-content/uploads/2018/07/ian.png" alt="Ian Rodrigues">
<div>
<h1 class="text-4xl font-bold">Ian Rodrigues</h1>
<p>yet another blog about dev, sometimes ops</p>
</div>
<!-- <div>
<a class="text-lg mb-8 inline-block" href="">← Back Home</a>
<h1 class="text-4xl font-bold">Writing value objects in PHP</h1>
<time datetime="2019-11-03 13:30:00 -0300">03 Nov 2019</time>
<ol class="mt-4">
<li class="inline-block">
<a class="border-none text-gray-800 text-xs bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="">php</a>
</li>
<li class="inline-block">
<a class="border-none text-gray-800 text-xs bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="">ddd</a>
</li>
<li class="inline-block">
<a class="border-none text-gray-800 text-xs bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="">value objects</a>
</li>
<li class="inline-block">
<a class="border-none text-gray-800 text-xs bg-gray-400 hover:bg-gray-600 hover:text-white rounded-sm px-3 py-1" href="">doctrine</a>
</li>
</ol>
</div> -->
</div>
<!-- Languages -->
<nav>
<a href="https://ianrodrigues.dev/pt-br/">🇺🇸 English</a> /
<a href="https://ianrodrigues.dev/pt-br/">🇩🇪 Deustch</a> /
<a href="https://ianrodrigues.dev/pt-br/">🇧🇷 Português</a>
</nav>
</header>
<section class="mb-24">
<div>
<h2 class="text-3xl font-bold mb-2">2019</h2>
<ol>
<li class="mb-6 md:flex md:flex-row">
<time class=" block md:flex-l-24" datetime="2019-11-03 13:30:00 -0300">Nov 09</time>
<a class="text-lg md:ml-12" href="/2019/11/writing-value-objects-in-php/">Writing value objects in PHP</a>
</li>
<li class="mb-6 md:flex md:flex-row">
<time class=" block md:flex-l-24" datetime="2019-11-03 13:30:00 -0300">Aug 23</time>
<a class="text-lg md:ml-12" href="/2019/11/writing-value-objects-in-php/">Hello, World!</a>
</li>
</ol>
</div>
</section>
<article class="mb-12">
<p>If you already have heard about <a href="https://en.wikipedia.org/wiki/Domain-driven_design">DDD (Domain-Driven Design)</a>, you probably also may have heard about Value Objects. It is one of the building blocks introduced by Eric Evans in <a href="https://www.amazon.com.br/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215">“the blue book”</a>.</p>
<p>A value object is a type which wraps data and is distinguishable only by its properties. Unlike an <code>Entity</code>, it doesn’t have a unique identifier. Thus, <strong>two value objects with the same property values should be considered equal</strong>.</p>
<p>A good example of candidates for value objects are:</p>
<ul>
<li>phone number</li>
<li>address</li>
<li>price</li>
<li>a commit hash</li>
<li>a entity identifier</li>
<li>and so on.</li>
</ul>
<p>When designing a value object, you should pay attention to its three main characteristics: <strong>immutability, structural equality, and self-validation</strong>.</p>
<p>Here’s an example:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-keyword">const</span> USD = <span class="hljs-string">'USD'</span>;
<span class="hljs-keyword">const</span> CAD = <span class="hljs-string">'CAD'</span>;
<span class="hljs-comment">/** <span class="hljs-doctag">@var</span> float */</span>
<span class="hljs-keyword">private</span> $amount;
<span class="hljs-comment">/** <span class="hljs-doctag">@var</span> string */</span>
<span class="hljs-keyword">private</span> $currency;
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(float $amount, string $currency = <span class="hljs-string">'USD'</span>)</span>
</span>{
<span class="hljs-keyword">if</span> ($amount < <span class="hljs-number">0</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \InvalidArgumentException(<span class="hljs-string">"Amount should be a positive value: {$amount}."</span>);
}
<span class="hljs-keyword">if</span> (!in_array($currency, <span class="hljs-keyword">$this</span>->getAvailableCurrencies())) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \InvalidArgumentException(<span class="hljs-string">"Currency should be a valid one: {$currency}."</span>);
}
<span class="hljs-keyword">$this</span>->amount = $amount;
<span class="hljs-keyword">$this</span>->currency = $currency;
}
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAvailableCurrencies</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
</span>{
<span class="hljs-keyword">return</span> [<span class="hljs-keyword">self</span>::USD, <span class="hljs-keyword">self</span>::CAD];
}
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAmount</span><span class="hljs-params">()</span>: <span class="hljs-title">float</span>
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->amount;
}
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCurrency</span><span class="hljs-params">()</span>: <span class="hljs-title">string</span>
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->currency;
}
}
</code></pre>
<h2 id="immutability">Immutability</h2>
<p>Once you instantiate a value object, <strong>it should be the same for the rest of the application lifecycle</strong>. If you need to change its value, it should be done by entirely replacing that object.</p>
<p>Using mutable value objects is acceptable if you are using them <strong>entirely within a local scope</strong>, with only one reference to the object. Otherwise, you may have problems.</p>
<p>Taking the previous example, here’s how you can update the amount of a <code>Price</code> type:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hasSameCurrency</span><span class="hljs-params">(Price $price)</span>: <span class="hljs-title">bool</span>
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->currency === $price->currency;
}
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum</span><span class="hljs-params">(Price $price)</span>: <span class="hljs-title">self</span>
</span>{
<span class="hljs-keyword">if</span> (!<span class="hljs-keyword">$this</span>->hasSameCurrency($price)) {
<span class="hljs-keyword">throw</span> \InvalidArgumentException(
<span class="hljs-string">"You can only sum values with the same currency: {$this->currency} !== {$price->currency}."</span>
);
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">self</span>(<span class="hljs-keyword">$this</span>->amount + $price->amount, <span class="hljs-keyword">$this</span>->currency);
}
}
</code></pre>
<h2 id="structural-equality">Structural Equality</h2>
<p>Value objects <strong>don’t have an identifier</strong>. In other words, if two value objects have the same internal values, they must be considered equal. As PHP doesn’t have a way to override the equality operator, you should implement it by yourself.</p>
<p>You can create a specialized method to do that:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEqualsTo</span><span class="hljs-params">(Price $price)</span>: <span class="hljs-title">bool</span>
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->amount === $price->amount && <span class="hljs-keyword">$this</span>->currency === $price->currency;
}
}
</code></pre>
<p>Another option is to create a hash based on its properties:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hash</span><span class="hljs-params">()</span>: <span class="hljs-title">string</span>
</span>{
<span class="hljs-keyword">return</span> md5(<span class="hljs-string">"{$this->amount}{$this->currency}"</span>);
}
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEqualsTo</span><span class="hljs-params">(Price $price)</span>: <span class="hljs-title">bool</span>
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>->hash() === $price->hash();
}
}
</code></pre>
<h2 id="self-validation">Self-Validation</h2>
<p>The <strong>validation of a value object should occur on its creation</strong>. If any of its properties are invalid, it should throw an exception. Putting this together with immutability, once you create a value object, you can be sure it will always be valid.</p>
<p>Taking the <code>Price</code> type example once again, it doesn’t make sense to have a negative amount for the domain of the application:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(float $amount, string $currency = <span class="hljs-string">'USD'</span>)</span>
</span>{
<span class="hljs-keyword">if</span> ($amount < <span class="hljs-number">0</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \InvalidArgumentException(<span class="hljs-string">"Amount should be a positive value: {$amount}."</span>);
}
<span class="hljs-keyword">if</span> (!in_array($currency, <span class="hljs-keyword">$this</span>->getAvailableCurrencies())) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \InvalidArgumentException(<span class="hljs-string">"Currency should be a valid one: {$currency}."</span>);
}
<span class="hljs-keyword">$this</span>->amount = $amount;
<span class="hljs-keyword">$this</span>->currency = $currency;
}
}
</code></pre>
<h2 id="using-with-doctrine">Using with Doctrine</h2>
<p>Storing and retrieving value objects from the database using <a href="https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/tutorials/embeddables.html">Doctrine</a> is quite easy using <code>Embeddable</code>s. According to the documentation, <code>Embeddable</code>s are not entities. But, you embed them in entities, which makes them perfect for dealing with value objects.</p>
<p>Let’s suppose you have a <code>Product</code> class, and you would like to store the price in that class. You will end up with the following modeling:</p>
<pre><code class="language-php hljs"><span class="hljs-meta"><?php</span> <span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);
<span class="hljs-comment">/** <span class="hljs-doctag">@Embeddable</span> */</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Price</span>
</span>{
<span class="hljs-comment">/** <span class="hljs-doctag">@Column</span>(type="float") */</span>
<span class="hljs-keyword">private</span> $amount;
<span class="hljs-comment">/** <span class="hljs-doctag">@Column</span>(type="string") */</span>
<span class="hljs-keyword">private</span> $currency;
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(float $amount, string $currency = <span class="hljs-string">'USD'</span>)</span>
</span>{
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">$this</span>->amount = $amount;
<span class="hljs-keyword">$this</span>->currency = $currency;
}
<span class="hljs-comment">// ...</span>
}
<span class="hljs-comment">/** <span class="hljs-doctag">@Entity</span> */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Product</span>
</span>{
<span class="hljs-comment">/** <span class="hljs-doctag">@Embedded</span>(class="Price") */</span>
<span class="hljs-keyword">private</span> $price;
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(Price $price)</span>
</span>{
<span class="hljs-keyword">$this</span>->price = $price;
}
}
</code></pre>
<p>Doctrine will automatically create the columns from the <code>Price</code> class into the table of the <code>Product</code> class. By default, it prefixes the database columns after the <code>Embeddable</code> class name, in this case: <code>price_amount</code> and <code>price_currency</code>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Value objects are useful for writing <strong>clean code</strong>. Instead of writing:</p>
<pre><code class="language-php hljs"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPhoneNumber</span><span class="hljs-params">(string $phone)</span>: <span class="hljs-title">void</span> </span>{}
</code></pre>
<p>You can write:</p>
<pre><code class="language-php hljs"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPhoneNumber</span><span class="hljs-params">(PhoneNumber $phone)</span>: <span class="hljs-title">void</span> </span>{}
</code></pre>
<p>Which makes it easy to read and reason about it, also you don’t need to figure out which phone format you should use.</p>
<p>Since their attributes define them, and you can share them with other different entities, they can be cacheable forever.</p>
<p>They can help you to <strong>reduce duplication</strong>. Instead of having multiples <code>amount</code> and <code>currency</code> fields, you can use a pure <code>Price</code> class.</p>
<p>Of course, like everything in life, you <strong>should not abuse</strong> of value objects. Imagine you converting tons of objects to primitive values to store them in the database, or converting those back to value objects when fetching them from the database.Indeed, you can have performance issues. Also, having tons of granular value objects can bloat your codebase.</p>
<p>With value objects, you can <strong>reduce the <a href="https://refactoring.guru/smells/primitive-obsession">primitive obsession</a></strong>. Use them to represent a field or group of fields of your domain that require validation or can cause ambiguity if you use primitive values.</p>
<p>Thanks for reading, and happy coding!</p>
<h2 id="further-reading">Further Reading</h4>
<ul>
<li><a href="https://twitter.com/martinfowler">Martin Fowler</a> has an article about value objects, check it out here: <a href="https://martinfowler.com/bliki/ValueObject.html">https://martinfowler.com/bliki/ValueObject.html</a></li>
</ul>
<div id="disqus_thread"><iframe id="dsq-app7082" name="dsq-app7082" allowtransparency="true" scrolling="no" tabindex="0" title="Disqus" style="width: 1px !important; min-width: 100% !important; border: medium none !important; overflow: hidden !important; height: 428px !important;" src="https://disqus.com/embed/comments/?base=default&f=ianrodrigues-1&t_u=https%3A%2F%2Fianrodrigues.dev%2F2019%2F11%2Fwriting-value-objects-in-php%2F&t_d=Writing%20value%20objects%20in%20PHP&t_t=Writing%20value%20objects%20in%20PHP&s_o=default#version=49c585dbc35ad09e087de9f308aa12d0" horizontalscrolling="no" verticalscrolling="no" width="100%" frameborder="0"></iframe><iframe id="indicator-north" name="indicator-north" allowtransparency="true" scrolling="no" tabindex="0" title="Disqus" style="width: 684px !important; border: medium none !important; overflow: hidden !important; top: 0px !important; min-width: 684px !important; max-width: 684px !important; position: fixed !important; z-index: 2147483646 !important; height: 19px !important; min-height: 19px !important; max-height: 19px !important; display: none !important;" frameborder="0"></iframe><iframe id="indicator-south" name="indicator-south" allowtransparency="true" scrolling="no" tabindex="0" title="Disqus" style="width: 684px !important; border: medium none !important; overflow: hidden !important; bottom: 0px !important; min-width: 684px !important; max-width: 684px !important; position: fixed !important; z-index: 2147483646 !important; height: 19px !important; min-height: 19px !important; max-height: 19px !important; display: none !important;" frameborder="0"></iframe></div>
<script type="application/javascript">
var disqus_config = function () {
};
(function() {
if ([""].indexOf(window.location.hostname) != -1) {
document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
return;
}
var d = document, s = d.createElement('script'); s.async = true;
s.src = '//' + "ianrodrigues-1" + '.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
</article>
<footer>
<p>
© 2019. Ian Rodrigues, software engineer @ <a class="border-b border-gray-700" href="">oowlish technology</a>
</p>
<p>
<a href="">GitHub</a> / <a href="">Twitter</a>
</p>
</footer>
</div>
</body>
</html>
|