Builder Pattern in PHP/Laravel: Building Clean and Flexible Order Objects

In practice, orders in e-commerce systems often evolve over time, which makes them a perfect candidate for the Builder Pattern.

Creating orders in an e-commerce application often seems straightforward at first: a customer selects items, adds them to a cart, and checks out. But as your application grows, the Order class can quickly become complex. You might need to handle optional discounts, shipping options, gift wrapping, different payment methods, and more. Before long, your constructor looks like this:

$order = new Order(
    $customerId,
    $items,
    $paymentMethod,
    $shippingAddress,
    $discountCode,
    $giftWrap = true,
    $specialInstructions = null
);
PHP

Not only is it hard to read, but it’s also easy to make mistakes when creating orders. Because of this, the Builder Pattern becomes a natural solution. It lets you construct objects step by step, handling optional parameters elegantly while keeping your code readable and maintainable.

In this article, we’ll explore how to apply the Builder Pattern to an Order class in PHP/Laravel, improving readability, testability, and flexibility.

Previous article in this category: https://codecraftdiary.com/2026/01/17/factory-method-in-php/


Why the Builder Pattern Works for Orders

The Builder Pattern is especially useful when:

  • A class has many optional parameters
  • The class requires complex setup or validation
  • You want to avoid long constructors
  • You need clear, readable, fluent code

In e-commerce, orders often fit all these criteria. By using a builder, we can separate the construction logic from the order behavior, making it easier to extend and maintain.


Step 1: Designing the Order Class

Let’s start with a simple Order class that represents a finalized order object. We’ll assume that once an order is created, it’s immutable:

class Order
{
    public function __construct(
        public int $customerId,
        public array $items,
        public string $paymentMethod,
        public string $shippingAddress,
        public ?string $discountCode = null,
        public bool $giftWrap = false,
        public ?string $specialInstructions = null
    ) {}
}
PHP

However, the constructor still takes multiple parameters, some of them optional. While this works, it doesn’t scale well if more options are added in the future.


Step 2: Creating the Order Builder

We can create an OrderBuilder class to handle object creation step by step:

class OrderBuilder
{
    private int $customerId;
    private array $items = [];
    private string $paymentMethod = 'credit_card';
    private string $shippingAddress = '';
    private ?string $discountCode = null;
    private bool $giftWrap = false;
    private ?string $specialInstructions = null;

    public static function create(): self
    {
        return new self();
    }

    public function customerId(int $customerId): self
    {
        $this->customerId = $customerId;
        return $this;
    }

    public function addItem(array $item): self
    {
        $this->items[] = $item;
        return $this;
    }

    public function paymentMethod(string $method): self
    {
        $this->paymentMethod = $method;
        return $this;
    }

    public function shippingAddress(string $address): self
    {
        $this->shippingAddress = $address;
        return $this;
    }

    public function discountCode(string $code): self
    {
        $this->discountCode = $code;
        return $this;
    }

    public function giftWrap(bool $flag): self
    {
        $this->giftWrap = $flag;
        return $this;
    }

    public function specialInstructions(string $instructions): self
    {
        $this->specialInstructions = $instructions;
        return $this;
    }

    public function build(): Order
    {
        if (empty($this->customerId)) {
            throw new \InvalidArgumentException('Customer ID is required.');
        }

        if (empty($this->items)) {
            throw new \InvalidArgumentException('At least one item is required.');
        }

        if (empty($this->shippingAddress)) {
            throw new \InvalidArgumentException('Shipping address is required.');
        }

        return new Order(
            $this->customerId,
            $this->items,
            $this->paymentMethod,
            $this->shippingAddress,
            $this->discountCode,
            $this->giftWrap,
            $this->specialInstructions
        );
    }
}
PHP

Notice a few key things:

  • Fluent interface: Each setter returns $this, allowing chained calls.
  • Validation in build(): Mandatory fields are checked before creating the Order.
  • Stepwise construction: You can add items or set options one by one.

Step 3: Using the Builder

Here’s how creating an order looks without a builder:

$order = new Order(
    123,
    [['product_id' => 1, 'quantity' => 2], ['product_id' => 5, 'quantity' => 1]],
    'paypal',
    '123 Main Street, City, Country',
    'DISCOUNT10',
    true,
    'Leave at the front door'
);
PHP

As a result, it’s hard to read and error-prone.

With the builder, it becomes:

$order = OrderBuilder::create()
    ->customerId(123)
    ->addItem(['product_id' => 1, 'quantity' => 2])
    ->addItem(['product_id' => 5, 'quantity' => 1])
    ->paymentMethod('paypal')
    ->shippingAddress('123 Main Street, City, Country')
    ->discountCode('DISCOUNT10')
    ->giftWrap(true)
    ->specialInstructions('Leave at the front door')
    ->build();
PHP
  • Much cleaner and readable.
  • Easy to skip optional fields or add more items.
  • Validation ensures that you set all required fields.

Step 4: Integrating with Laravel Jobs or Services

In Laravel, orders are often processed asynchronously using Jobs or Services. Builders make this easier:

$order = OrderBuilder::create()
    ->customerId($user->id)
    ->addItem(['product_id' => $product->id, 'quantity' => 1])
    ->shippingAddress($user->address)
    ->build();

dispatch(new ProcessOrderJob($order));
PHP

You can also create reusable templates for common order setups:

class OrderBuilderFactory
{
    public static function giftOrder(int $customerId, array $items, string $address): OrderBuilder
    {
        return OrderBuilder::create()
            ->customerId($customerId)
            ->shippingAddress($address)
            ->giftWrap(true)
            ->paymentMethod('credit_card')
            ->addItem(...$items);
    }
}

$order = OrderBuilderFactory::giftOrder($user->id, $items, $user->address)->build();
PHP

This pattern is perfect when you have standardized order types, like gifts or bulk orders.


Step 5: Advantages of Using the Builder Pattern

  1. Readability: The creation of orders reads like a series of instructions.
  2. Validation: All required fields are checked in one place.
  3. Flexibility: Optional fields can be skipped without affecting the rest.
  4. Testability: Builders can be tested independently from the Order class.
  5. Extensibility: Adding new optional features (e.g., gift wrap, promo codes) doesn’t break existing code.

Step 6: When Not to Use a Builder

You don’t always need builders. Avoid using them if:

  • The class only has 2–3 parameters.
  • Objects are simple DTOs without optional parameters.
  • Fluent APIs add unnecessary complexity.

For small objects, a simple constructor or named constructors may suffice.


Step 7: Summary

The Builder Pattern is a simple yet powerful way to manage complex object creation in PHP and Laravel. By separating construction from the object itself, your code becomes readable, maintainable, and testable.

For an Order class with multiple optional features, the builder provides a clear, step-by-step way to create instances without dealing with confusing long constructors.

Key takeaways:

  • Use the builder for complex or optional-heavy objects.
  • Keep validation in the build() method.
  • Leverage fluent interfaces for readability.
  • Consider factory methods for reusable templates.

With the Builder Pattern, your order creation code can grow with your application—without turning into a mess of parameters and confusion.

Leave a Reply

Your email address will not be published. Required fields are marked *