MVC(Laravel)中在哪里添加逻辑?

173
假设每当我执行CRUD操作或以特定方式修改关系时,我还想做其他事情。例如,每当有人发布帖子时,我也想将某些内容保存到分析表中。可能不是最好的例子,但通常存在这种“分组”功能。
通常,我会将此类逻辑放入控制器中。这很好,直到您想在许多地方重现此功能为止。当您开始涉及部分内容、创建API和生成虚拟内容时,会出现DRY问题。
我看到的处理方法包括事件、存储库、库和添加到模型中。以下是我的理解:
服务:这是大多数人可能会放置此代码的位置。我对服务的主要问题是有时很难在其中找到特定功能,并且当人们专注于使用Eloquent时,它们会被遗忘。当我可以只使用 $post->is_published = 1 时,怎么知道我需要在库中调用一个名为publishPost() 的方法呢?
我唯一能看到这个工作良好的条件是如果你只使用服务(并且最好从控制器完全禁止访问Eloquent)。
归根结底,如果您的请求通常遵循您的模型结构,则似乎只会创建大量额外的不必要文件。
存储库:据我所知,这基本上就像服务,但有一个接口,所以您可以在ORM之间切换,但我不需要。
事件:从某种意义上讲,我认为这是最优美的系统,因为您知道Eloquent方法始终会调用您的模型事件,因此您可以像往常一样编写控制器。我可以看到这些事件变得混乱,如果有人有使用事件进行关键耦合的大型项目示例,我想看看。
模型:传统上,我会有执行CRUD并处理关键耦合的类。这实际上使事情变得容易,因为您知道与CRUD + 需要完成的任何其他功能相关的所有功能都在那里。

简单,但在MVC架构中,这通常不是我所看到的。但从某种意义上来说,我更喜欢这种方法而不是服务,因为它更容易找到,并且有较少的文件需要跟踪。但它可能会变得有些凌乱。我想听听这种方法的缺点以及为什么大多数人似乎不这样做。

每种方法的优缺点是什么?我是否遗漏了什么?


7
你能简化一下你的问题吗? - The Alpha
3
你可以点击这里查看。 - The Alpha
1
我怎么知道在库中需要调用publishPost()方法,当我只需要 $post->is_published = 1?文档呢? - ceejayoz
1
关于Eloquent和ORM的优点之一就是不需要大量文档,使用它们更加容易。 - Sabrina Leggett
2
感谢您发布这篇文章。我也遇到了同样的问题,发现您的帖子和答案非常有帮助。最终,我决定 Laravel 并不适合用于超出快速开发 Ruby-on-Rails 网站的任何项目。到处都是陷阱,很难找到类和函数,还有大量的自动垃圾代码。ORM 从来没有起过作用,如果您正在使用它,那么您应该考虑使用 NoSQL。 - Alex Barker
太棒了,谢谢你的问题!下面的人给出了很好的答案。为我的项目节省了时间。 - Qazi Ammar
4个回答

204
我认为,只要遵循SOLID原则,您提出的所有模式/架构都非常有用。
对于“在哪里添加逻辑”,我认为重要的是参考单一职责原则。此外,我的答案考虑到您正在开展中型/大型项目。如果这是一个“将某些东西扔在页面上”的项目,请忘记这个答案,并将所有内容添加到控制器或模型中。
简短的回答是:在有服务的情况下,放在您认为合适的位置
长答案如下: 控制器:控制器的职责是什么?当然,您可以将所有逻辑放在控制器中,但这是控制器的责任吗?我不这么认为。
对我来说,控制器必须接收请求并返回数据,这不是放置验证、调用数据库方法等的地方。 模型: 这是添加诸如用户注册时发送欢迎电子邮件或更新帖子投票计数等逻辑的好地方吗?如果您需要从代码的另一个位置发送相同的电子邮件,该怎么办?您会创建一个静态方法吗?如果该电子邮件需要另一个模型的信息怎么办?

我认为模型应该代表一个实体。对于 Laravel,我只使用模型类来添加像 fillableguardedtable 和关系之类的东西(这是因为我使用存储库模式,否则模型也将具有 saveupdatefind 等方法)。

存储库(存储库模式): 起初,我对此非常困惑。而且,像您一样,我认为“好吧,我使用 MySQL 就可以了。”

然而,我已平衡使用存储库模式的利弊,并现在使用它。我认为,现在,就在这个时刻,我只需要使用 MySQL。但是,如果三年后我需要更改为类似 MongoDB 的东西,大部分工作都已完成。所有这些都是以额外接口和 $app->bind(«interface», «repository») 的代价。

