lepix
lepix

Reputation: 4990

Symfony : should logic about formatting a parameter be in repository or controller?

What is the best practice according the Symfony way ?

Option 1 :

class MyController extends Controller
{
    public function myAction(...)
    {
        // ..
        $example = $this->getDoctrine()->getRepository('AppBundle:Contract')->getContracts(array(
            'company_id'                => $company->getId(),
            'contract_month_date_start'  => date('Y-m-d', strtotime('first day of this month', $contractDate->getTimestamp())),
            'contract_month_date_end'    => date('Y-m-d', strtotime('last day of this month', $contractDate->getTimestamp())),
        ));
        // ..
    }
}

class ExampleRepository extends EntityRepository
{
    public function getContracts($options)
    {

        //..

        $qb->select('contract')
            ->from('AppBundle:Contract', 'contract')
            ->where('contract.companyId = :company_id')
            ->andWhere('contract.startDate < :contract_month_date_end')
            ->andWhere('contract.endDate < :contract_month_date_end')
            ->setParameter('company_id', $options['company_id'])
            ->setParameter('contract_month_date_start', $options['contract_month_date_start'])
            ->setParameter('contract_month_date_end', $options['contract_month_date_end']);

        //..
    }
}

Option 2 :

class MyController extends Controller
{
    public function myAction(...)
    {
        // ..
        $example = $this->getDoctrine()->getRepository('AppBundle:Contract')->getContracts(array(
            'company'      => $company,
            'contractDate' => $contractDate
        ));
        // ..
    }
}


class ExampleRepository extends EntityRepository
{
    public function getContracts($options)
    {

        //..

        $company_id                = $options['company']->getId();
        $contract_month_date_start = date('Y-m-d', strtotime('first day of this month', $options['contractDate']));
        $contract_month_date_end   = date('Y-m-d', strtotime('last day of this month', $options['contractDate']));

        $qb->select('contract')
            ->from('AppBundle:Contract', 'contract')
            ->where('contract.companyId = :company_id')
            ->andWhere('contract.startDate < :contract_month_date_end')
            ->andWhere('contract.endDate < :contract_month_date_end')
            ->setParameter('company_id', $company_id)
            ->setParameter('contract_month_date_start', $contract_month_date_start)
            ->setParameter('contract_month_date_end', $contract_month_date_end);

        //..
    }
}

I feel like the second one is better (less duplicate code between the differents controllers using the repository). But I also feel like it gives too much responsibility to the Repository.

Upvotes: 3

Views: 181

Answers (3)

Aziule
Aziule

Reputation: 441

TL;DR: in your example, your formatting logic should ideally be handled in an intermediate layer.

Here are a few improvements:

First, do not get the repository from your controller. Instead, declare it as a service (for example, in your app/config/services.yml).

Also, you could abstract the access to your data (= your repository) using a new layer. The communication would be done like this:

Controller <=> Business layer <=> Data access.

  • Controller: responsible for calling the correct methods of the business layer and handling responses (rendering HTML, returning JSON, etc.).
  • Business layer: responsible for the logic of your app.
  • Data access: responsible for querying your DB (for example) and getting the data without including any business logic. Can only be accessed by the business layer.

Using such an architecture will help you having a more scalable application without taking too long to implement it.

In your code, it would look like the following:

Your controller, responsible for "routing" the user's request to the correct service

class MyController extends Controller
{
    public function myAction()
    {
        // Accessing the contract manager, previously declared as a service in services.yml
        $contractManager = $this->get('manager.contract');
        $contracts = $contractManager->getMonthlyContracts($company);
    }
}

Your business layer, responsible for executing the logic:

// This class must be declared as a service in services.yml
class ContractManager {
    private $contractRepository;

    public function __construct(ContractRepository $contractRepository)
    {
        $this->contractRepository = $contractRepository;
    }

    public function getMonthlyContracts(Company $company)
    {
        // The business layer is responsible for the logic of fetching the data. Therefore, its methods are business-related (here "find the monthly contracts for company X", which is very specific).
        // It is its role to set the start and end dates, not the controller's
        $contracts = $this->contractRepository->findByCompany(
            $company,
            new \DateTime('first day of this month'),
            new \DateTime('last day of this month')
        );

        // Do some logic with the contracts if required...

        return $contracts;
    }
}

Your repository, responsible for accessing the data:

class ContractRepository extends EntityRepository
{
    // The repository handles basic access to data without any logic, hence the generic "findByCompany" method name
    public function findByCompany(Company $company, \DateTime $from, \DateTime $to)
    {
        $qb->select('contract')
            ->from('AppBundle:Contract', 'contract')
            ->where('contract.company = :company')
            ->andWhere('contract.startDate > :from')
            ->andWhere('contract.endDate < :to')
            ->setParameter('company', $company)
            ->setParameter('from', $from)
            ->setParameter('to', $to);

        //...
    }
}

Please have a look at the Symfony doc about how to declare a repository as a service in your services.yml.

Upvotes: 1

lordrhodos
lordrhodos

Reputation: 2745

Symfony best practices are not clear regarding your task, but IMHO the following statement could be mapped to your second option

Overall, this means you should aggressively decouple your business logic from the framework while, at the same time, aggressively coupling your controllers and routing to the framework in order to get the most out of it.

As you have already stated, the second option keeps the controllers thinner and moves the logic to the repository level, which is in terms of a Symfony way a totally valid approach.

This best practice statement adds some additional support for option 2

In Symfony applications, business logic is all the custom code you write for your app that's not specific to the framework (e.g. routing and controllers). Domain classes, Doctrine entities and regular PHP classes that are used as services are good examples of business logic.

If you need this formatting at different places around your application another approach could be to create a service class which you can inject into the Repositories where you need it.

Upvotes: 1

Matteo
Matteo

Reputation: 39390

I prefer to do this kind of job in the presentation layer, if you are using TWIG you should take a look at the date format filter or if it is an api you can work to the presentation layer with JMS serialization as example.

In this manner the business logic don't change and is not affected about change of the presentation logic

My two cents.

Upvotes: 0

Related Questions