使控制器更薄。 Laravel 8

Making controller thinner. Laravel 8

我有Post控制器,这是它的方法store()

public function store(Request $request)
{
    $this->handleUploadedImage(
        $request->file('upload'),
        $request->input('CKEditorFuncNum')
    );

    $post = Post::create([
        'content'      => request('content'),
        'is_published' => request('is_published'),
        'slug'         => Carbon::now()->format('Y-m-d-His'),
        'title'        => $this->firstSentence(request('content')),
        'template'     => $this->randomTemplate(request('template')),
    ]);
    $post->tag(explode(',', $request->tags));

    return redirect()->route('posts');
}

方法handleUploadedImage()现在存储在Post控制器本身。但我打算在其他控制器中使用它。我应该把它移到哪里?不在请求 class 中,因为它与验证无关。不在 Models/Post 中,因为它不仅适用于 Post 型号。对于服务提供商 class.

来说,它不是全局函数

方法 firstSentence()randomTemplate() 也存储在该控制器中。它们只会在其中使用。也许我应该将它们移到 Models/Post?那么,如何在方法 store() 中调用它们(更具体地说,在方法 create() 中)?

我读过这个理论,我理解(希望)瘦控制器和胖模型的概念,但我需要一些关于这个例子的实用的具体建议。能否请您建议,移动到哪里以及如何调用这些方法?

我平时做什么

public function store(ValidationRequest $request)
{
    $result = $this->dispatchNow($request->validated())
    return redirect()->route('posts');
}

因此,我创建了一个作业来处理这些注册步骤,我可以在系统的其他部分重复使用它。

