Reason Layer
“Everything that exists has a reason for its existence”
—Leibniz, Principle of Sufficient Reason (1714)
Reason for Existence
ExpressDelivery can exist as such because it has express shipping capabilities. StandardDelivery can exist as such because it has standard shipping capabilities. This foundation for “why it can be in that existence” is the raison d’être.
The Reason Layer is a design pattern that expresses this raison d’être as a single object.
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order, // Immanence
#[Inject] ExpressShipping $reason // Reason for existence
) {
$this->fee = $reason->calculateFee($order->weight);
}
}
ExpressShipping is the raison d’être of ExpressDelivery. It provides the complete tool set needed for express delivery.
Reason as $being
When a reason object is passed as $being, it takes on an additional role. Its type serves as the basis for determining the transformation destination, while simultaneously providing the methods specific to that mode of existence.
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] ExpressShipping $being // Type determines transformation and provides express-specific methods
) {
$this->fee = $being->calculateFee($order->weight);
}
}
final readonly class StandardDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] StandardShipping $being // Type determines transformation and provides standard-specific methods
) {
$this->fee = $being->calculateFee($order->weight);
}
}
The type ExpressShipping $being itself is the reason why it becomes ExpressDelivery. The framework reads this type and automatically selects the corresponding transformation destination.
Any Reason object can serve as either #[Inject] (providing transcendent capabilities) or $being (determining destiny). The difference is not in the object itself, but in how it is used. A JTASProtocol that evaluates patients as #[Inject] in one context could determine destiny as $being in another.
Defining Reason Classes
Reason classes bundle the services necessary to realize a specific mode of existence:
namespace App\Reason;
final readonly class ExpressShipping
{
public function __construct(
private PriorityCarrier $carrier,
private RealTimeTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // Express rate
{
return $this->carrier->expressFee($weight);
}
public function guaranteeDeliveryBy(Address $address): \DateTimeImmutable // Guaranteed delivery date
{
return $this->carrier->guaranteedDate($address);
}
public function realTimeTrack(TrackingId $id): TrackingStatus // Real-time tracking
{
return $this->tracker->realTimeStatus($id);
}
}
final readonly class StandardShipping
{
public function __construct(
private RegularCarrier $carrier,
private BatchTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // Standard rate
{
return $this->carrier->standardFee($weight);
}
public function estimateDeliveryWindow(Address $address): DateRange // Estimated delivery window
{
return $this->carrier->estimateWindow($address);
}
}
Difference from Individual Injection
The Reason Layer uses #[Inject]. So how does it differ from using multiple #[Inject] attributes separately?
Individual injection:
public function __construct(
#[Input] OrderData $order,
#[Inject] PriorityCarrier $carrier,
#[Inject] RealTimeTracker $tracker,
#[Inject] InsuranceService $insurance,
#[Inject] DeliveryScheduler $scheduler
) {
// Using scattered tools individually
}
Reason Layer:
public function __construct(
#[Input] OrderData $order,
#[Inject] ExpressShipping $reason // Related tools bundled as reason for existence
) {
$this->fee = $reason->calculateFee($order->weight);
}
“What is needed to become ExpressDelivery?” — a single reason object answers that question. Objects themselves declare “what to become”, while reason objects realize “how to achieve that state”.
Reason Returning Potential
So far, Reason has returned immediate values like Fee. But consider order processing: inventory must be reserved, payment captured, and shipping arranged—and all three must succeed before any of them commit. If payment fails after inventory is reserved, that reservation must not persist.
For this kind of coordination, Reason returns a Potential—an object that holds a deferred operation, realized later via be(). Nothing is committed until every participant has been prepared successfully. This pattern is only needed when several external operations must be prepared together before any of them commits.
Potential: Prepared but Uncommitted
A Reason method prepares the external operation and returns a Potential:
final class PaymentGateway
{
public function authorize(string $cardNumber, int $amount): PaymentCapture
{
$authCode = $this->api->authorize($cardNumber, $amount);
return new PaymentCapture(
$authCode,
$amount,
fn () => $this->api->capture($authCode, $amount),
);
}
}
PaymentCapture is a Potential—it holds the authorization code and a deferred capture operation. The payment is authorized but not yet captured. Calling be() commits it:
$capture = $gateway->authorize($cardNumber, $amount);
$capture->authorizationCode; // Available immediately
$capture->be(); // Commits the capture
Moment: Holding Potential
A class that holds a Potential from Reason is called a Moment (Hegel’s 契機—an essential aspect that only makes sense as part of a whole). A Moment implements MomentInterface, a one-method interface that the application defines (the framework core does not ship it; see the order processing demo):
interface MomentInterface
{
public function be(): void;
}
final readonly class PaymentCompleted implements MomentInterface
{
public PaymentCapture $capture;
public function __construct(
#[CardNumber] public string $cardNumber, // DI qualifier attribute
#[Amount] public int $amount, // DI qualifier attribute
#[Inject] PaymentGateway $gateway,
) {
$this->capture = $gateway->authorize($cardNumber, $amount);
}
public function be(): void
{
$this->capture->be();
}
}
A Moment is built by the DI container rather than from a previous object, so its values arrive through application-defined qualifier attributes (#[CardNumber], #[Amount]) bound in the module, not through #[Input].
Convergence: Final Realizes Moments
When multiple Moments must all succeed together, a Final Object receives them via #[Inject] and calls be() on each. This is not an external command—it is self-completion:
final readonly class OrderConfirmed
{
public string $orderId;
public string $status;
public function __construct(
#[Inject] public InventoryReserved $inventory,
#[Inject] public PaymentCompleted $payment,
#[Inject] public ShippingArranged $shipping,
) {
$this->inventory->be();
$this->payment->be();
$this->shipping->be();
$this->orderId = 'ORD-' . date('Ymd') . '-' . bin2hex(random_bytes(4));
$this->status = 'confirmed';
}
}
If any Moment cannot be created (because its Reason failed), the Final Object is never constructed and nothing has been committed—no manual rollback flags, no nested try-catch for the preparation phase. If all Moments exist, be() commits each deferred operation in order. Should a later be() fail, the earlier commits remain; compensating for that belongs to the Reason (for example, a gateway that can void a capture), not to the Final Object.
When to Use This Pattern
Use Potential-returning Reason when multiple external operations must all be prepared before any of them commits. True atomicity across external services still depends on those services (a shared transaction or compensating actions); the pattern guarantees that a failed preparation commits nothing. For simple cases where Reason returns an immediate value, Potential is unnecessary.
The inability to exist is itself an existence. Learn how to handle this in Validation and Error Handling ➡️