You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/How-to-use-Specifications.md
+52-92Lines changed: 52 additions & 92 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -323,97 +323,7 @@ foreach ($ordersToCollect as $orderReadModel) {
323
323
324
324
---
325
325
326
-
## Approach 3: Specifications Inside the Aggregate
327
-
328
-
Some DDD practitioners argue that complex business rules should live entirely inside the aggregate. The specification pattern is not used externally; instead, the aggregate exposes a single method that encapsulates the rule.
329
-
330
-
**Pros:**
331
-
332
-
- All business logic in one place
333
-
- Maximum encapsulation
334
-
- Easy to understand and maintain for simple cases
335
-
336
-
**Cons:**
337
-
338
-
- Loses composability of specifications
339
-
- Cannot easily reuse partial rules
340
-
- Aggregate becomes responsible for query logic
341
-
342
-
### Example
343
-
344
-
```php
345
-
<?php
346
-
347
-
declare(strict_types=1);
348
-
349
-
namespace App\Domain\Invoice;
350
-
351
-
/**
352
-
* Invoice aggregate with internal business rule
353
-
*/
354
-
class Invoice
355
-
{
356
-
private float $amount;
357
-
private \DateTimeImmutable $dueDate;
358
-
private bool $noticeSent;
359
-
private bool $inCollection;
360
-
361
-
public function __construct(
362
-
float $amount,
363
-
\DateTimeImmutable $dueDate
364
-
) {
365
-
$this->amount = $amount;
366
-
$this->dueDate = $dueDate;
367
-
$this->noticeSent = false;
368
-
$this->inCollection = false;
369
-
}
370
-
371
-
/**
372
-
* Business rule encapsulated inside the aggregate
373
-
*/
374
-
public function shouldBeSentToCollection(\DateTimeImmutable $now): bool
375
-
{
376
-
return $this->isOverdue($now)
377
-
&& $this->noticeSent
378
-
&& !$this->inCollection;
379
-
}
380
-
381
-
private function isOverdue(\DateTimeImmutable $now): bool
382
-
{
383
-
return $this->dueDate < $now;
384
-
}
385
-
386
-
public function markNoticeSent(): void
387
-
{
388
-
$this->noticeSent = true;
389
-
}
390
-
391
-
public function sendToCollection(): void
392
-
{
393
-
if (!$this->shouldBeSentToCollection(new \DateTimeImmutable())) {
394
-
throw new \DomainException('Invoice cannot be sent to collection');
395
-
}
396
-
397
-
$this->inCollection = true;
398
-
}
399
-
}
400
-
```
401
-
402
-
### Usage
403
-
404
-
```php
405
-
$now = new \DateTimeImmutable();
406
-
407
-
foreach ($invoices as $invoice) {
408
-
if ($invoice->shouldBeSentToCollection($now)) {
409
-
$invoice->sendToCollection();
410
-
}
411
-
}
412
-
```
413
-
414
-
---
415
-
416
-
## Approach 4: Pragmatic Property Access
326
+
## Approach 3: Pragmatic Property Access
417
327
418
328
Many real-world implementations take a pragmatic stance: specifications are used for query/filtering logic where exposing read-only properties is acceptable. The aggregate's invariants and mutation logic remain protected, but query-related properties can be exposed via getters or public readonly properties.
419
329
@@ -556,6 +466,56 @@ foreach ($orders as $order) {
556
466
}
557
467
```
558
468
469
+
## Approach 4: Double Dispatch (The Purist Approach)
470
+
471
+
The aggregate remains completely "blind" to its properties from the outside. Instead, the aggregate accepts the specification and "feeds" it the necessary data through a specific internal interface.
472
+
473
+
**Pros:**
474
+
475
+
- Zero leakage: Properties remain private and no getters are created.
476
+
- The aggregate controls exactly what data the specification is allowed to see.
477
+
478
+
**Cons:**
479
+
480
+
- Requires a custom interface for the specification.
481
+
- Slightly higher cognitive complexity.
482
+
483
+
```php
484
+
namespace App\Domain\Order;
485
+
486
+
/**
487
+
* Interface that defines what data an Order Spec is allowed to see
488
+
*/
489
+
interface OrderDataInterface {
490
+
public function getTotalAmount(): float;
491
+
public function getDueDate(): \DateTimeImmutable;
492
+
}
493
+
494
+
class Order implements OrderDataInterface
495
+
{
496
+
private float $totalAmount;
497
+
private \DateTimeImmutable $dueDate;
498
+
499
+
// The aggregate "accepts" the spec and passes itself as the data provider
500
+
public function satisfies(OrderSpecificationInterface $spec): bool
501
+
{
502
+
return $spec->isSatisfiedByOrder($this);
503
+
}
504
+
505
+
public function getTotalAmount(): float { return $this->totalAmount; }
506
+
public function getDueDate(): \DateTimeImmutable { return $this->dueDate; }
507
+
}
508
+
509
+
// The Specification now depends on the Interface, not the Aggregate
510
+
class MinimumOrderValueSpecification implements OrderSpecificationInterface
511
+
{
512
+
public function isSatisfiedByOrder(OrderDataInterface $order): bool
0 commit comments