Imagine that you are building an invoicing system for a small business. You want to ensure that the status of each invoice is always valid and that the quantities and prices of the invoice line items are always positive. One way to do this is to use value objects to represent the status of each invoice and the quantities and prices of the line items.
First, you could create a InvoiceStatus value object to represent the status of each invoice. This value object would have a private constructor and several static methods for creating different instances of the object that represent different statuses. For example, you might have a sent() method that creates an InvoiceStatus object representing an invoice that has been sent to the customer, and a paid() method that creates an object representing an invoice that has been paid by the customer. This ensures that only valid statuses can be used and provides meaning to those statuses.
Next, you could create a Quantity value object to represent the quantities of the invoice line items. This object could have a constructor that takes a quantity as an argument and ensures that the quantity is always positive. You could also create a Price value object to represent the prices of the line items in the same way.
To use these value objects in your application, you would simply pass them as arguments to your model methods or constructors instead of using native types like strings or integers. For example, to create a new invoice with a particular status, you could do something like this:
$invoice = new Invoice(1, InvoiceStatus::sent());
Similarly, to add a line item to an invoice, you could do something like this:
$invoice->addLineItem('Apple', Quantity::fromInt(5), Price::fromInt(100));
Using value objects in this way can help you enforce data integrity and provide additional context for certain values in your application.
Now let’s demonstrate another example.
To use value objects to represent shipping addresses and package weights for a shipment:
<?php namespace App\Models; final class ShippingAddress { private function __construct( private string $value ) {} public function __toString(): string { return $this->value; } public static function fromString(string $address): self { // Validate the shipping address if (!preg_match('/^\d+\s[A-Za-z0-9._%+-]+\s[A-Za-z0-9._%+-]+$/', $address)) { throw new \InvalidArgumentException("Invalid shipping address '{$address}'"); } return new self($address); } } final class PackageWeight { private function __construct( private int $value ) {} public function __toString(): string { return (string) $this->value; } public static function fromInt(int $weight): self { if ($weight <= 0) { throw new \InvalidArgumentException("Invalid package weight '{$weight}'"); } return new self($weight); } } class Shipment { public function __construct( private int $id, private ShippingAddress $shippingAddress ) {} public function addPackage(PackageWeight $packageWeight): void { // Add the package to the shipment } } // Example usage $shipment = new Shipment(1, ShippingAddress::fromString('123 Main St, Anytown USA 12345')); $shipment->addPackage(PackageWeight::fromInt(5));
In this example, the ShippingAddress and PackageWeight value objects have private constructors and static methods for creating new instances of the objects. The ShippingAddress object has a fromString() method that takes an address as an argument and returns a new ShippingAddress object, while the PackageWeight object has a fromInt() method that takes a weight as an argument and returns a new PackageWeight object. Both of these methods also include validation to ensure that only valid values are used.
The Shipment class has a constructor that takes an ID and a ShippingAddress object as arguments and a method called addPackage() that takes a PackageWeight object as an argument. To create a new shipment with a particular shipping address, you would call the ShippingAddress::fromString() method and pass the result to the Shipment constructor. Similarly, to add a package to a shipment, you would call the PackageWeight::fromInt() method and pass the result to the addPackage() method.
You might be wondering why did you put it Models directory?
Well, In the example I provided, I placed the value objects in the App\Models namespace for organization purposes. This is a common practice when working with object-oriented code, as it helps to keep related classes together and makes it easier to find and maintain them.
In a larger application, it is often useful to group related classes by their purpose or role within the application. For example, you might have a Models namespace for domain models, a Services namespace for service objects, and a Http namespace for controllers and other HTTP-related classes. Placing the value objects in the Models namespace in this way helps to reflect their role as part of the domain model and makes it clear how they fit into the overall architecture of the application.
Note that this in example of how it can be done and it might not be the best way. You can read more on wikipedia or check other expert’s opinions if you want to know more about this!
Thanks.
Comments (0)