Use a classe Action em vez de Controller no Symfony

Aug 23 2020

Eu sou adepto da abordagem Action Class usando em vez de Controller . A explicação é muito simples: muitas vezes o Controlador inclui muitas ações, ao seguir o princípio de Injeção de Dependências devemos passar todas as dependências necessárias para um construtor e isso cria uma situação em que o Controlador tem um grande número de dependências, mas no momento certo (por exemplo, solicitação) usamos apenas algumas dependências. É difícil manter e testar esse código espaguete.

Para esclarecer, já trabalhei com essa abordagem no Zend Framework 2, mas lá é chamada de Middleware . Encontrei algo semelhante na API-Platform, onde também usam a classe Action em vez de Controller, mas o problema é que não sei como prepará-la.

UPD: como posso obter a próxima classe de ação e substituir o controlador padrão e qual configuração devo adicionar no projeto Symfony regular?

<?php
declare(strict_types=1);

namespace App\Action\Product;

use App\Entity\Product;
use Doctrine\ORM\EntityManager;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SoftDeleteAction
{
    /**
     * @var EntityManager
     */
    private $entityManager; /** * @param EntityManager $entityManager
     */
    public function __construct(EntityManager $entityManager) { $this->entityManager = $entityManager; } /** * @Route( * name="app_product_delete", * path="products/{id}/delete" * ) * * @Method("DELETE") * * @param Product $product
     *
     * @return Response
     */
    public function __invoke(Request $request, $id): Response
    {
        $product = $this->entityManager->find(Product::class, $id); $product->delete();
        $this->entityManager->flush();

        return new Response('', 204);
    }
}

Respostas

3 Cerad Aug 23 2020 at 21:45

A questão é um pouco vaga para o stackoverflow, embora também seja um pouco interessante. Então, aqui estão alguns detalhes de configuração.

Comece com um projeto de esqueleto S4 pronto para uso:

symfony new --version=lts s4api
cd s4api
bin/console --version # 4.4.11
composer require orm-pack

Adicione a SoftDeleteAction

namespace App\Action\Product;
class SoftDeleteAction
{
    private $entityManager; public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }
    public function __invoke(Request $request, int $id) : Response
    {
        return new Response('Product ' . $id);
    }
}

E definir a rota:

# config/routes.yaml
app_product_delete:
    path: /products/{id}/delete
    controller: App\Action\Product\SoftDeleteAction

Neste ponto, a fiação está quase completa. Se você acessar o url, obterá:

The controller for URI "/products/42/delete" is not callable:

O motivo é que os serviços são privados por padrão. Normalmente você estenderia de AbstractController, que se encarrega de tornar o serviço público, mas neste caso a abordagem mais rápida é apenas marcar a ação como um controlador:

# config/services.yaml
    App\Action\Product\SoftDeleteAction:
        tags: ['controller.service_arguments']

Neste ponto, você deve ter uma ação conectada funcionando.

Claro que existem muitas variações e mais alguns detalhes. Você vai querer restringir a rota para POST ou DELETE falso.

Você também pode considerar adicionar um ControllerServiceArgumentsInterface vazio e, em seguida, usar a funcionalidade de instância de serviços para aplicar o tag do controlador, de forma que você não precise mais definir manualmente os serviços do controlador.

Mas isso deve ser o suficiente para você começar.

1 SerhiiPopov Sep 24 2020 at 18:38

A abordagem que estava tentando implementar é nomeada como padrão ADR (Action-Domain-Responder) e Symfony já suportou isso iniciado a partir da versão 3.3. Você pode se referir a ele como controladores invokáveis .

Dos documentos oficiais:

Os controladores também podem definir uma única ação usando o método __invoke (), que é uma prática comum ao seguir o padrão ADR (Action-Domain-Responder):

// src/Controller/Hello.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/hello/{name}", name="hello")
 */
class Hello
{
    public function __invoke($name = 'World') { return new Response(sprintf('Hello %s!', $name));
    }
}