ใช้คลาส Action แทน Controller ใน Symfony

Aug 23 2020

ผมสานุศิษย์ของการดำเนินการชั้นวิธีใช้แทนการควบคุม คำอธิบายนั้นง่ายมาก: บ่อยครั้งที่Controllerมีการกระทำหลายอย่างเมื่อทำตามหลักการDependency Injectionเราจะต้องส่งการอ้างอิงที่จำเป็นทั้งหมดไปยังตัวสร้างและสิ่งนี้จะทำให้สถานการณ์เมื่อControllerมีการอ้างอิงจำนวนมาก แต่ในช่วงเวลาหนึ่ง (เช่นคำขอ) เราใช้การอ้างอิงเพียงบางส่วนเท่านั้น ยากที่จะรักษาและทดสอบรหัสสปาเก็ตตี้นั้น

ชี้แจงผมเคยใช้อยู่แล้วในการทำงานด้วยวิธีการว่าใน Zend Framework 2 แต่มีมันชื่อมิดเดิ้ล ฉันพบสิ่งที่คล้ายกันใน API-Platform โดยที่พวกเขาใช้คลาส Actionแทน Controller ด้วย แต่ปัญหาคือฉันไม่รู้วิธีปรุง

UPD: ฉันจะรับ Action Class ถัดไปและแทนที่ Controller มาตรฐานได้อย่างไรและควรเพิ่มการกำหนดค่าใดในโครงการ Symfony ปกติ

<?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);
    }
}

คำตอบ

3 Cerad Aug 23 2020 at 21:45

คำถามค่อนข้างคลุมเครือสำหรับ stackoverflow แม้ว่ามันจะน่าสนใจเล็กน้อยก็ตาม ดังนั้นนี่คือรายละเอียดการกำหนดค่าบางส่วน

เริ่มต้นด้วยโครงการโครงกระดูก S4 นอกกรอบ:

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

เพิ่ม 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);
    }
}

และกำหนดเส้นทาง:

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

ณ จุดนี้การเดินสายเกือบเสร็จสมบูรณ์ หากคุณไปที่ url คุณจะได้รับ:

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

เหตุผลก็คือบริการเป็นส่วนตัวโดยปริยาย โดยปกติคุณจะขยายจาก AbstractController ซึ่งดูแลการทำให้บริการเป็นสาธารณะ แต่ในกรณีนี้วิธีที่เร็วที่สุดคือเพียงแค่แท็กการกระทำเป็นตัวควบคุม:

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

ณ จุดนี้คุณควรมีการดำเนินการแบบมีสาย

แน่นอนว่ามีรูปแบบต่างๆมากมายและรายละเอียดเพิ่มเติมเล็กน้อย คุณจะต้อง จำกัด เส้นทางเป็น POST หรือ DELETE ปลอม

คุณอาจพิจารณาเพิ่ม ControllerServiceArgumentsInterface ที่ว่างเปล่าจากนั้นใช้ฟังก์ชันอินสแตนซ์บริการเพื่อใช้แท็กคอนโทรลเลอร์เพื่อให้คุณไม่จำเป็นต้องกำหนดบริการคอนโทรลเลอร์ด้วยตนเองอีกต่อไป

แต่ก็น่าจะเพียงพอสำหรับคุณเริ่มต้น

1 SerhiiPopov Sep 24 2020 at 18:38

แนวทางที่ฉันพยายามใช้มีชื่อว่ารูปแบบ ADR (Action-Domain-Responder)และ Symfony ได้สนับสนุนสิ่งนี้แล้วโดยเริ่มจากเวอร์ชัน 3.3 คุณสามารถเรียกมันว่าควบคุม Invokable

จากเอกสารอย่างเป็นทางการ:

ตัวควบคุมยังสามารถกำหนดการดำเนินการเดียวโดยใช้เมธอด __invoke () ซึ่งเป็นแนวทางปฏิบัติทั่วไปเมื่อทำตามรูปแบบ 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));
    }
}