事件 (观察者模式): 事件对于可以在任何时间抛出的事情非常有用。例如,想象一下向用户发送通知。当您需要时,您可以触发事件,在应用程序的任何类中发送通知。然后,您可以拥有一个名为UserNotificationEvents的类来处理所有与用户通知相关的已触发事件。

服务: 目前为止,您可以选择将逻辑添加到控制器或模型中。对于我来说,将逻辑添加到服务中是有意义的。让我们面对现实,服务是一个高级名称,代表类。您可以在应用程序中拥有尽可能多的类,只要对您有意义即可。

举个例子:不久前,我开发了类似Google表单的东西。我从CustomFormService开始,最终有了CustomFormServiceCustomFormRenderCustomFieldServiceCustomFieldRenderCustomAnswerServiceCustomAnswerRender。为什么?因为这对我来说是有意义的。如果你与一个团队合作,应该把你的逻辑放在对团队有意义的地方。

使用服务(Service)而不是控制器(Controller)/模型(Model)的优势在于,您不受单个控制器或单个模型的限制。根据您的应用程序的设计和需求,可以创建尽可能多的服务。此外,调用服务(Service)的优点是您可以在应用程序的任何类中调用它。

虽然这篇文章很长,但我想向您展示我如何构建我的应用程序:

app/
    controllers/
    MyCompany/
        Composers/
        Exceptions/
        Models/
        Observers/
        Sanitizers/
        ServiceProviders/
        Services/
        Validators/
    views
    (...)

我为每个文件夹指定特定的功能。例如,Validators 目录包含一个 BaseValidator 类,负责根据特定验证器(通常是每个模型的一个)的规则和消息处理验证。虽然这段代码也可以轻松地放在一个服务中,但对我来说,在服务内使用时有一个专用的文件夹是有意义的(至少现在是这样)。
我建议您阅读以下文章,因为它们可能会更好地解释这些内容: Breaking the Mold by Dayle Rees(CodeBright 的作者):这是我将所有东西整合在一起的地方,尽管我改变了一些东西以适应我的需求。 Decoupling your code in Laravel using Repositories and Services by Chris Goosey:这篇文章很好地解释了什么是服务和仓库模式以及它们如何结合在一起。

Laracasts还有简化仓库单一职责原则这两个资源,提供了实用的例子(尽管需要付费)。


3
很好的解释。目前我所处的项目中,我将业务逻辑放在了模型中,这实际上效果非常好。我们肯定需要稍微扭曲一下SOLID原则,但是它并没有给我们带来麻烦。虽然有些粗糙和快速,但是因为代码复用度高,所以我们的项目非常易于维护。目前我肯定会继续沿用这种方式,因为它能够完成工作。但在未来的任何项目中,我可能会选择遵循标准做法,而似乎repository已经成为了一种标准。 - Sabrina Leggett
2
很高兴你找到了一种对你有意义的方法。但要小心你所做的假设,尤其是今天的假设。我曾经参与一个项目三年多,最终得到了5000多行代码的控制器和模型。祝你的项目好运。 - Luís Cruz
也有点不太好,但我在考虑使用特征来避免模型变得庞大。这样我就可以将它们分开一些。 - Sabrina Leggett
本文很好地阐述了何时使用服务是有意义的。在您的表单示例中,使用服务确实是有意义的,但他解释了他如何做到这一点,即当逻辑直接与模型相关时,他将其放入该模型中。http://www.justinweiss.com/articles/where-do-you-put-your-code/ - Sabrina Leggett
我真的很喜欢这个解释。我有一个问题:你提到不要在控制器中进行验证,那么你认为最好的验证位置在哪里?许多人建议将其放入扩展请求类中(也是我们目前所做的),但如果我不仅想在HTTP请求上进行验证,还想在Artisan命令等其他地方进行验证,那么这真的是一个好的位置吗? - kingshark

31

我想回答自己的问题。我可以谈论这个话题好几天,但我打算快速发布这篇文章以确保我把它发出去。

我最终决定采用 Laravel 提供的现有结构,也就是将我的文件主要保留为 Model、View 和 Controller。我还有一个 Libraries 文件夹,用于存放不太是模型的可重用组件。

