« back published by @mmartin_joo on January 24, 2023
Value Objects Everywhere
In this article, I'd like to talk about value objects. If you don't know what are they, here's a quick introduction. Or you can read my detailed article about the basics.
Value Object is an elementary class that contains mainly (but not only) scalar data. So it's a wrapper class that holds together related information. Here's an example:
class Percent
{
public readonly ?float $value;
public readonly string $formatted;
public function __construct(float $value)
{
$this->value = $value;
if ($value === null) {
$this->formatted = '';
} else {
$this->formatted = number_format(
$value * 100, 2
) . '%';
}
}
public static function from(?float $value): self
{
return new self($value);
}
}
This class represents a percentage value. This simple class gives you three advantages:
- It encapsulates the logic that handles null values and represents them as percentages.
- You always have two decimal places (by default) in your percentages.
- Better types.
An important note: business logic or calculation is not part of a value object. The only exception I make is basic formatting.
That's it. This is a value object. It's an object that contains some values. The original definition of a value object states two more things:
- It's immutable. You have no setters and only read-only properties.
- It does not contain an ID or any other property related to the identification. Two value objects are equal only when the values are the same. This is the main difference between a VO and a DTO.
Data Modeling
To really understand value objects, we'll implement a very basic financial app. Something like Seekingalpha, Morningstar, Atom Finance, or Hypercharts. If you don't know these apps, here's a simplified introduction:
- In the app we store companies. Publicly-traded companies, such as Apple or Microsoft.
- We also store financial data, such as income statements.
- The app will calculate some important metrics from these data. For example, profit margin, gross margin, and a few others.
In the sample application, I'll only implement a handful of metrics, and I'll only store the income statements (no balance sheets or cash flows). This is more than enough to illustrate to use of value objects.
This is what the database looks like:
As you can see, it's quite easy. This is a sample row from the companies
table:
id | ticker | name | price_per_share | market_cap |
---|---|---|---|---|
1 | AAPL | Apple Inc. | 14964 | 2420000 |
2 | MSFT | Microsoft Inc. | 27324 | 2040000 |
price_per_share
is the current share price of the company's stock. It's stored in cent value, so 14964
is $149.64
. This is a common practice in order to avoid rounding mistakes.
market_cap
is the current market capitalization of the company (price_per_share * number of shares). It is stored in millions, so 2420000
is $2,420,000,000,000
or $2,420B
or $2.42T
. Storing huge financial numbers in millions (or thousands in some cases) is also a common practice in financial applications.
Now let's see the income_statements
table:
company_id | year | revenue | gross_profit |
---|---|---|---|
1 | 2022 | 386017 | 167231 |
1 | 2021 | 246807 | 167231 |
Each item on the income statement has its own column such as revenue or gross_profit. One row in this table describes a year for a given company. And as you can probably guess, these numbers are also in millions. So 386017
means $386,017,000,000
or $386B
for short.
If you're wondering why to store these numbers in millions, the answer is pretty simple: it's easier to read. Just check out Apple's page on Seekingalpha, for example:
The metrics
table is very similar to income_statements
:
company_id | year | gross_margin | profit_margin | pe_ratio |
---|---|---|---|---|
1 | 2022 | 0.43 | 0.26 | 2432 |
2 | 2022 | 0.68 | 0.34 | 2851 |
Each metric has its own column, and each row represents a year for a given company. Most metrics are percentage values stored as decimals. The pe_ratio
stands for "price/earnings ratio." If a company's share trades at $260 and its earnings are $20 per share, then the P/E ratio is 13.00. It's a decimal number stored as an integer.
Maybe you're asking "why not call it price_per_earnings_ratio?" It's a good question! In my opinion, our goal as software developers should be to write code that is as close to the business language as possible. But in the financial sector, nobody calls it "price per earnings ratio." It's just the "PE ratio." So, in fact, this is the correct language, in my opinion.
API
We want to implement three APIs.
GET /companies/{company}
It'll return the basic company profile:
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
It'll also return the price and market cap data in human-readable formats.
GET /companies/{company}/income-statements
It returns the income statements grouped by items and years:
{
"data": {
"years": [
2022,
2021
],
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
},
"eps": {
"2022": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
},
"2021": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
}
}
}
}
The right data structure will heavily depend on the exact use case and UI. This structure is pretty good for a layout similar to Seekingalpha's (the screenshot from earlier). This API also formats the values.
GET /companies/{company}/metrics
This is the API that returns the metrics:
{
"data": {
"years": [
2022
],
"gross_margin": {
"2022": {
"value": 0.43,
"formatted": "43.00%",
"top_line": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"bottom_line": {
"value": 167231000000,
"millions": 167231,
"formatted": "167,231"
}
}
},
"pe_ratio": {
"2022": {
"value": "24.32"
}
}
}
}
Each margin contains the top and bottom line information as well. In the case of gross margin, the top line is the revenue and the bottom line is the gross profit.
Identifying Value Objects
Now that we've seen the database and the API, it's time to define the value objects. If you take a closer look at the JSON you can identify five different kinds of values:
- Ratio. It's a simple number expressed as a float. Right now, the PE ratio is the only ratio-type data in the app.
- Margin. It has a raw value, a percentage, a top line, and a bottom-line value. Gross margin, operating margin, and profit_margin will use this data type.
- Price. It has a cent, dollar, and formatted value. Both
price_per_share
andeps
(which is earnings per share) use this data type. - Market Cap. It's a unique one because it has three different formats:
2.42T
,242B
, and577M
. All of these are valid numbers to express a company's market capitalization. When a company hits the trillion mark we don't want to use1000B
but rather1T
. SO we need to handle these cases. - Millions. Every item in the income statement is expressed as millions so it makes sense to use a value object called
Millions
.
Now, take a look at these value object names! We're working on a financial app, and we'll have classes like Millions
, Margin
, or MarketCap
.
This is the kind of codebase that makes sense. Even after five years.
Implementing Value Objects
Price
Price seems the most obvious so let's start with that one. The class itself is pretty straightforward:
class Price
{
public readonly int $cent;
public readonly float $dollar;
public readonly string $formatted;
public function __construct(int $cent)
{
$this->cent = $cent;
$this->dollar = $cent / 100;
$this->formatted = '$' . number_format($this->dollar, 2);
}
public static function from(int $cent): self
{
return new self($cent);
}
}
Several important things:
- Every value object has
public readonly
properties.readonly
makes sure they are immutable, whilepublic
makes them easy to access, so we don't need to write getters or setters. - A lot of value object has a
from
factory function. It fits the overall style of Laravel very well.
This object can be used like this:
$company = Company::first();
$price = Price::from($company->price_per_share);
The next question is: how do we use this object? There are two paths we can take:
- Casting the values on the Model's level.
- Or casting them on the API's level.
Casting in models
We have at least two possible solutions to cast attributes to value objects in the models.
Using attribute accessors:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Company extends Model
{
public function pricePerShare(): Attribute
{
return Attribute::make(
get: fn (int $value) => Price::from($value)
);
}
}
It's an excellent solution and can work 95% of the time. However, right we are in the remaining 5% because we have 10+ attributes we want to cast. In the IncomeStatement
model we need to cast almost every attribute to a Millions
instance. Just imagine how the class would look like with attribute accessors:
namespace App\Models;
class IncomeStatement extends Model
{
public function pricePerShare(): Attribute
{
return Attribute::make(
get: fn (int $value) => Millions::from($value)
);
}
/* same code here */
public function costOfRevenue(): Attribute {}
/* same code here */
public function grossProfit(): Attribute {}
/* same code here */
public function operatingExpenses(): Attribute {}
// 8 more methods here
}
So in our case, using attribute accessors is not optimal. Fortunately, Laravel has a solution for us! We can extract the casting logic into a separate Cast
class:
namespace App\Models\Casts;
use App\ValueObjects\Price;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class PriceCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return Price::from($value);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
This class does the same thing as the attribute accessor:
get
is called when you access a property from the model and it transforms the integer into aPrice
object.set
is called when you set a property in the model before you save it. It should transform aPrice
object into an integer. But as you can see, I just left it as is because we don't need this for the example. If you return$value
from the set method, Laravel won't do any extra work. So there's no attribute mutation.
The last step is to actually use this Cast
inside the Company
model:
class Company extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'price_per_share' => PriceCast::class,
];
}
Now we can use it like this:
$company = Company::first();
// This is when the PriceCast::get() will be executed
$pricePerShare = $company->price_per_share;
// $127.89
echo $pricePerShare->formatted;
// 127.89
echo $pricePerShare->dollar;
// 12789
echo $pricePerShare->cent;
Where are we going to use them? In resources, for example:
namespace App\Http\Resources;
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'ticker' => $this->ticker,
'name' => $this->name,
'price_per_share' => $this->price_per_share,
'market_cap' => $this->market_cap,
];
}
}
Since these value objects contain only public properties Laravel will automatically transform them into arrays when converting the response into JSON. So this resource will result in the following JSON response:
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
This is how we can cast values in Eloquent models. But we can skip this setup and cast the values directly inside resources.
Casting in resources
This is much more simple than the previous one. All we need to do is create a Price
object inside the resource:
namespace App\Http\Resources;
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'ticker' => $this->ticker,
'name' => $this->name,
'price_per_share' => Price::from($this->price_per_share),
'market_cap' => MarketCap::from($this->market_cap),
];
}
}
Now the Company
model does not have any casts, so we just instantiate a Price
and a MarketCap
object from the integer values.
How to choose between the two?
- To be honest, it's hard to tell without a concrete use case.
- However, if you only need these values in the API, then maybe you can skip the whole
Cast
thing and just create a value object in resources. - But if you need these values to handle other use-cases as well it's more convenient to use Eloquent casts. Some examples:
- Notifications. For example, a new income statement just came out, and you want to notify your users and include some key values in the e-mail. Another example can be a price notification.
- Queue jobs. For example, you need to recalculate price-dependent metrics and values on a scheduled basis.
- Broadcasting via websocket. For example, the price is updated in real-time on the FE.
- Each of these scenarios can benefit from using Eloquent
Cast
because otherwise you end instantiating these value objects in every place.
- In general, I think it's a good idea to use these objects in models. It makes your codebase more high-level, and easier to maintain.
So I'm going to use Eloquent Cast
to handle the casting.
MarketCap
As discussed earlier, the market cap is a bit more unique, so it has its own value object. We need this data structure:
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
The formatted
property will change based on the market cap of the company, for example:
"market_cap": {
"millions": 204100,
"formatted": "204.1B"
}
And the last case:
"market_cap": {
"millions": 172,
"formatted": "172M"
}
This is what the class looks like:
namespace App\ValueObjects;
class MarketCap
{
public readonly int $millions;
public readonly string $formatted;
public function __construct(int $millions)
{
$this->millions = $millions;
// Trillions
if ($millions >= 1_000_000) {
$this->formatted = number_format(
$this->millions / 1_000_000, 2
) . 'T';
}
// Billions
if ($millions < 1_000_000 && $millions >= 1_000) {
$this->formatted = number_format(
$this->millions / 1_000, 1
) . 'B';
}
// Millions
if ($millions < 1_000) {
$this->formatted = number_format($this->millions) . 'M';
}
}
public static function from(int $millions): self
{
return new self($millions);
}
}
We need to check the value of $millions
and do the appropriate division and use the right suffix.
The cast is almost identical to PriceCast
:
namespace App\Models\Casts;
class MarketCapCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return MarketCap::from($value);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
Once again, we don't need to do anything in set
. The last thing is to use this cast:
namespace App\Models;
class Company extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'price_per_share' => PriceCast::class,
'market_cap' => MarketCapCast::class,
];
}
I won't list the other Cast
classes because all of them are the same. You can check them out in the repository.
Millions
This value object is pretty simple:
namespace App\ValueObjects;
class Millions
{
public readonly int $value;
public readonly int $millions;
public readonly string $formatted;
public function __construct(int $millions)
{
$this->value = $millions * 1_000_000;
$this->millions = $millions;
$this->formatted = number_format($this->millions, 0, ',');
}
public static function from(int $millions): self
{
return new self($millions);
}
}
There are three properties:
value
contains the raw number as an integer.millions
contains the number expressed in millions.formatted
contains the formatted number, something like192,557
As JSON:
"revenue": {
"2022": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
}
}
Millions
is used in the IncomeStatement
model, and this is where we benefit from using Eloquent Casts
:
namespace App\Models;
class IncomeStatement extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'revenue' => MillionsCast::class,
'cost_of_revenue' => MillionsCast::class,
'gross_profit' => MillionsCast::class,
'operating_expenses' => MillionsCast::class,
'operating_profit' => MillionsCast::class,
'interest_expense' => MillionsCast::class,
'income_tax_expense' => MillionsCast::class,
'net_income' => MillionsCast::class,
'eps' => PriceCast::class,
];
}
Margin
It's also a fairly simple class:
namespace App\ValueObjects;
class Margin
{
public readonly float $value;
public readonly string $formatted;
public readonly Millions $top_line;
public readonly Millions $bottom_line;
public function __construct(
float $value,
Millions $topLine,
Millions $bottomLine
) {
$this->value = $value;
$this->top_line = $topLine;
$this->bottom_line = $bottomLine;
$this->formatted = number_format($value * 100, 2) . '%';
}
public static function make(
float $value,
Millions $topLine,
Millions $bottomLine
): self {
return new self($value, $topLine, $bottomLine);
}
}
This shows another great feature of value objects: they can be nested. In this example, the top_line
and bottom_line
attributes are Millions
instances. These numbers describe how the margin is calculated. For example, the gross margin is calculated by dividing the revenue (top line) by the gross profit (bottom line). This will look like this in JSON:
"gross_margin": {
"2022": {
"value": 0.68,
"formatted": "68.00%",
"top_line": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
},
"bottom_line": {
"value": 132345000000,
"millions": 132345,
"formatted": "132,345"
}
}
}
However, if you take a look at the make
method, you can see we expect two additional parameters: $topLine
and $bottomLine
. This means we can use this object like this:
$company = Company::first();
$incomeStatement = $company->income_statements()
->where('year', 2022)
->first();
$metrics = $company->metrics()->where('year', 2022)->first();
$grossMargin = Margin::make(
$metrics->gross_margin,
$incomeStatement->revenue,
$incomeStatement->gross_profit,
);
Since we are using Eloquent Casts
we need the revenue and gross profit (in this specific example) in the MarginCast
class. We can do something like this:
namespace App\Models\Casts;
class MarginCast implements CastsAttributes
{
/**
* @param Metric $model
*/
public function get($model, $key, $value, $attributes)
{
$incomeStatement = $model
->company
->income_statements()
->where('year', $model->year)
->first();
[$topLine, $bottomLine] = $model->getTopAndBottomLine(
$incomeStatement,
$key,
);
return Margin::make($value, $topLine, $bottomLine);
}
public function set($model, $key, $value, $attributes)
{
return $value;
}
}
As you can see, the model, in this case, is a Metric
model (this is where the cast will be used) so we can query the appropriate income statement for the same year. After that, we need a method that can return the top and bottom line for a particular metric:
namespace App\Models;
class Metric extends Model
{
public function getTopAndBottomLine(
IncomeStatement $incomeStatement,
string $metricName
): array {
return match ($metricName) {
'gross_margin' => [
$incomeStatement->revenue,
$incomeStatement->gross_profit
],
'operating_margin' => [
$incomeStatement->revenue,
$incomeStatement->operating_profit
],
'profit_margin' => [
$incomeStatement->revenue,
$incomeStatement->net_income
],
};
}
}
This method simply returns the right items from the income statement based on the metric. The logic is quite simple, but it's much more complicated than the other ones, so I recommend you to check out the source code and open these classes.
You may be asking: "Wait a minute... We are querying companies and income statements in the MarginCast for every attribute??? That's like 10 extra queries every time we query a simple Metric, right?"
Good question! The answer is: nope. These casts are lazily executed. This means the get
function will only be executed when you actually access the given property. But as you might already guess we'll access every property in a resource, so a bunch of extra queries will be executed. What can we do about it?
- Eager load relationships when querying a metric. This will prevent us from running into N+1 query problems.
- Cache the income statements. After all, they are historical data, updated once a year. This will also prevent extra queries.
- If performance is still an issue, you can drop the whole
MarginCast
class, and use the object in the resource directly. In this case, you have more flexibility. For example, you can query every important data in one query, and only interact with collections when determining the top and bottom line values.
PeRatio
After all of these complications, let's see the last and probably most simple VO:
namespace App\ValueObjects;
class PeRatio
{
public readonly string $value;
public function __construct(int $peRatio)
{
$this->value = number_format($peRatio / 100, 2);
}
public static function from(int $peRatio): self
{
return new self($peRatio);
}
}
This class can also be used to cover other ratio-type numbers, but right now PE is the only one, so I decided to call the class PeRatio
.
Income Statement Summary
Now that we have all the value objects, we can move on to the resource. Our goal is to get a summary view of the income statements of the company. This is the JSON structure:
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
}
}
}
There are at least two ways we can approach this problem:
- A more "static" approach
- And a more "dynamic" one
By "dynamic," I mean something like this:
class IncomeStatementResource
{
public $preserveKeys = true;
public function toArray(Request $request)
{
$data = [];
// $this is a Company
$data['years'] = $this->income_statements->pluck('year');
foreach ($this->income_statements as $incomeStatement) {
foreach ($incomeStatement->getAttributes() as $attribute => $value) {
$notRelated = [
'id', 'year', 'company_id',
'created_at', 'updated_at',
];
if (in_array($attribute, $notRelated)) {
continue;
}
Arr::set(
$data,
"items.{$attribute}.{$incomeStatement->year}",
$incomeStatement->{$attribute}
);
}
}
return $data;
}
}
Are you having a hard time understanding what's going on? It's not your fault! It's mine. This code sucks. I mean, it's very "dynamic" so it'll work no matter if you have four columns in the income_statements
or 15. But other than that it seems a bit funky to me. Moreover, it has no "real" form, so it's very weird to put it in a resource.
Don't get me wrong, sometimes you just need solutions like this. But an income statement has a finite amount of items (columns), and it's not something that is subject to change.
Let's see a more declarative approach:
namespace App\Http\Resources;
class IncomeStatementsSummaryResource extends JsonResource
{
public $preserveKeys = true;
public function toArray($request)
{
// $this is a Collection<IncomeStatement>
$years = $this->pluck('year');
return [
'years' => $years,
'items' => [
'revenue' => $this->getItem(
'revenue',
$years
),
'cost_of_revenue' => $this->getItem(
'cost_of_revenue',
$years
),
'gross_profit' => $this->getItem(
'gross_profit',
$years
),
'operating_expenses' => $this->getItem(
'operating_expenses',
$years
),
'operating_profit' => $this->getItem(
'operating_profit',
$years
),
'interest_expense' => $this->getItem(
'interest_expense',
$years
),
'income_tax_expense' => $this->getItem(
'income_tax_expense',
$years
),
'net_income' => $this->getItem(
'net_income',
$years
),
'eps' => $this->getItem(
'eps',
$years
),
]
];
}
/**
* @return array<int, int>
*/
private function getItem(
string $name,
Collection $years
): array {
$data = [];
foreach ($years as $year) {
$data[$year] = $this
->where('year', $year)
->first()
->{$name};
}
return $data;
}
}
Can you see the difference? It's easy to understand, readable has a real form, and does not require more code at all (all right, in this PDF it seems much longer, but in the repository, each item is one line). However, it's called IncomeStatementsSummaryResource
, and there's a reason why. This resource requires a Collection<IncomeStatement>
so it can be used like this:
namespace App\Http\Controllers;
class IncomeStatementController extends Controller
{
public function index(Company $company)
{
return IncomeStatementsSummaryResource::make(
$company->income_statements
);
}
}
We pass all the income statements of a company as a Collection. So this line in the resource won't run additional queries:
// $this->where() is a Collection method
$data[$year] = $this->where('year', $year)->first()->{$name};
The last important thing is this line here:
public $preserveKeys = true;
Without this Laravel will override the array keys and it'll convert the years to standard zero-based array indices:
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": [
{
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
{
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
]
}
}
As you can see the year-based object becomes a JSON array. This is why I used the $preserveKeys
property from the parent JsonResource
class.
Metrics Summary
The metrics summary API is basically the same as the income statement. So not surprisingly the Resource looks almost the same:
namespace App\Http\Resources;
class MetricsSummaryResource extends JsonResource
{
public $preserveKeys = true;
public function toArray($request)
{
$years = $this->pluck('year');
return [
'years' => $years,
'items' => [
'gross_margin' => $this->getItem(
'gross_margin',
$years
),
'operating_margin' => $this->getItem(
'operating_margin',
$years
),
'profit_margin' => $this->getItem(
'profit_margin',
$years
),
'pe_ratio' => $this->getItem(
'pe_ratio',
$years
),
]
];
}
private function getItem(
string $name,
Collection $years
): array {
$data = [];
foreach ($years as $year) {
$data[$year] = $this
->where('year', $year)
->first()
->{$name};
}
return $data;
}
}
Can be used like this:
namespace App\Http\Controllers;
class MetricController extends Controller
{
public function index(Company $company)
{
return MetricsSummaryResource::make($company->metrics);
}
}
Conclusion
It was a longer exclusive, I know. Give it some time, maybe read it again later.
Value objects are awesome, in my opinion! I almost use them in every project, no matter if it's old, new, DDD, or not DDD, legacy, or not. It's pretty easy to start using them, and you'll have a very high-level, declarative codebase.
I often got the question: "what else can be expressed as a value object?" Almost anything, to name a few examples:
- Addresses. In an e-commerce application where you have to deal with shipping, it can be beneficial to use objects instead of strings. You can express each part of an address as a property:
- City
- ZIP code
- Line 1
- Line 2
- Numbers and percents. As we've seen.
- Email addresses.
- Name. With parts like first, last middle, title
- Any measurement unit, such as weight, temperature, distance
- GPS coordinates.
- EndDate and StartDate. They can be created from a Carbon but ensure that a StartDate is always at 00:00:00 meanwhile an EndDate is always at 23:59:59.
- Any other application-specific concepts.
This whole article comes from my new 250-page book Laravel Concepts. Check it out: