Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Hexagonal Maker Bundle Laravel Package

ahmed-bhs/hexagonal-maker-bundle

View on GitHub
Deep Wiki
Context7

layout: default

Anti-Patterns and Pitfalls in Hexagonal Architecture

Table of Contents

  1. Anemic Domain Model
  2. God Objects
  3. Leaky Abstractions
  4. Repository as Service Locator
  5. Transaction Management Issues
  6. Cascade Delete Problems
  7. Over-Engineering
  8. Testing Anti-Patterns

Anemic Domain Model

The Problem

Anemic Domain Model: Entities are just data containers with getters/setters, all logic is in handlers.

// ❌ BAD: Anemic entity
class Order
{
    private OrderStatus $status;
    private Money $total;
    private \DateTimeImmutable $shippedAt;

    // Only getters and setters, no behavior
    public function getStatus(): OrderStatus { return $this->status; }
    public function setStatus(OrderStatus $status): void { $this->status = $status; }

    public function getTotal(): Money { return $this->total; }
    public function setTotal(Money $total): void { $this->total = $total; }

    public function getShippedAt(): ?\DateTimeImmutable { return $this->shippedAt; }
    public function setShippedAt(\DateTimeImmutable $shippedAt): void { $this->shippedAt = $shippedAt; }
}

// ❌ BAD: All business logic in handler
class ShipOrderHandler
{
    public function __invoke(ShipOrderCommand $command): void
    {
        $order = $this->orders->findById($command->orderId);

        // Business logic in handler (should be in entity!)
        if ($order->getStatus() === OrderStatus::CANCELLED) {
            throw new CannotShipCancelledOrderException();
        }

        if ($order->getStatus() === OrderStatus::SHIPPED) {
            throw new OrderAlreadyShippedException();
        }

        $order->setStatus(OrderStatus::SHIPPED);
        $order->setShippedAt(new \DateTimeImmutable());

        $this->orders->save($order);
    }
}

Problems:

  • Business rules scattered in handlers
  • Hard to test (need handler to test business logic)
  • Cannot reuse logic elsewhere
  • Entity is just a data bag

The Solution: Rich Domain Model

// ✅ GOOD: Rich entity with behavior
class Order
{
    private OrderStatus $status;
    private Money $total;
    private ?\DateTimeImmutable $shippedAt = null;

    // Business logic encapsulated in entity
    public function ship(): void
    {
        if ($this->status === OrderStatus::CANCELLED) {
            throw new CannotShipCancelledOrderException();
        }

        if ($this->status === OrderStatus::SHIPPED) {
            throw new OrderAlreadyShippedException();
        }

        $this->status = OrderStatus::SHIPPED;
        $this->shippedAt = new \DateTimeImmutable();
    }

    public function cancel(): void
    {
        if ($this->status === OrderStatus::SHIPPED) {
            throw new CannotCancelShippedOrderException();
        }

        $this->status = OrderStatus::CANCELLED;
    }

    // Getters only (no setters!)
    public function getStatus(): OrderStatus { return $this->status; }
    public function getTotal(): Money { return $this->total; }
    public function getShippedAt(): ?\DateTimeImmutable { return $this->shippedAt; }
}

// ✅ GOOD: Thin handler, just orchestration
class ShipOrderHandler
{
    public function __invoke(ShipOrderCommand $command): void
    {
        $order = $this->orders->findById($command->orderId);

        $order->ship(); // Business logic in entity

        $this->orders->save($order);
        $this->eventDispatcher->dispatch(new OrderShippedEvent($order->getId()));
    }
}


**Benefits:**
- Business logic in domain where it belongs
- Easy to test (`$order->ship()` can be tested without handler)

- Entity protects its invariants

---

## God Objects

### The Problem: Fat Handlers

