| name | acc-create-domain-service |
| description | Generates DDD Domain Services for PHP 8.5. Creates stateless services for business logic that doesn't belong to entities or value objects. Includes unit tests. |
Domain Service Generator
Generate DDD-compliant Domain Services for business operations spanning multiple aggregates or requiring external coordination.
Domain Service Characteristics
- Stateless: No internal state, operates on passed arguments
- Domain Logic: Contains business rules that don't fit in entities
- Cross-Aggregate: Coordinates multiple aggregates
- Named by Domain Operation: Verb-based naming (e.g., TransferMoney, CalculateShipping)
- No Infrastructure: Pure domain logic, no DB/HTTP calls
- Immutable Dependencies: Uses repository interfaces, not implementations
When to Use Domain Service
| Scenario | Example |
|---|
| Operation spans multiple aggregates | MoneyTransfer between accounts |
| Complex business calculation | PricingCalculator, TaxCalculator |
| Domain policy enforcement | PasswordPolicy, OrderPolicy |
| Stateless transformation | CurrencyConverter |
| Aggregate coordination | OrderFulfillmentService |
Template
<?php
declare(strict_types=1);
namespace Domain\{BoundedContext}\Service;
use Domain\{BoundedContext}\Entity\{Entity};
use Domain\{BoundedContext}\ValueObject\{ValueObjects};
use Domain\{BoundedContext}\Repository\{RepositoryInterfaces};
use Domain\{BoundedContext}\Exception\{DomainExceptions};
final readonly class {Name}Service
{
public function __construct(
{repositoryDependencies}
) {}
public function {operation}({parameters}): {ReturnType}
{
{domainLogic}
}
{privateMethods}
}
Examples
Money Transfer Service
<?php
declare(strict_types=1);
namespace Domain\Banking\Service;
use Domain\Banking\Entity\Account;
use Domain\Banking\ValueObject\Money;
use Domain\Banking\Repository\AccountRepositoryInterface;
use Domain\Banking\Exception\InsufficientFundsException;
use Domain\Banking\Exception\SameAccountTransferException;
final readonly class MoneyTransferService
{
public function __construct(
private AccountRepositoryInterface $accounts
) {}
public function transfer(
Account $source,
Account $destination,
Money
): {
(->()->(->())) {
();
}
(!->()) {
(->(), );
}
->();
->();
}
}
Pricing Calculator Service
<?php
declare(strict_types=1);
namespace Domain\Pricing\Service;
use Domain\Pricing\ValueObject\Money;
use Domain\Pricing\ValueObject\Discount;
use Domain\Pricing\ValueObject\TaxRate;
use Domain\Order\Entity\Order;
use Domain\Customer\Entity\Customer;
final readonly class PricingCalculatorService
{
public function calculateTotal(
Order $order,
Customer $customer,
?Discount $discount = null
): Money {
$subtotal = $this->calculateSubtotal($order);
$discounted = $this->applyDiscount($subtotal, , );
= ->(, ->());
;
}
{
->()->(
fn(Money , OrderItem ) => ->(
->()->(->())
),
::(->())
);
}
{
( === ) {
;
}
(!->()) {
;
}
->();
}
{
= ::(->());
->(->(->()));
}
}
Password Policy Service
<?php
declare(strict_types=1);
namespace Domain\User\Service;
use Domain\User\ValueObject\Password;
use Domain\User\ValueObject\PasswordStrength;
use Domain\User\Exception\WeakPasswordException;
final readonly class PasswordPolicyService
{
private const MIN_LENGTH = 8;
private const REQUIRED_STRENGTH = PasswordStrength::Strong;
public function validate(Password $password): void
{
$violations = [];
if ($password->length() < self::MIN_LENGTH) {
$violations[] = "Password must be at least " . self::MIN_LENGTH . " characters";
}
(!->()) {
[] = ;
}
(!->()) {
[] = ;
}
(!->()) {
[] = ;
}
(!->()) {
[] = ;
}
(->()->(::)) {
[] = . ::->value;
}
( !== []) {
();
}
}
{
= ;
(->() >= ) += ;
(->() >= ) += ;
(->()) += ;
(->()) += ;
(->()) += ;
(->()) += ;
() {
>= => ::,
>= => ::,
=> ::,
};
}
}
Test Template
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\{BoundedContext}\Service;
use Domain\{BoundedContext}\Service\{Name}Service;
use Domain\{BoundedContext}\Entity\{Entity};
use Domain\{BoundedContext}\ValueObject\{ValueObject};
use Domain\{BoundedContext}\Exception\{DomainException};
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass({Name}Service::class)]
final class {Name}ServiceTest extends TestCase
{
{Name}Service ;
{
->service = {Name}(
{mockDependencies}
);
}
{Operation}():
{
{arrange}
= ->service->{operation}({parameters});
{assert}
}
{Operation}ThrowsOn{Condition}():
{
{arrange}
->({}::);
->service->{operation}({invalidParameters});
}
{additionalTests}
}
Example Test
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Banking\Service;
use Domain\Banking\Service\MoneyTransferService;
use Domain\Banking\Entity\Account;
use Domain\Banking\ValueObject\AccountId;
use Domain\Banking\ValueObject\Money;
use Domain\Banking\Exception\InsufficientFundsException;
use Domain\Banking\Exception\SameAccountTransferException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass(::)
{
MoneyTransferService ;
{
->service = (
->(::)
);
}
{
= ->(::());
= ->(::());
= ::();
->service->(, , );
::(->()->(::()));
::(->()->(::()));
}
{
= ->(::());
= ->(::());
= ::();
->(::);
->service->(, , );
}
{
= ->(::());
->(::);
->service->(, , ::());
}
{
= (::());
->();
;
}
}
Naming Conventions
| Pattern | Example |
|---|
| Service | {Operation}Service |
| Method | {verb}{noun} |
| Exception | {Condition}Exception |
| Test | {ServiceName}Test |
File Placement
| Component | Path |
|---|
| Domain Service | src/Domain/{BoundedContext}/Service/ |
| Exceptions | src/Domain/{BoundedContext}/Exception/ |
| Unit Tests | tests/Unit/Domain/{BoundedContext}/Service/ |
Anti-patterns to Avoid
| Anti-pattern | Problem | Solution |
|---|
| Anemic Service | Just delegates to entities | Move logic to entities |
| Infrastructure in Service | DB/HTTP calls | Use repository interfaces |
| Stateful Service | Maintains internal state | Make stateless |
| God Service | Too many responsibilities | Split into focused services |
| Business Logic in Constructors | Complex setup | Keep constructors simple |