我没有将我的模型包装在服务/库中。提供的所有原因都没有完全说服我使用服务的好处。虽然我可能错了,但据我所见,它们只会导致我需要创建和在模型之间切换的大量额外几乎为空的文件,并且实际上减少了使用 eloquent 的好处(特别是当涉及到检索模型时,例如使用分页、范围等)。

我将业务逻辑放在模型中并直接从控制器访问 eloquent。我使用多种方法来确保业务逻辑不会被绕过:

  • 访问器和修改器:Laravel 有很好的访问器和修改器。如果我想在将帖子从草稿移到已发布状态时执行某个操作,我可以通过创建函数 setIsPublishedAttribute 并在其中包含逻辑来调用此操作
  • 覆盖 Create/Update 等操作:您可以始终在模型中覆盖 Eloquent 方法以包含自定义功能。这样,您可以在任何 CRUD 操作上调用功能。编辑:我认为在较新的 Laravel 版本中覆盖创建时存在一个错误(因此现在我使用在 boot 中注册的事件)。
  • 验证:我通过同样的方式挂接我的验证,例如,在需要时我会通过覆盖 CRUD 函数和访问器/修改器来运行验证。有关更多信息,请参见 Esensi 或 dwightwatson/validating。
  • 魔术方法:我使用模型的 __get 和 __set 方法来适当地挂接功能
  • 扩展 Eloquent:如果您想在所有更新/创建上执行某个操作,甚至可以扩展 eloquent 并将其应用于多个模型。
  • 事件: 这是一个直截了当且普遍认可的处理方式。我认为使用事件的最大缺点是难以追踪异常(也许 Laravel 的新事件系统不再有这个问题)。我还喜欢按它们所做的事情分组我的事件,而不是按它们何时被调用...比如,有一个 MailSender 订阅者,它监听发送邮件的事件。
  • 添加 Pivot/BelongsToMany 事件: 我最长时间困扰的事情之一是如何将行为附加到 belongsToMany 关系的修改上。比如,每当用户加入一个组时执行一个操作。我快要完成一个自定义库来解决这个问题。我还没有发布它,但它是有效的!我会尝试尽快发布链接。编辑 最终我将所有的 pivot 都变成了普通模型,我的生活变得轻松多了...

解决人们对使用模型的担忧:

  • 组织: 是的,如果在模型中包含更多逻辑,它们可能会变得更长,但是总体上,我发现我的 75% 模型仍然相当小。如果我选择对更大的模型进行组织,我可以使用 traits(例如,创建一个文件夹来存放模型,并根据需要添加一些文件,如 PostScopes、PostAccessors、PostValidation 等)。我知道这不一定是 traits 的作用,但这个系统完全没有问题。

附加说明: 我觉得将模型包装在服务中就像拥有一个瑞士军刀,有很多工具,然后在它周围建立另一把几乎做同样事情的刀?是的,有时你可能想要切掉刀片或确保两个刀片一起使用...但通常还有其他方法可以做到这一点...

何时使用服务:本文很好地阐述了何时使用服务的GREAT示例(提示:并不是经常)。他说基本上当您的对象使用多个模型或模型在其生命周期的奇怪部分时,这是有意义的。http://www.justinweiss.com/articles/where-do-you-put-your-code/