```php
// ❌ BAD: God handler (200+ lines)
class ProcessOrderHandler
{
    public function __invoke(ProcessOrderCommand $command): void
    {
        // Input validation
        if (empty($command->items)) {
            throw new InvalidOrderException("Order must have items");
        }

        // Check customer
        $customer = $this->customers->findById($command->customerId);
        if (!$customer) {
            throw new CustomerNotFoundException($command->customerId);
        }

        if (!$customer->isActive()) {
            throw new InactiveCustomerException();
        }

        // Check inventory for each item
        foreach ($command->items as $item) {
            $product = $this->products->findById($item->productId);
            if (!$product) {
                throw new ProductNotFoundException($item->productId);
            }

            if ($product->getStock() < $item->quantity) {
                throw new InsufficientStockException($item->productId);
            }
        }

        // Calculate pricing
        $subtotal = 0;
        foreach ($command->items as $item) {
            $product = $this->products->findById($item->productId);
            $subtotal += $product->getPrice() * $item->quantity;
        }

        $tax = $subtotal * $this->taxCalculator->getTaxRate($command->shippingAddress);
        $shipping = $this->shippingCalculator->calculate($command->shippingAddress, $command->items);
        $total = $subtotal + $tax + $shipping;

        // Create order
        $order = new Order(
            OrderId::generate(),
            $command->customerId,
            $command->items,
            $subtotal,
            $tax,
            $shipping,
            $total,
            OrderStatus::PENDING,
            new \DateTimeImmutable()
        );

        // Reserve stock
        foreach ($command->items as $item) {
            $product = $this->products->findById($item->productId);
            $product->reserveStock($item->quantity);
            $this->products->save($product);
        }

        // Save order
        $this->orders->save($order);

        // Send notifications
        $this->emailSender->send(new OrderConfirmationEmail($order));
        $this->eventDispatcher->dispatch(new OrderCreatedEvent($order->getId()));

        // ... 100 more lines
    }
}


**Problems:**
- Too many responsibilities
- Hard to test

- Hard to understand

---

```php
// ✅ GOOD: Split into multiple handlers/services

// 1. Handler: orchestration only
class ProcessOrderHandler
{
    public function __construct(
        private OrderFactory $orderFactory,
        private OrderRepositoryInterface $orders,
        private InventoryService $inventory,
        private EventDispatcherInterface $events,
    ) {}

    public function __invoke(ProcessOrderCommand $command): void
    {
        // Factory handles creation + validation
        $order = $this->orderFactory->create(
            customerId: $command->customerId,
            items: $command->items,
            shippingAddress: $command->shippingAddress
        );

        // Domain service handles inventory
        $this->inventory->reserveStock($order->getItems());

        // Repository handles persistence
        $this->orders->save($order);

        // Event dispatcher handles notifications
        $this->events->dispatch(new OrderCreatedEvent($order->getId()));
    }
}

// 2. Factory: handles complex creation logic
class OrderFactory
{
    public function create(
        CustomerId $customerId,
        array $items,
        ShippingAddress $address
    ): Order {
        $this->validateCustomer($customerId);
        $this->validateItems($items);

        $subtotal = $this->calculateSubtotal($items);
        $tax = $this->taxCalculator->calculate($subtotal, $address);
        $shipping = $this->shippingCalculator->calculate($address, $items);

        return new Order(
            id: OrderId::generate(),
            customerId: $customerId,
            items: $items,
            subtotal: $subtotal,
            tax: $tax,
            shipping: $shipping,
            total: $subtotal->add($tax)->add($shipping),
            status: OrderStatus::PENDING,
            createdAt: new \DateTimeImmutable()
        );
    }

    private function validateCustomer(CustomerId $customerId): void { /* ... */ }
    private function validateItems(array $items): void { /* ... */ }
    private function calculateSubtotal(array $items): Money { /* ... */ }
}

// 3. Domain service: handles cross-entity operations
class InventoryService
{
    public function reserveStock(array $items): void
    {
        foreach ($items as $item) {
            $product = $this->products->findById($item->getProductId());

            if ($product->getStock() < $item->getQuantity()) {
                throw new InsufficientStockException($item->getProductId());
            }

            $product->reserveStock($item->getQuantity());
            $this->products->save($product);
        }
    }
}

}


**Benefits:**
- Single Responsibility Principle

- Easy to understand

```php
// ❌ BAD: Port leaks Doctrine details
namespace App\User\Domain\Port;

use Doctrine\ORM\QueryBuilder;

interface UserRepositoryInterface
{
    public function save(User $user): void;

    // ❌ Exposes Doctrine QueryBuilder!
    public function createQueryBuilder(): QueryBuilder;

    // ❌ Exposes Doctrine-specific method!
    public function findBy(array $criteria, ?array $orderBy = null): array;
}

}

// ✅ GOOD: Port uses domain language only
namespace App\User\Domain\Port;

interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(UserId $id): ?User;
    public function findByEmail(Email $email): ?User;
    public function findActiveUsers(): array; // array<User>
}

// Adapter implements port with Doctrine
namespace App\User\Infrastructure\Persistence;

class DoctrineUserRepository implements UserRepositoryInterface
{
    public function findActiveUsers(): array
    {
        // Doctrine details hidden in adapter
        return $this->entityManager
            ->createQueryBuilder()
            ->select('u')
            ->from(User::class, 'u')
            ->where('u.isActive = :active')
            ->setParameter('active', true)
            ->getQuery()
            ->getResult();
    }
}
}

}



- Domain independent of infrastructure

- Clear, domain-specific API

---

```php
// ❌ BAD: Order repository fetches customers and products
class DoctrineOrderRepository implements OrderRepositoryInterface
{
    public function save(Order $order): void
    {
        // Fetch customer (unrelated to order persistence!)
        $customer = $this->entityManager->find(Customer::class, $order->getCustomerId());

        // Fetch products (unrelated to order persistence!)
        foreach ($order->getItems() as $item) {
            $product = $this->entityManager->find(Product::class, $item->getProductId());
        }

        $this->entityManager->persist($order);
        $this->entityManager->flush();
    }
}
}

