« 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:

Laravel Value Objects

As you can see, it's quite easy. This is a sample row from the companies table:

idtickernameprice_per_sharemarket_cap
1AAPLApple Inc.149642420000
2MSFTMicrosoft Inc.273242040000

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_idyearrevenuegross_profit
12022386017167231
12021246807167231

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:

Laravel Value Objects

The metrics table is very similar to income_statements:

company_idyeargross_marginprofit_marginpe_ratio
120220.430.262432
220220.680.342851

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 and eps (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, and 577M. 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 use 1000B but rather 1T. 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, while public 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 a Price object.
  • set is called when you set a property in the model before you save it. It should transform a Price 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 like 192,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:

Laravel Concepts