The Problem
Whenever I have to do some reporting I always end up having a lengthy conversation with the people who will use the reports. Because more than once, we disagree on how to calculate the reports. The disagreement stem from the fact that a report is a calculation/selection of things over an specified interval which requires a starting endpoint and an ending endpoint. but what people always fail to realise is that a report also needs some bounds in other words we as developers need to know how to handle the report bounds. Should they be included or excluded of the report. In a previous post I have explain that I prefer to always include the start endpoint and exclude the end endpoint, to me it is the most reasonable default behaviour to chose.
The solution
However as in everything there’s no single right response and context matter. Meaning that depending on the context the type of bounds chosen can change and thus affect the interval and subsequently the reports. In order to avoid or at least to reduce the changes, we need a way to express those bounds and to make sure our code can be made aware of them in an efficient and predicable way.
The initial implementation
Luckily for us with the addition of Enumerations or Enums in PHP8.1
we can easily do that as seen in the example below:
The terms lower and upper derived from how interval are expressed in mathematics.
The reason we choose Enumeration over a list of scalar values is because Enumeration represents a list of finite states. In our examples bound intervals only exist in four different types. By avoiding string or integer or boolean you get a concise, readable and less error prone structure to use. You could easily make a typo with integer or with string and it would go unnoticed whilst with an Enum the PHP engine feedback loop will warn your directly of your mistake.
With the defined Enums we have listed all possible bounds. When used in combination of any type of interval you can be sure that your are covering and expressing all types of bounded intervals.
For instance let’s create an interval value objects which deals with integer ranges.
We will be instantiating and using the immutable value object as follow
Adding some behaviours
Now that we have the Bounds
Enum at our disposal we can attach some behaviour to it, to improve bounds interaction.
Here’s an example to illustrate, let’s say we have two NumberInterval
, $intervalOne
and $intervalTwo
and we need to know if both intervals meets (ie: if they are touching each other). In order to satisfy those requirements, the following rules needs to be checked:
$intervalOne
upper number must be equal to$intervalTwo
lower number$intervalOne
upper bound must be inclusive$intervalTow
lower bound must be inclusive
Or
$intervalTwo
upper number must be equal to$intervalOne
lower number$intervalTwo
upper bound must be inclusive$intervalOne
lower bound must be inclusive
Let’s expose the inclusion rules on the Bounds
enum.
And add the missing relation methods to the NumberInterval
class.
We now have a more powerful public API which is easier to read and to predict.
As you can see the NumberInterval
acts like an orchestrator who uses each of its inner property to handle basic operations to expose more powerful features to the end users.
What about representation ?
As you may have notice our Enum is not backed. Which means that no string representation is currently associated with it. But then again, this is not a limitation as we can work around the missing feature. Once again, the Bounds
Enumeration will be responsible for the formatting while The NumberInterval
will be responsible for calling the right methods on each of its properties to generate the expected string representation.
The Iso 80000 is the standard to represents style guide for mathematical and scientific notation
And we will add to NumberInterval
a toString
conversion method
Now we can represents the interval as a string.
If, for instance, you are using PostgreSQL you can use this string representation as part of your SQL query to select some range in your persistence layer. The same could be achieved for MySQL/MariaDB
with a different method, of course.
Final thoughts
While the Bounds
feature could have been implemented in previous version of PHP, by using PHP8+ feature like constructor promotion, Enum, readonly properties and the match construct, to name but a few, makes the implementation easier to reason with and improved its maintenance costs. And last but not least it makes for a better DX.
TL;DR
If you liked the presented Bounds
Enum you can find an improved and more complete version in the codebase of the League\Period
v5 package which will be released soon. In the previous version of League\Period the bounds when constants attached to the Period class whilst in the new version they are completely separated of the class and upgraded to a proper structure to better reason with. As the Enumeration object is now fully decoupled from the rest of the package you can re-use it in your next PHP8.1+ project.