使控制器更薄。 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\strings
的 helper
(并且不要忘记在 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
。原则上,该服务将执行以下任务:
- 创建一个实体 - 例如
Domain\Model\Post\Post
- 并设置其属性(title
、content
、is_published
、template
等)在用户输入上。例如,这可以在方法 App\Service\Post::createPost
中完成。
- 将实体存储在数据库中,作为数据库记录。例如,这可以在方法
App\Service\Post::storePost
中完成。
- 其他任务...
关于第一个任务,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 抽象,那么您可以在映射器之上添加一个新的抽象层: 存储库 .
资源
- Keynote: Architecture the Lost Years 罗伯特·C·马丁
- Sandro Mancuso : Crafted Design
- Clean, high quality code...
以及 sitepoint.com 上 Gervasio 的 4 部分精彩系列:
我有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\strings
的 helper
(并且不要忘记在 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
。原则上,该服务将执行以下任务:
- 创建一个实体 - 例如
Domain\Model\Post\Post
- 并设置其属性(title
、content
、is_published
、template
等)在用户输入上。例如,这可以在方法App\Service\Post::createPost
中完成。 - 将实体存储在数据库中,作为数据库记录。例如,这可以在方法
App\Service\Post::storePost
中完成。 - 其他任务...
关于第一个任务,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 抽象,那么您可以在映射器之上添加一个新的抽象层: 存储库 .
资源
- Keynote: Architecture the Lost Years 罗伯特·C·马丁
- Sandro Mancuso : Crafted Design
- Clean, high quality code...
以及 sitepoint.com 上 Gervasio 的 4 部分精彩系列: