Symfony to framework, w którym kontroler powinien przyjąć Request i zwrócić Response, nie jest wymagane dziedziczenie z klas abstrakcyjnych. Kontrolerem może być prosta klasa PHP.

Poniżej przykład prostego w pełni funkcjonalnego kontrolera:

namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

Co jeśli jednak potrzebujemy czegoś więcej? Symfony dostarcza klase abstrakcyjną Controller, która zawiera dostęp do kontenera oraz zestaw skrótów do przydatnych metod

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    // ...
}

Kontroler wg najlepszych praktyk symfony powinien stosować się do zasady 5-10-20, która mówi o tym, że kontroler nie powinien zawierać więcej niż 5 metod, 10 akcji, a każda akcja powinna mieć mniej niż 20 linii. Jest to istotne z punktu widzenia czytelności kodu. Taki podział, nie jest możliwy bez użycia serwisów.

Załóżmy, że potrzebujemy użyć dwóch serwisów pierwszy do logowania, a drugi to EntityManger z Doctrine możemy to zróbić na dwa sposoby.

Pobranie serwisów przez kontener

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HelloController extends Controller
{
    public function helloAction()
    {
        $this->get('entity_manager')->getRepository('AppBundle::Hello')->findAll();
        $this->get('logger')->log(Logger:INFO, 'S**t happens', []);
        $this->getDoctrine()->getManager()->getRepository('AppBundle::Hello')->findAll();
    }

    public function whatsupAction()
    {
          $this->get('entity_manager')->getRepository('AppBundle::Hello')->findAll();
          $this->get('logger')->log(Logger:INFO, 'S**t happens', []);
    }
}

W powyższym kodzie widać, że często musimy pobierać serwisy ponieważ nie da ich się przypisać w konstruktorze powtarzamy kod. Można jednak zrobić to inaczej.

Wstrzykiwanie serwisów przez Dependency Injeciton

namespace AppBundle\Controller;

class HelloController
{
    private $em;
    private $logger;
    
    public function __construct(EntityManagerInterface $em, LoggerInterface $logger)
    {
        $this->em = $em;
        $this->logger = $logger;
    }

    public function helloAction()
    {
        $this->em->getRepository('AppBundle::Hello')->findAll();
        $this->logger->log(Logger:INFO, 'S**t happens', []);
    }

    public function whatsupAction()
    {
        $this->em->getRepository('AppBundle::Hello')->findAll();
        $this->em->log(Logger:INFO, 'S**t happens', []);
    }
}

Co nam to daje?

Po pierwsze, po konstruktorze klasy jesteśmy w stanie domyśleć się co robi klasa, w tym przypadku klasa na pewno będzie pobierała coś z bazy i logowała operacje.
Po drugie nasz kod jest zgodny z SOLID.
Po trzecie możemy łatwo napisać unit testy, gdzie tak naprawdę testujemy serwisy.
Po czwarte możemy zmodyfikować zachowanie kontrolera wymieniając komponenty w definicji serwisu bez konieczności zmiany kodu kontrolera.

Jakie są minusy?

Więcej pracy, nie mamy dostępu do użytecznych metod z abstrakcyjnej klasy Controller
Konieczność definiowania serwisów dla każdego kontrolera.

 

Przydatne linki:

  • http://symfony.com/doc/current/cookbook/controller/service.html
  • http://symfony.com/doc/current/book/controller.html
  • http://symfony.com/doc/current/book/service_container.html
  • https://knpuniversity.com/screencast/question-answer-day/controllers-services
  • http://richardmiller.co.uk/2011/04/15/symfony2-controller-as-service/