**Problem:** Repository becomes service locator, violates Single Responsibility.

```php
// ✅ GOOD: Order repository only manages orders
class DoctrineOrderRepository implements OrderRepositoryInterface
{
    public function save(Order $order): void
    {
        $this->entityManager->persist($order);
        $this->entityManager->flush();
        // That's it! No fetching other entities
    }

    public function findById(OrderId $id): ?Order
    {
        return $this->entityManager->find(Order::class, $id->toString());
    }
}

// Handler coordinates multiple repositories
class CreateOrderHandler
{
    public function __construct(
        private OrderRepositoryInterface $orders,
        private CustomerRepositoryInterface $customers, // Separate repository
        private ProductRepositoryInterface $products,   // Separate repository
    ) {}

    public function __invoke(CreateOrderCommand $command): void
    {
        // Handler fetches entities from their own repositories
        $customer = $this->customers->findById($command->customerId);
        // Validate customer...

        foreach ($command->items as $item) {
            $product = $this->products->findById($item->productId);
            // Validate product...
        }

        $order = OrderFactory::create($customer, $command->items);
        $this->orders->save($order); // Order repository only saves orders
    }
}
    $this->orders->save($order); // Order repository only saves orders

}

// ❌ BAD: Handler starts transaction, but repository also flushes
class CheckoutOrderHandler
{
    public function __invoke(CheckoutOrderCommand $command): void
    {
        $this->entityManager->beginTransaction();

        try {
            $order = $this->orders->findById($command->orderId);
            $order->confirm();

            $this->orders->save($order); // ❌ Calls flush() inside transaction!

            $this->inventory->reserveStock($order->getItems()); // ❌ Also flushes!

            $this->entityManager->commit(); // May commit already-flushed data
        } catch (\Exception $e) {
            $this->entityManager->rollback(); // May not rollback everything!
            throw $e;
        }
    }
}

class DoctrineOrderRepository implements OrderRepositoryInterface
{
    public function save(Order $order): void
    {
        $this->entityManager->persist($order);
        $this->entityManager->flush(); // ❌ Flushes immediately!
    }
}
    $this->entityManager->flush(); // ❌ Flushes immediately!

}

// ✅ GOOD: Handler controls transaction, repositories don't flush
class CheckoutOrderHandler
{
    public function __invoke(CheckoutOrderCommand $command): void
    {
        $this->entityManager->beginTransaction();

        try {
            $order = $this->orders->findById($command->orderId);
            $order->confirm();

            $this->orders->persist($order); // Just persist, don't flush

            $this->inventory->reserveStock($order->getItems()); // Just persist

            $this->entityManager->flush(); // Flush all changes at once
            $this->entityManager->commit();
        } catch (\Exception $e) {
            $this->entityManager->rollback();
            throw $e;
        }
    }
}

// Repository: persist() method (no flush)
class DoctrineOrderRepository implements OrderRepositoryInterface
{
    public function persist(Order $order): void
    {
        $this->entityManager->persist($order);
        // No flush! Let handler control transaction
    }

    public function flush(): void
    {
        $this->entityManager->flush();
    }
}
{
// ✅ GOOD: Use transactional helper
class CheckoutOrderHandler
{
    public function __invoke(CheckoutOrderCommand $command): void
    {
        $this->entityManager->wrapInTransaction(function() use ($command) {
            $order = $this->orders->findById($command->orderId);
            $order->confirm();

            $this->orders->persist($order);
            $this->inventory->reserveStock($order->getItems());

            // Automatically flushes and commits, or rollbacks on exception
        });
    }
}
    });

}

// ❌ BAD: Deleting order deletes customer!
#[ORM\Entity]
class Order
{
    #[ORM\ManyToOne(targetEntity: Customer::class, cascade: ['remove'])] // ❌ Wrong!
    private Customer $customer;
}

// Deleting order accidentally deletes customer
$this->orders->delete($order); // ❌ Customer also deleted!
private Customer $customer;

$this->orders->delete($order); // ❌ Customer also deleted!

// ✅ GOOD: No cascade, explicit deletion
#[ORM\Entity]
class Order
{
    #[ORM\ManyToOne(targetEntity: Customer::class)]
    private Customer $customer; // No cascade
}

// Handler explicitly controls what gets deleted
class DeleteOrderHandler
{
    public function __invoke(DeleteOrderCommand $command): void
    {
        $order = $this->orders->findById($command->orderId);

        // Only delete order, not customer
        $this->orders->delete($order);

        // If needed, handle customer separately
        // $this->customers->delete($order->getCustomer());
    }
}
    $this->orders->delete($order);
// ✅ GOOD: Cascade within aggregate
#[ORM\Entity]
class Order
{
    #[ORM\OneToMany(
        targetEntity: OrderItem::class,
        mappedBy: 'order',
        cascade: ['persist', 'remove'] // ✅ OK: OrderItem is part of Order aggregate
    )]
    private array $items;
}
#[ORM\OneToMany(

    mappedBy: 'order',

OK: OrderItem is part of Order aggregate

// ❌ BAD: Over-engineered for simple CRUD
interface UserCreatorInterface { /* ... */ }
interface UserUpdaterInterface { /* ... */ }
interface UserDeleterInterface { /* ... */ }
interface UserFinderInterface { /* ... */ }
interface UserValidatorInterface { /* ... */ }
interface UserFactoryInterface { /* ... */ }
interface UserMapperInterface { /* ... */ }

class UserCreator implements UserCreatorInterface { /* ... */ }
class UserUpdater implements UserUpdaterInterface { /* ... */ }
// ... 10 more classes for simple user management

interface UserFinderInterface { /* ... */ }

interface UserFactoryInterface { /* ... */ }

// ✅ GOOD: Simple interface for simple needs
interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(UserId $id): ?User;
    public function findByEmail(Email $email): ?User;
    public function delete(User $user): void;
}

class DoctrineUserRepository implements UserRepositoryInterface
{
    // Simple implementation
}

// Refactor into separate interfaces ONLY when:
// - Multiple implementations needed
// - Interface becomes too large
// - Different clients need different methods

class DoctrineUserRepository implements UserRepositoryInterface

// Simple implementation

// - Multiple implementations needed

// ❌ BAD: Testing internal state instead of behavior
class OrderTest extends TestCase
{
    public function test_ship_order(): void
    {
        $order = new Order(OrderId::generate(), OrderStatus::CONFIRMED);

        $order->ship();

        // ❌ Testing private property directly (using reflection)
        $reflection = new \ReflectionClass($order);
        $property = $reflection->getProperty('status');
        $property->setAccessible(true);

        $this->assertEquals(OrderStatus::SHIPPED, $property->getValue($order));
    }
}
    $reflection = new \ReflectionClass($order);

    $property->setAccessible(true);
// ✅ GOOD: Test public behavior
class OrderTest extends TestCase
{
    public function test_ship_order(): void
    {
        $order = new Order(OrderId::generate(), OrderStatus::CONFIRMED);

        $order->ship();

        // Test public method
        $this->assertEquals(OrderStatus::SHIPPED, $order->getStatus());
        $this->assertNotNull($order->getShippedAt());
    }

    public function test_cannot_ship_cancelled_order(): void
    {
        $order = new Order(OrderId::generate(), OrderStatus::CANCELLED);

        $this->expectException(CannotShipCancelledOrderException::class);

        $order->ship();
    }
}
{
// ❌ BAD: Mocking value objects and entities
class RegisterUserHandlerTest extends TestCase
{
    public function test_registers_user(): void
    {
        $user = $this->createMock(User::class); // ❌ Mocking entity
        $email = $this->createMock(Email::class); // ❌ Mocking value object

        $factory = $this->createMock(UserFactory::class);
        $factory->method('create')->willReturn($user);

        $repository = $this->createMock(UserRepositoryInterface::class);
        $repository->expects($this->once())->method('save')->with($user);

        $handler = new RegisterUserHandler($factory, $repository);
        $handler(new RegisterUserCommand('test@example.com', 'password'));
    }
}
    $factory = $this->createMock(UserFactory::class);

    $repository->expects($this->once())->method('save')->with($user);
// ✅ GOOD: Real domain objects, mock infrastructure
class RegisterUserHandlerTest extends TestCase
{
    public function test_registers_user(): void
    {
        // Real factory and entities
        $repository = new InMemoryUserRepository(); // Fake infrastructure

        $handler = new RegisterUserHandler($repository);

        $handler(new RegisterUserCommand('test@example.com', 'ValidPass123'));

        // Verify using real repository
        $this->assertTrue($repository->existsByEmail('test@example.com'));

        $user = $repository->findByEmail('test@example.com');
        $this->assertFalse($user->isActive());
    }
}
    $handler = new RegisterUserHandler($repository);

    $handler(new RegisterUserCommand('test@example.com', 'ValidPass123'));
// ❌ BAD: Only testing happy path
class OrderTest extends TestCase
{
    public functio...
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky