Making a fat model in Symfony 2 - Composition or I

2019-05-06 16:29发布

I've got to the point with Symfony 2 / Doctrine 2 where I've come to realise that we have build too much business logic in our application into services & controllers - and not enough into the model.

We wish to introduce configuration to our models (to modify behaviour) potentially giving models access to services directly in order to carry out their behaviours.

I've noticed that the following question has the completely wrong answer marked as correct with 8 upvotes - so I know the approach we have taken up to now (anaemic model) is considered the 'correct' way to do things by a lot of Symfony 2 users. After reading more into domain driven design I know this is not the case.

Symfony2 MVC: where does my code belong?

I see a lot of bundles define the behaviour in the model and extend this in entities/documents. This pattern works to a certain extent - but I think we need to introduce an additional stage. Some of the behaviour of our models is optional and having that behaviour will depend on what additional bundles are registered in our application (so including X bundle will allow the application to do more things). An Example.

We have an order object which at the moment has a bidirectional relationship with entities in the courier bundle meaning there is a hard dependency. I want to decouple this and have the courier bundle(s) optionally add behaviour to the order. Consider this method call.

// no courier bundle is registered
$order->getShippingMethods();
// throws NoAvailableShippingMethodsException;

// one bundle registered
$order-getShippingMethods();
// returns an array with one shipping method

etc....

Now currently we have an OrderProvider service which just sits on top of the Entity Manager - so if you call

$orderProvider->GetOrder($id);

You just get the entity returned 'direct' from the database. My question here is what pattens are other people using here? I'm thinking about moving all 'business logic' into a model class that the entity extends, having the service layer pull the entity out (entity being dumb record with properties in the database and getters), and then configure the model using configuration (the configuration being injected into the OrderProvider service), which will modify the behaviour of the model. For the example given I might do something like (within the OrderProvider)..

// trimmed down for example purposes by removing exceptions etc.
public function getOrder($id) 
{
    $order = $this->orderRepository->findOneById($id);
    if ($this->couriers){
       $order->addCouriers($couriers);
    }
    return $order;
}

// this function would be called by the courier bundle automatically using semantic configuration / tags / setter injection
public function addCourier(CourierInterface $courier)
{
    $this->couriers[] = $courier;
}

The other option that I have is to create a new type of object - which decorates the base order and is already configured (as it ITSELF will be defined as a service in the DIC) and inject the order into that. The difference is subtle and both approaches would work but I'm wondering which is the best path.

Finally I have one issue with all of this that I can't get my head around. If my base Order entity has relationships with other entities and THOSE entities need to be configured - where should this happen? For example if I access my customer thus.

$order->getCustomer();

I get the customer (entity). But It may be the case that I need to add some configuration to the customer object too - like

$customer->getContactMethods();

Now the behaviour of this method might differ depending on whether my application has registered a twitter bundle or a facebook bundle or something else. Given the above example I'm not going to get a sufficiently configured customer - but rather the 'vanilla' base entity. Only way I can see around this is to cut relationships between entities which require configuration and pull the entity from a CustomerProvider service:

$customerProvider->getCustomerByOrder($order);

This seems to me to be removing information from the model layer and moves back towards a reliance on using multiple services for simple tasks (which I'm trying to get away from). Thoughts and links to resources appreciated.

Edit: Relevant - I see the cons listed in the first answer every single day which is why I've asked this question -> Anemic Domain Model: Pros/Cons

1条回答
Melony?
2楼-- · 2019-05-06 17:15

It seems like a complexity of your project is the modularity requirement - application behavior must be extensible via bundles. I'm not familiar with Symfony 2 / Doctrine 2 but a typical DDD tactic is to try and make sure that domain entities such as Order and Customer are unaware of bundle configurations. In other words, surrounding services should not add bundle-specific behaviors to entities. Delegating the responsibility for bundle awareness to entities will make them too complex. Fabricating entity class hierarchies to support extensive behavior is also too complex. Instead, this extensibility should be managed by application services. An application service would determine which bundles are loaded and orchestrate the appropriate entities as a result.

Another strategic pattern to consider is bounded contexts. Is it possible to partition your application into bounded contexts which align with the modules? For example, to address the $order-getShippingMethods() method you can create two BCs, one where there is an order model that has a getShippingMethods() method and another without it. Having two models may seem like a violation of DRY but if the models represent different things (ie an order that has shipping data vs an order that doesn't) then nothing is actually repeated.

查看更多
登录 后发表回答