1

Status quo

  • I created an aggregate, let's call it Foo. It has two entities within itself, let's call them Foo & Bar.
  • You can mutate things by calling the aggregate's public methods. E.g. $foo->doSomething(). I.e. the aggregate itself has an repository injection. Looks this way
class Foo
{
    /**
     * @var FooRepositoryInterface
     */
    private $fooRepository;

    /**
     * @var BarRepositoryInterface
     */
    private $barRepository;

    /**
     * @var FooEntity|null
     */
    private $fooEntity;

    /**
     * @var BarEntity|null
     */
    private $barEntity;

    public function __construct(
        FooRepositoryInterface $fooRepository,
        BarRepositoryInterface $barRepository,
        ?FooEntity $fooEntity,
        ?BarEntity $barEntity
    ) {
        $this->fooRepository = $fooRepository;
        $this->barRepository = $barRepository;
        $this->fooEntity = $fooEntity;
        $this->barEntity = $barEntity;
    }

    public function doSomething()
    {
        // check if the entities are not nulls or nulls. Depends on what the method does
        // create/mutate entities and persist them
        // assign entities to $this
    }

    public function getSomethingA(string $bla)
    {
        return $this->foo->getSomething();
    }

    public function getSomethingB()
    {
        return $this->bar->getSomething();
    }
}

I.e. the entire persistence "logic" is within an aggregate. As there are dependencies on the repositories, makes sense to erect such aggregates via factories. E.g.

class FooFactory {
    /**
     * @var FooRepositoryInterface
     */
    private $fooRepository;

    /**
     * @var BarRepositoryInterface
     */
    private $barRepository;

    public function __construct(
        FooRepositoryInterface $fooRepository,
        BarRepositoryInterface $barRepository
    ) {
        $this->fooRepository = $fooRepository;
        $this->barRepository = $barRepository;
    }

    public function createForFooBaring(): Foo
    {
        return new Foo($this->fooRepository, $this->barRepository, null, null);
    }
}

And then in a use-case e.g. or a service, just inject the factory and you are good to mutate things.

class UseCase {
    private $fooFactory;

    public function __construct(FooFactory $fooFactory)
    {
        $this->fooFactory = $fooFactory;
    }

    public function execute(Input $input): Output
    {
        $foo = $this->fooFactory->createForFooBaring();
        $foo->doSomething($input->getSomething()); // that's it. The data is transactionally (if it's a must) persisted. You read the business logic here, not technical implementation of inserts or updates. Awesome!
    }
}

Problem

Now, let's assume I want to find an aggregate root in the use-case above. Smells like it's right about time to inject a repository here, find entities and use a factory to establish an existing aggregate.

But. First of all, the factory itself has all of the dependencies needed, yet if we do something in the factory like

public function findByEmail(string $email): ?Foo
{
   $fooEntity = $this->fooRepository->findByEmail($email); 
   // let's assume here we found it
   $barEntity = $this->barRepository->findBySomeForeignKey($fooEntity->getBarFk());
   // let's assume here we found it

   return new Foo($this->fooRepository, $this->barRepository, $fooEntity, $barEntity);
}

Nice. But. It's no longer a factory sorta. Could rename it to say... uhm... FooManager/Service e.g. and then it fits an idea.

Hence the question. How do you normally find an aggregate in DDD?

1
  • 2
    Not sure if I'm a reputable source, but Auberon's answer is what you are looking for. Keep your domain model free of infrastructure concepts & dependencies.
    – plalx
    Commented Oct 18, 2020 at 2:31

1 Answer 1

4

the aggregate itself has an repository injection

This is discouraged. This is because now your domain object is concerned with both business rules as well as persistence. You can study Clean Architecture to see why that is important to seperate.

use a factory to establish an existing aggregate.

The concept of a Factory only exists in DDD for creating new entities. Not existing ones. It mentions the use of 'reconstitution' factories, but only as an implementation detail of your database layer.

DDD asks that your UseCase looks like this

class UseCase {
    private $fooFactory;
    private $fooRepository;

    public function __construct(FooFactory $fooFactory, FooRepository $fooRepository)
    {
        $this->fooFactory = $fooFactory;
        $this->fooRepository = $fooRepository;
    }

    public function add(Input $input): Output
    {
        $foo = $this->fooFactory->createFoo($input->getEmail());
        $fooRepository->Add($foo);
    }

    public function get(Input $input): Output
    {
        $foo = $this->fooRepository->getFooByEmail($input->getEmail());
        
        //other stuff...

        //create Output object and put foo in there.
    }
}

FooFactory...

class FooFactory {

    public function __construct(

    ) {

    }

    public function createFoo(string $email): Foo
    {
        id = ... // generate id... (or use the one provided by user if that's relevant)
        
        //create Bars if you want... But not by injecting repositories! Use a BarFactory if you wish. 
        return new Foo($email, $bars);
    }
}

FooRepository...

class FooRepository{

    public function __construct(

    ) {

    }

    public function getByEmail(string $email): Foo
    {
        //get data from database (id, bars, etc etc)
        
        //You can use a factory here if you want. DDD mentions a reconstitution factory,
        //but it is not the same as the factory that creates new entities above!
        return new Foo(id, email, bars)
    }

    public function get(string $id): Foo
    {
        //get data from database (email, bars, etc etc)
       
        return new Foo(id, email, bars)
    }
}

In short, inject repositories in your UseCase like you smelled; but don't inject them in factories or your domain model

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.