你的 firstSentence 我会移动到一个叫做字符串 app\helpers\stringshelper (并且不要忘记在 composer.json 中更新它,你可以只使用 firstSentence( $var) 在你系统的任何部分

randomTemplate 非常适合某个特征,但我不知道该方法的作用。

首先,注意事项:我不使用 Laravel,所以我将向您展示一个适用于所有框架的通用解决方案。

的确,控制器应该始终保持纤薄。但这也应该适用于模型层。这两个目标都可以通过将 application-specific 逻辑 移动到 应用程序服务 来实现(因此,不要移动到模型层,使模型变胖!) .它们也是 so-called service layer. Read this 的组成部分。

在您的情况下,您似乎可以优雅地将上传图像的处理逻辑推送到服务中,例如 App\Service\Http\Upload\ImageHandler,包含一个 handle 方法。 class 和方法的名称可以更好地选择,但取决于确切的 class 职责。

创建和存储 post 的逻辑将进入另一个应用程序服务:例如 App\Service\Post。原则上,该服务将执行以下任务:

  1. 创建一个实体 - 例如 Domain\Model\Post\Post - 并设置其属性(titlecontentis_publishedtemplate 等)在用户输入上。例如,这可以在方法 App\Service\Post::createPost 中完成。
  2. 将实体存储在数据库中,作为数据库记录。例如,这可以在方法 App\Service\Post::storePost 中完成。
  3. 其他任务...

关于第一个任务,App\Service\Post服务的两个方法可能有用:

  • generatePostTitle,封装了从user-provided“内容”中提取第一句的逻辑,以便从中设置post实体的标题;
  • generatePostTemplate,包含你在randomTemplate()的评论中描述的逻辑。

关于第二个任务,就个人而言,我会通过在其之上使用特定的 data mapper - to directly communicate with the database API - and a specific repository 将实体存储在数据库中 - 作为 collection 的抽象 post objects.

服务:

<?php

namespace App\Service;

use Carbon;
use Domain\Model\Post\Post;
use Domain\Model\Post\PostCollection;

/**
 * Post service.
 */
class Post {

    /**
     * Post collection.
     * 
     * @var PostCollection
     */
    private $postCollection;

    /**
     * @param PostCollection $postCollection Post collection.
     */
    public function __construct(PostCollection $postCollection) {
        $this->postCollection = $postCollection;
    }

    /**
     * Create a post.
     * 
     * @param string $content Post content.
     * @param string $template The template used for the post.
     * @param bool $isPublished (optional) Indicate if the post is published.
     * @return Post Post.
     */
    public function createPost(
        string $content,
        string $template,
        bool $isPublished
        /* , ... */
    ) {
        $title = $this->generatePostTitle($content);
        $slug = $this->generatePostSlug();
        $template = $this->generatePostTemplate($template);

        $post = new Post();

        $post
            ->setTitle($title)
            ->setContent($content)
            ->setIsPublished($isPublished ? 1 : 0)
            ->setSlug($slug)
            ->setTemplate($template)
        ;

        return $post;
    }

    /**
     * Store a post.
     * 
     * @param Post $post Post.
     * @return Post Post.
     */
    public function storePost(Post $post) {
        return $this->postCollection->storePost($post);
    }

    /**
     * Generate the title of a post by extracting 
     * a certain part from the given post content.
     * 
     * @return string Generated post title.
     */
    private function generatePostTitle(string $content) {
        return substr($content, 0, 300) . '...';
    }

    /**
     * Generate the slug of a post.
     * 
     * @return string Generated slug.
     */
    private function generatePostSlug() {
        return Carbon::now()->format('Y-m-d-His');
    }

    /**
     * Generate the template assignable to 
     * a post based on the given template.
     * 
     * @return string Generated post template.
     */
    private function generatePostTemplate(string $template) {
        return 'the-generated-template';
    }

}

存储库接口:

<?php

namespace Domain\Model\Post;

use Domain\Model\Post\Post;

/**
 * Post collection interface.
 */
interface PostCollection {

    /**
     * Store a post.
     * 
     * @param Post $post Post.
     * @return Post Post.
     */
    public function storePost(Post $post);

    /**
     * Find a post by id.
     * 
     * @param int $id Post id.
     * @return Post|null Post.
     */
    public function findPostById(int $id);

    /**
     * Find all posts.
     * 
     * @return Post[] Post list.
     */
    public function findAllPosts();

    /**
     * Check if the given post exists.
     * 
     * @param Post $post Post.
     * @return bool True if post exists, false otherwise.
     */
    public function postExists(Post $post);
}

存储库实现:

<?php

namespace Domain\Infrastructure\Repository\Post;

use Domain\Model\Post\Post;
use Domain\Infrastructure\Mapper\Post\PostMapper;
use Domain\Model\Post\PostCollection as PostCollectionInterface;

/**
 * Post collection.
 */
class PostCollection implements PostCollectionInterface {

    /**
     * Posts list.
     * 
     * @var Post[]
     */
    private $posts;

    /**
     * Post mapper.
     * 
     * @var PostMapper
     */
    private $postMapper;

    /**
     * @param PostMapper $postMapper Post mapper.
     */
    public function __construct(PostMapper $postMapper) {
        $this->postMapper = $postMapper;
    }

    /**
     * Store a post.
     * 
     * @param Post $post Post.
     * @return Post Post.
     */
    public function storePost(Post $post) {
        $savedPost = $this->postMapper->savePost($post);

        $this->posts[$savedPost->getId()] = $savedPost;

        return $savedPost;
    }

    /**
     * Find a post by id.
     * 
     * @param int $id Post id.
     * @return Post|null Post.
     */
    public function findPostById(int $id) {
        //... 
    }

    /**
     * Find all posts.
     * 
     * @return Post[] Post list.
     */
    public function findAllPosts() {
        //...
    }

    /**
     * Check if the given post exists.
     * 
     * @param Post $post Post.
     * @return bool True if post exists, false otherwise.
     */
    public function postExists(Post $post) {
        //...
    }

}

数据映射器接口:

<?php

namespace Domain\Infrastructure\Mapper\Post;

use Domain\Model\Post\Post;

/**
 * Post mapper.
 */
interface PostMapper {

    /**
     * Save a post.
     * 
     * @param Post $post Post.
     * @return Post Post entity with id automatically assigned upon persisting.
     */
    public function savePost(Post $post);

    /**
     * Fetch a post by id.
     * 
     * @param int $id Post id.
     * @return Post|null Post.
     */
    public function fetchPostById(int $id);

    /**
     * Fetch all posts.
     * 
     * @return Post[] Post list.
     */
    public function fetchAllPosts();

    /**
     * Check if a post exists.
     * 
     * @param Post $post Post.
     * @return bool True if the post exists, false otherwise.
     */
    public function postExists(Post $post);
}

数据映射器 PDO 实现:

<?php

namespace Domain\Infrastructure\Mapper\Post;

use PDO;
use Domain\Model\Post\Post;
use Domain\Infrastructure\Mapper\Post\PostMapper;

/**
 * PDO post mapper.
 */
class PdoPostMapper implements PostMapper {

    /**
     * Database connection.
     * 
     * @var PDO
     */
    private $connection;

    /**
     * @param PDO $connection Database connection.
     */
    public function __construct(PDO $connection) {
        $this->connection = $connection;
    }

    /**
     * Save a post.
     * 
     * @param Post $post Post.
     * @return Post Post entity with id automatically assigned upon persisting.
     */
    public function savePost(Post $post) {
        /*
         * If $post->getId() is set, then call $this->updatePost() 
         * to update the existing post record in the database.
         * Otherwise call $this->insertPost() to insert a new 
         * post record in the database.
         */
        // ...
    }

    /**
     * Fetch a post by id.
     * 
     * @param int $id Post id.
     * @return Post|null Post.
     */
    public function fetchPostById(int $id) {
        //...    
    }

    /**
     * Fetch all posts.
     * 
     * @return Post[] Post list.
     */
    public function fetchAllPosts() {
        //...
    }

    /**
     * Check if a post exists.
     * 
     * @param Post $post Post.
     * @return bool True if the post exists, false otherwise.
     */
    public function postExists(Post $post) {
        //...
    }

    /**
     * Update an existing post.
     * 
     * @param Post $post Post.
     * @return Post Post entity with the same id upon updating.
     */
    private function updatePost(Post $post) {
        // Persist using SQL and PDO statements...
    }

    /**
     * Insert a post.
     * 
     * @param Post $post Post.
     * @return Post Post entity with id automatically assigned upon persisting.
     */
    private function insertPost(Post $post) {
        // Persist using SQL and PDO statements...
    }

}

最后,您的控制器将如下所示。通过阅读它的代码,它的作用就很明显了:只是将用户输入推送到服务层。服务层的使用提供了可重用性的巨大优势。

<?php

namespace App\Controller;

use App\Service\Post;
use App\Service\Http\Upload\ImageHandler;

class PostController {

    private $imageHandler;
    private $postService;

    public function __construct(ImageHandler $imageHandler, Post $postService) {
        $this->imageHandler = $imageHandler;
        $this->postService = $postService;
    }

    public function storePost(Request $request) {
        $this->imageHandler->handle(
            $request->file('upload'),
            $request->input('CKEditorFuncNum')
        );

        $post = $this->postService->createPost(
            request('content'),
            request('template'),
            request('is_published')
            /* , ... */
        );

        return redirect()->route('posts');
    }

}

PS:请记住,模型层绝不能知道其数据来自何处,也没有关于传递给它的数据是如何创建的。因此,模型层必须对浏览器、请求、控制器、视图、响应等一无所知。它只接收原始值,objects,或 DTO(“数据传输objects") 作为参数 - 例如,参见上面的存储库和数据映射器。

PS 2:请注意,很多框架都在谈论 repositories,但实际上,它们是谈论 数据映射器 。我的建议是在您的思想和代码中遵循 Fowler 的约定。因此,创建 数据映射器 以直接访问持久性 space(数据库、文件系统等)。如果您的项目变得更加复杂,或者如果您只想拥有 collection-like 抽象,那么您可以在映射器之上添加一个新的抽象层: 存储库 .

资源

以及 sitepoint.com 上 Gervasio 的 4 部分精彩系列: