During a workshop we were discussing the concept of a Data Transfer Object (DTO). The main characteristic of a DTO is that it holds only primitive-type values (strings, integers, booleans), lists or maps of these values including "nested" DTOs. Not sure who came up with this idea, but I'm using it because it ensures that the DTO becomes a data structure that only enforces a schema (field names, the expected types, required fields, and optional fields), but doesn't enforce semantics for any value put into it. That way it can be created from any data source, like submitted form values, CLI arguments, JSON, XML, Yaml, and so on. Using primitive values in a DTO makes it clear that the values are not validated. The DTO is just used to transfer or carry data from one layer to the next. A question that popped up during the workshop: can we consider DateTimeImmutable
a primitive-type value too? If so, can we use this type inside DTOs?
I thought it was an interesting question to explore. I'd like to say "No" immediately, but why?
Is something deserving of a predicate? To decide, we have to define what the predicate means. Stated in an abstract way like this it makes a lot of sense, but when discussing concrete questions it's not often clear that we should talk about definitions; we often like to jump to an answer immediately! So, in this case, what's a primitive-type value?
First, we could consider "primitive" to mean "can't be further divided into parts". In that sense, when a Money
value object would consist of an integer for the number of cents, and a string declaring the currency, Money
is not primitive, but the integer and the string inside are, because it doesn't make sense to take them apart. Although you might say that something more primitive than a string is a character. It's just that PHP doesn't distinguish this type. DateTimeImmutable
in this sense is not primitive, the value that it contains (the timestamp) is.
Second, we could consider "primitive" to mean "what's not an object", or as PHP calls these values: scalars. Again, "primitive" takes into account what the programming language considers primitive, because Java for instance has strings which are considered primitive values but are nevertheless objects. Java has a weird relationship with primitive values anyway, because strings, integers, etc. look very primitive in Java code (i.e. you don't do new String("a string")
but just write "a string"
)). With PHP there's less confusion around this concept. When "primitive" is used in this sense, DateTimeImmutable
could never be considered a primitive-type value, because it's an object, but in Java it could be, because other primitive values are considered to be primitive regardless of them being an object.
Third, we could consider "primitive" to mean "whatever types the language offers out-of-the-box". This is often equivalent to "native". Unfortunately, this isn't a very helpful definition, since what's native is unclear. Is there a "core" part of the language that defines these types? In that case, where does DateTimeImmutable
belong? Isn't that part of an extension? Also, would we consider file handles (resources) primitive types? In the end, most of what is part of the "core" or "native" language is quite arbitrary.
Fourth, we could consider "primitive" to mean - regardless of the language - "what types do we need to describe data?" In that sense, we may look back in the history of humanity itself and consider numbers very primitive (e.g. for describing the value of something). Same for strings (e.g. for writing down a name). Arguably a date or a time isn't primitive, because it's built up from strings (or characters), and numbers.
Fifth, we could consider "primitive" to mean "bare values, not necessarily sensible or correct ones". So "2" is a primitive value, it doesn't say 2 of what, so we can't judge if the value is correct. "UKj" is a primitive value, it doesn't say what it describes, so there's no way to judge this value. Using this definition, a DateTimeImmutable
value is certainly not a primitive value because when you instantiate it, it processes the provided string constructor argument and throws an error if it is not a sensible one. Or, maybe worse, converts it into a value that does make sense, but may no longer match the intention of the actor that produced the value.
For me, this final point is the most important attribute of primitive-ness, which disqualifies DateTimeImmutable
as a primitive-type value. Anyway, we already established that DateTimeImmutable
can't be considered primitive according to the other definitions either.
Am I missing any possible definitions of "primitive" here? Just let me know!
It seems to me that the issue is “behavior”; this is an extension of your points one & five. Look at
DateTimeImmutable
as a DTO itself (and thus usable in another DTO). Much like yourMoney
example,DateTimeImmutable
carries a “value” (cents/seconds) and a “qualifier” (currency/timezone). This would normally be acceptable (desirable!) as a DTO - however there is also a lot of behavior packed intoDateTimeImmutable
that has very undesirable consequences. Some of these you mentioned already and I won’t repeat, but I will also add that it draws its qualifier from the current server setting when not explicitly provided. I could imagine aDateTimeDTO
as a simple variant or extension ofDateTimeImmutable
, one that only accepted Unix timestamps (which are always UTC) and an optional qualifier timezone. It would lose a lot of the native version’s utility but: isn’t that what we want from a DTO?That's a good point, Matt, about the server setting. And even if you do provide a timezone explicitly, it does some validation on the provided constructor arguments, including the date/time string passed to it, which already promotes it from a primitive value to an actual (value) object.