2
有趣且有价值的想法。但我很好奇 - 如果业务逻辑与模型紧密相连,而模型又与Eloquent紧密相连,而Eloquent又与数据库紧密相连,那么你是如何对业务逻辑进行单元测试的呢? - JustAMartin
1
你可以阅读这篇文章:http://code.tutsplus.com/tutorials/testing-like-a-boss-in-laravel-models--net-30087。如果你想进一步分解,也可以使用事件。 - Sabrina Leggett
1
@JustAMartin,你确定在单元测试中不能使用数据库吗?不这样做的原因是什么?许多人认为,在单元测试中使用数据库通常是可以接受的。(包括Martin Fowler在内,http://martinfowler.com/bliki/UnitTest.html:“我不把使用doubles来代替外部资源视为绝对规则。如果与资源的交互稳定且足够快,则没有理由不在单元测试中这样做。”) - Alex P.
@AlexP11223 是的,那很有道理。我尝试将SQLite集成为我的测试数据库,总体上来说进展顺利,尽管SQLite有一些严重的限制,这些限制必须在Laravel迁移和自定义查询中加以考虑(如果有的话)。当然,这些测试不是严格的单元测试,而是功能测试,但这样做更有效率。不过,如果你想要完全隔离地测试你的模型(作为严格的单元测试),可能需要大量额外的代码(模拟等)。 - JustAMartin
对我来说这听起来是一个少于100个控制器调用数据访问层的好项目。对于大于此规模的项目,我强烈建议通过服务层传递所有数据访问调用。这确保了随着需求的变化(它们总是会发生),在代码中有一个“单一点”来处理所有“获取x类型数据”的调用。您不想查找50多个对该模型的调用,并确定它是否以受新业务规则影响的方式使用。 - Adam Lenda
在服务中,我仍然看到一些奇怪的地方出现了重复的事情,你必须无论如何去寻找它们。可以说,事件是确保业务逻辑实际上被调用的更好方法。我不再经常编写Laravel代码,但如果我记得正确,他们的事件系统非常好。我唯一的问题是有时很容易忽略副作用。 - Sabrina Leggett

24

我用来创建控制器和模型之间逻辑的方法是创建一个服务层。基本上,这是我的应用中进行任何操作时的流程:

  1. 控制器获取用户请求的操作并发送参数,然后将所有内容委托给服务类。
  2. 服务类执行与操作相关的所有逻辑:输入验证、事件记录、数据库操作等等......
  3. 模型保存字段信息、数据转换以及属性验证定义的信息。

这就是我的做法:

下面是一个控制器用于创建某个东西的方法:

public function processCreateCongregation()
{
    // Get input data.
    $congregation                 = new Congregation;
    $congregation->name           = Input::get('name');
    $congregation->address        = Input::get('address');
    $congregation->pm_day_of_week = Input::get('pm_day_of_week');
    $pmHours                      = Input::get('pm_datetime_hours');
    $pmMinutes                    = Input::get('pm_datetime_minutes');
    $congregation->pm_datetime    = Carbon::createFromTime($pmHours, $pmMinutes, 0);

    // Delegates actual operation to service.
    try
    {
        CongregationService::createCongregation($congregation);
        $this->success(trans('messages.congregationCreated'));
        return Redirect::route('congregations.list');
    }
    catch (ValidationException $e)
    {
        // Catch validation errors thrown by service operation.
        return Redirect::route('congregations.create')
            ->withInput(Input::all())
            ->withErrors($e->getValidator());
    }
    catch (Exception $e)
    {
        // Catch any unexpected exception.
        return $this->unexpected($e);
    }
}

这是处理操作相关逻辑的服务类:

public static function createCongregation(Congregation $congregation)
{
    // Log the operation.
    Log::info('Create congregation.', compact('congregation'));

    // Validate data.
    $validator = $congregation->getValidator();

    if ($validator->fails())
    {
        throw new ValidationException($validator);
    }

    // Save to the database.
    $congregation->created_by = Auth::user()->id;
    $congregation->updated_by = Auth::user()->id;

    $congregation->save();
}

这是我的模型:

class Congregation extends Eloquent
{
    protected $table = 'congregations';

    public function getValidator()
    {
        $data = array(
            'name' => $this->name,
            'address' => $this->address,
            'pm_day_of_week' => $this->pm_day_of_week,
            'pm_datetime' => $this->pm_datetime,
        );

        $rules = array(
            'name' => ['required', 'unique:congregations'],
            'address' => ['required'],
            'pm_day_of_week' => ['required', 'integer', 'between:0,6'],
            'pm_datetime' => ['required', 'regex:/([01]?[0-9]|2[0-3]):[0-5]?[0-9]:[0-5][0-9]/'],
        );

        return Validator::make($data, $rules);
    }

    public function getDates()
    {
        return array_merge_recursive(parent::getDates(), array(
            'pm_datetime',
            'cbs_datetime',
        ));
    }
}

关于我用于组织 Laravel 应用程序代码的方法,更多信息请参见:https://github.com/rmariuzzo/Pitimi


这是一个有趣的问题@SabrinaGelbart,我被教导要让模型代表数据库实体并且不保持任何逻辑。这就是我创建那些额外的文件命名为服务的原因:用于保存所有逻辑和额外操作。我不确定你之前描述的事件的完整含义是什么,但我认为通过使用服务和Laravel的事件,我们可以使所有服务方法在开始和结束时触发事件。这样,任何事件都可以完全与逻辑分离。你觉得呢? - Rubens Mariuzzo
我也被教过这个关于模型的问题...希望能得到一个好的解释为什么会这样(也许是依赖问题)? - Sabrina Leggett
@OğuzhanPişkin,你需要做两件事情:1)在composer.json中包含你的新目录;2)你需要运行php artisan dump-autoloadcomposer dump-autoload - Rubens Mariuzzo
1
如果你只是在执行 $congregation->save();,那么也许你不需要使用 Repositories。然而,随着时间的推移,你可能会发现自己对数据访问的需求增加了。你可能开始需要 $congregation->destroyByUser()$congregationUsers->findByName($arrayOfSelectedFields); 等操作。为什么不将服务与数据访问需求分离呢?让应用程序的其余部分使用从 repos 返回的对象/数组,并处理操作/格式化等工作。你的 repo 会变得越来越多(但将它们拆分成不同的文件,最终项目的复杂性必须存在于某个地方)。 - prograhammer
我喜欢@RubensMariuzzo所提出的方法:将业务逻辑保留在服务层(您所称的库)中,原因是这样可以遵循单一职责和关注点分离等原则。您的服务层将处理业务逻辑,而您的模型将处理数据库操作。 - Guillermo Mansilla
显示剩余2条评论

19

在我看来,Laravel已经为您提供了许多选项来存储您的业务逻辑。

简短回答:

  • 使用Laravel的Request对象自动验证输入,并在请求中持久化数据(创建模型)。由于所有用户输入都直接可用于请求中,我认为在此处执行此操作是有意义的。
  • 使用Laravel的Job对象执行需要单独组件的任务,然后简单地调度它们。我认为Job包含服务类。它们执行任务,例如业务逻辑。

更长的答案:

必要时使用Repositories: Repositories往往会变得臃肿不堪,大多数情况下仅被用作模型的accessor。我觉得它们确实有些用处,但除非您正在开发一个需要那么多灵活性才能完全摆脱Laravel的庞大应用程序,否则请远离repositories。以后您会感激自己的,您的代码也会更加直观。

问问自己是否可能会更改PHP框架或Laravel不支持的数据库类型。

如果您的答案是“可能不”,则不要实现存储库模式。

除上述之外,请不要在Eloquent等出色的ORM之上添加模式。您只会增加不必要的复杂性,而且这对您没有任何好处。

谨慎使用服务: 对我来说,服务类只是存储业务逻辑以执行具有给定依赖项的特定任务的地方。 Laravel已经为此提供了开箱即用的解决方案,称为“Jobs”,它们比自定义服务类更灵活。

我觉得Laravel为MVC逻辑问题提供了一个全面的解决方案。这只是组织的问题。

例如:

请求

namespace App\Http\Requests;

use App\Post;
use App\Jobs\PostNotifier;
use App\Events\PostWasCreated;
use App\Http\Requests\Request;

class PostRequest extends Request
{
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    public function persist(Post $post)
    {
        if (! $post->exists) {
            // If the post doesn't exist, we'll assign the
            // post as created by the current user.
            $post->user_id = auth()->id();
        }

        $post->title = $this->title;
        $post->description = $this->description;

        $post->save();

        // Maybe we'll fire an event here that we can catch somewhere 
        // else that needs to know when a post was created.
        event(new PostWasCreated($post));

        // Maybe we'll notify some users of the new post as well.
        dispatch(new PostNotifier($post));

        return $post;
    }
}

控制器:

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests\PostRequest;

class PostController extends Controller
{
    public function store(PostRequest $request)
    {
        $request->persist(new Post());

        flash()->success('Successfully created new post!');
        
        return redirect()->back();
    }

    public function update(PostRequest $request, Post $post)
    {
        $request->persist($post);

        flash()->success('Successfully updated post!');
        
        return redirect()->back();
    }
}

在上面的示例中,请求输入会自动进行验证,我们只需要调用persist方法并传入一个新的Post即可。我认为易读性和可维护性应该始终优先于复杂和不必要的设计模式。
您还可以使用完全相同的persist方法来更新帖子,因为我们可以检查帖子是否已存在,并在需要时执行交替逻辑。

2
但是 - 工作不应该被排队吗?有时我们可能确实希望它被排队,但并非总是如此。为什么不使用命令呢?如果您想编写一些业务逻辑,可能会作为命令、事件或排队执行,怎么办? - Sabrina Leggett
3
工作不需要排队,你可以通过实现 Laravel 提供的 ShouldQueue 接口来指定。如果你想在命令或事件中编写业务逻辑,只需在这些事件/命令中触发作业即可。Laravel 的作业非常灵活,但最终它们只是普通的服务类。 - Steve Bauman
1
非常好的观点!此外,工作可以立即通过dispatchSync()方法使用,该方法应忽略ShouldQueue - Bence Szalai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接