MVC(Laravel)在哪里添加逻辑


136

假设每当我执行CRUD操作或以特定方式修改关系时,我也想做其他事情。例如,每当有人发布帖子时,我也希望将某些内容保存到表中进行分析。也许不是最好的例子,但总的来说,这种“分组”功能很多。

通常我会看到这种逻辑被放入控制器。在您要在很多地方重现此功能之前,这一切都很好。当您开始使用局部函数,创建API并生成虚拟内容时,将其保持干燥就成为一个问题。

我所看到的管理事件的方法是事件,存储库,库以及添加到模型中。这是我对每种方法的理解:

服务:大多数人可能会在这里放置此代码。我对服务的主要问题是,有时很难在其中找到特定的功能,而当人们专注于使用Eloquent时,我感到它们被忘记了。我怎么会知道我publishPost()何时需要在库中调用方法$post->is_published = 1

我认为此方法运行良好的唯一条件是仅使用服务(理想情况下,使控制器能够以某种方式使Eloquent无法访问)。

最终,如果您的请求通常遵循您的模型结构,这似乎只会创建大量多余的文件。

存储库:据我了解,这基本上就像是服务,但是有一个接口,您可以在ORM之间进行切换,而这是我不需要的。

事件:从某种意义上讲,我认为这是最优雅的系统,因为您知道总是在Eloquent方法上调用模型事件,因此您可以像平时那样编写控制器。我可以看到它们变得凌乱,并且如果有人有使用事件进行关键耦合的大型项目的示例,我想看看。

模型:传统上,我会有执行CRUD并处理关键耦合的类。实际上,这使事情变得容易,因为您知道CRUD的所有功能以及其中要做的一切。

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

每种方法的优点/缺点是什么?我想念什么吗?


3
你能把问题最小化吗?
Alpha

3
您也可以检查一下
Alpha 2014年

1
“当我只需执行$ post-> is_published = 1时,我怎么知道需要在库中调用方法publishPost()?” 文档?
ceejayoz '16

雄辩的语言和ORMS的优点之一是,无需大量文档就可以更轻松地使用它们?
Sabrina Leggett

1
感谢您发布此信息。我为同样的问题而苦恼,发现您的帖子和答案非常有帮助。最终,我决定Laravel不能为超出快速且肮脏的Ruby-on-Rails网站的所有内容提供良好的体系结构。到处都是Tratte,难以找到类的功能,并且到处都有大量自动魔术垃圾。 ORM从未使用过,如果您正在使用它,则可能应该使用NoSQL。
亚历克斯·巴克

Answers:


171

我认为,只要遵循SOLID原则,您提出的所有模式/架构都是非常有用的。

对于在何处添加逻辑,我认为参考“ 单一责任原则”很重要。另外,我的回答还认为您正在从事中型/大型项目。如果是在页面上扔东西,请忘记此答案并将其全部添加到控制器或模型中。

简短的答案是:对您(使用服务)有意义

长答案:

控制器控制器的职责是什么?当然,您可以将所有逻辑放在控制器中,但这是控制器的责任吗?我不这么认为。

对我来说,控制器必须接收请求并返回数据,而这里并不是放置验证,调用db方法等的地方。

模型:这是添加逻辑的好地方,例如在用户注册或更新帖子的投票计数时发送欢迎电子邮件吗?如果您需要从代码中的其他位置发送相同的电子邮件怎么办?您是否创建静态方法?如果该电子邮件需要其他模型的信息怎么办?

我认为模型应该代表一个实体。随着Laravel,我只用模型类添加之类的东西fillableguardedtable和关系(这是因为我使用Repository模式,否则模型本来也有saveupdatefind,等方法)。

存储库(Repository Pattern):一开始我对此很困惑。而且,像您一样,我认为“好吧,我使用MySQL就是这样。”。

但是,我已经平衡了使用存储库模式的利弊,现在我使用它。我认为现在,此时此刻,我只需要使用MySQL。但是,如果从现在起三年后,我需要更改为MongoDB之类的东西,那么大部分工作就完成了。全部以一个额外的接口和一个为代价$app->bind(«interface», «repository»)

事件(观察者模式):事件对于可以在任何给定时间在任何类上抛出的事件很有用。例如,考虑向用户发送通知。在需要时,您可以触发事件以在应用程序的任何类中发送通知。然后,您可以拥有一个类似的类UserNotificationEvents来处理所有触发的用户通知事件。

服务:到目前为止,您可以选择向控制器或模型添加逻辑。对我来说,在Services中添加逻辑是很有意义的。面对现实,服务是类的一个奇特的名字。在您的应用程序中,您可以拥有尽可能多的类。

举个例子:不久前,我开发了类似Google Forms的东西。我开始与一个CustomFormService并结束了CustomFormServiceCustomFormRenderCustomFieldServiceCustomFieldRenderCustomAnswerServiceCustomAnswerRender。为什么?因为这对我来说很有意义。如果您与团队合作,则应将逻辑放在适合团队的位置。

使用服务vs控制器/模型的优点是您不受单个控制器或单个模型的约束。您可以根据应用程序的设计和需求创建任意数量的服务。除此之外,在应用程序的任何类中调用服务的优势。

这很长,但是我想向您展示如何构造应用程序:

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

我将每个文件夹用于特定功能。例如,Validators目录包含一个BaseValidator类,负责根据$rules$messages特定验证器(通常每个模型一个)来处理验证。我可以很容易地将此​​代码放入Service中,但是对于我来说,为此有一个特定的文件夹是有意义的,即使它仅在服务中使用(目前)。

我建议您阅读以下文章,因为它们可能会对您有所帮助:

Dayle ReesCodeBright的作者)打破常规:尽管我做了一些修改以满足自己的需求,但我还是把这一切放在一起了。

使用 Chris Goosey的“ 存储库和服务”在Laravel中解耦代码:这篇文章很好地解释了什么是服务和存储库模式以及它们如何组合在一起。

Laracasts还具有简化存储库单一职责存储库,它们是带有实际示例的良好资源(即使您必须付费)。


3
很好的解释。这是我目前所处的位置-在当前项目中,我将业务逻辑放入模型中,并且实际上运行得很好。我们当然需要对SOLID进行一些改动,但是,这还没有真正使我们陷入困境。它很快,有点脏,但到目前为止,我们的项目非常干燥,因为它很干。我现在肯定会坚持他们的做法,因为他们可以完成工作,但是在任何未来的项目中,我可能只会采用标准的东西,这听起来像是存储库已经成为标准。
Sabrina Leggett 2014年

2
很高兴您找到了一种对您有意义的方法。请小心您今天所做的假设。我已经在一个项目上工作了3年以上,最终得到了具有5000多个代码行的控制器和模型。祝您的项目好运。
路易斯·克鲁兹

也有点脏,但我当时正在考虑使用特征来避免模型变得庞大。这样,我就可以将它们分开一些
Sabrina Leggett

本文很好地阐明了使用服务的合理性。在您的Form示例中,使用服务确实很有意义,但是他解释了如何进行服务,这就是当逻辑与模型直接相关时,他将其放入该模型中。justinweiss.com/articles/where-do-you-put-your-code
Sabrina Leggett

我真的很喜欢这个解释。我有一个问题:您提到过不要将验证放在控制器中,那么您认为进行验证的最佳位置在哪里?许多人建议将其放入扩展的Request类(以及我们目前正在做的事情)中,但是如果我不仅要对http request进行验证,还要对artisan命令等进行验证,那真的是个好地方吗?
kingshark

24

我想对自己的问题发表回应。我可以谈论几天,但是我将尝试快速发布此消息,以确保我能理解它。

我最终利用了Laravel提供的现有结构,这意味着我主要将文件保存为Model,View和Controller。我也有一个Libraries文件夹,用于不是模型的可重用组件。

我没有在服务/图书馆中包装我的模型。提供的所有原因都不能100%使我相信使用服务的好处。虽然我可能是错的,但据我所知,它们只会导致大量额外的几乎为空的文件,在使用模型时,我需要创建它们并在它们之间进行切换,而且确实减少了雄辩的使用(尤其是在检索模型时) (例如,使用分页,合并范围等)。

我将业务逻辑放入模型中,并直接从我的控制器进行雄辩的访问。我使用多种方法来确保业务逻辑不会被绕过:

  • 存取器和更改器: Laravel具有出色的存取器和更改器。如果我想在帖子从草稿移到发布时执行操作,可以通过创建函数setIsPublishedAttribute并在其中包含逻辑来调用此操作
  • 覆盖创建/更新等:您始终可以在模型中覆盖Eloquent方法以包括自定义功能。这样,您可以在任何CRUD操作上调用功能。编辑:我认为在较新的Laravel版本中存在覆盖创建的错误(因此我使用了现在在引导中注册的事件)
  • 验证:我以相同的方式挂接我的验证,例如,我将通过覆盖CRUD函数以及必要时访问器/更改器来运行验证。有关更多信息,请参见Esensi或dwightwatson / validation。
  • 魔术方法:在适当的地方,我使用模型的__get和__set方法连接功能。
  • 扩展口才:如果您想对所有更新/创建都执行某项操作,您甚至可以扩展口才并将其应用于多个模型。
  • 活动:这很简单,通常也可以达成此目的。我认为事件的最大缺点是很难跟踪异常(对于Laravel的新事件系统而言,可能不是新情况)。我还喜欢按照事件的执行方式而不是调用它们的时间来对事件进行分组……例如,有一个MailSender订阅服务器,它监听发送邮件的事件。
  • 添加Pivot / BelongsToMany事件:最长的挣扎之一是如何将行为附加到对EmiratesToMany关系的修改中。例如,每当用户加入一个组时执行一个动作。为此,我几乎已经完成了自定义库的完善工作。我还没有发布它,但是它可以运行!会尽快尝试发布链接。编辑我最终将所有的枢轴都变成了普通模型,我的生活变得如此轻松多了...

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

  • 组织:是的,如果您在模型中包含更多逻辑,则可以更长一些,但是总的来说,我发现我的模型中有75%仍然很小。如果选择组织较大的文件,则可以使用特征来完成(例如,根据需要为模型创建一个文件夹,其中包含更多文件,例如PostScopes,PostAccessors,PostValidation等)。我知道这不一定是什么特性,但是这个系统可以正常工作。

附加说明:我觉得将模型包装到服务中就像是一把瑞士军刀,里面有很多工具,并在它周围建造另一把刀,基本上可以做同样的事情?是的,有时您可能想用胶带将刀片剥离或确保两个刀片一起使用...但是通常还有其他方法可以实现...

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


2
有趣而有效的想法。但是我很好奇-如果将业务逻辑绑定到与Eloquent关联的模型(与数据库关联的模型),如何对它进行单元测试?
JustAMartin

code.tutsplus.com/tutorials/…或者您可以使用我所说的事件,如果您想进一步细分
Sabrina Leggett

1
@JustAMartin您确定不能仅在单元测试中使用数据库吗?不这样做的原因是什么?许多人同意,通常可以在单元测试中使用数据库。(包括Martin Fowler,martinfowler.com/bliki/UnitTest.html:“我不认为将double用作外部资源是绝对原则。如果与该资源进行对话足够稳定和快速,那么我们就没有理由不这样做它在您的单元测试中”)
Alex P.

@ AlexP11223是的,这很有道理。我尝试将SQLite集成为我的测试数据库,尽管SQLite有一些严重的限制,但必须在Laravel迁移和自定义查询(如果有)中加以考虑,但总体上它还不错。当然,那不是严格的单元测试,而是功能测试,但是这样更有效。但是,如果您想完全隔离地测试模型(作为严格的单元测试),则可能需要大量的附加代码(模拟等)。
JustAMartin

22

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

  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


看来服务就是我在帖子中所说的库。我认为如果您不需要使用多个ORMS,这比存储库要好,但是问题是您必须迁移整个项目(不必与事件相关),并且看起来它只是最终反映了Model结构,因此您已经拥有了所有这些额外的文件。为什么不将其包括在模型中?至少这样您就没有多余的文件了。
Sabrina Leggett 2014年

这是一个有趣的问题,@ SabrinaGelbart,我被教导让模型代表数据库实体,并且不持有任何逻辑。这就是我创建那些名为服务的额外文件的原因:保存所有逻辑和任何额外的操作。我不确定您之前描述的事件的全部含义是什么,但是我认为使用服务并使用Laravel的事件,我们可以使所有服务方法在开始和结束时触发事件。这样,任何事件都可以与逻辑完全分离。你怎么看?
鲁本斯·马里努佐

我也被教导关于模型...很高兴能得到关于为什么的很好的解释(也许是依赖问题)?
Sabrina Leggett 2014年

我喜欢这种方法!我一直在搜索互联网,以了解如何处理模型的逻辑,查看存储库,但是它看起来过于复杂且无用。服务是一个好主意。我的问题是在应用程序文件夹中创建Services文件夹之后,您是否必须将其包含在bootstrap / start.php或用于引导的任何位置中,因为我看着您的git无法找到它?@RubensMariuzzo。该应用程序是否会自动变为可用状态?所以我们可以只使用CongregationService :: getCongregations(); ??
Oguzhan 2014年

1
如果您正在做的只是a,$congregation->save();则可能不需要存储库。但是,您可能会看到数据访问需求随时间增加。你可能会开始有需求,$congregation->destroyByUser()$congregationUsers->findByName($arrayOfSelectedFields);等等。为什么不将服务与数据访问需求脱钩。让您的其余应用程序处理从回购协议返回的对象/数组,并仅处理操作/格式化/等...您的回购协议将会增长(但将它们拆分为不同的文件,最终项目的复杂性必须驻留在某个地方)。
prograhammer

12

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

简短答案:

  • 使用laravel的Request对象自动验证您的输入,然后将数据保留在请求中(创建模型)。由于所有用户输入都可以请求中直接获得因此我认为在此处执行此操作很有意义。
  • 使用laravel的Job对象执行需要单个组件的任务,然后简单地分派它们。我认为Job包含服务类。他们执行任务,例如业务逻辑。

长(长)答案:

在需要时使用 存储库存储库注定会变得过大,并且在大多数情况下,它们仅用作accessor模型的。我觉得它们肯定有一定用处,但是除非您正在开发一个大型应用程序,而该应用程序需要具有足够的灵活性才能完全抛弃laravel,否则请远离存储库。稍后您将感谢自己,您的代码将更加直接。

问问自己,是否有可能要更改PHP框架 laravel不支持的数据库类型。

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

除上述内容外,请不要在Eloquent之类的一流ORM上方拍打图案。您只是在添加不必要的复杂性,这根本不会使您受益。

谨慎地使用服务: 对我而言,服务类只是存储业务逻辑以执行具有给定依赖性的特定任务的地方。Laravel开箱即用,称为“作业”,并且它们比自定义Service类具有更大的灵活性。

我觉得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
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    /**
     * Save the post.
     *
     * @param Post $post
     *
     * @return bool
     */
    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;

        // Perform other tasks, maybe fire an event, dispatch a job.

        if ($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 true;
        }

        return false;
    }
}

控制器

namespace App\Http\Controllers;

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

class PostController extends Controller
{

   /**
    * Creates a new post.
    *
    * @return string
    */
    public function store(PostRequest $request)
    {
        if ($request->persist(new Post())) {
            flash()->success('Successfully created new post!');
        } else {
            flash()->error('There was an issue creating a post. Please try again.');
        }

        return redirect()->back();
    }

   /**
    * Updates a post.
    *
    * @return string
    */
    public function update(PostRequest $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->persist($post)) {
            flash()->success('Successfully updated post!');
        } else {
            flash()->error('There was an issue updating this post. Please try again.');
        }

        return redirect()->back();
    }
}

在上面的示例中,请求输入被自动验证,我们要做的就是调用persist方法并传递一个新的Post。我认为可读性和可维护性应始终胜过复杂且不需要的设计模式。

然后,您也可以使用完全相同的persist方法来更新帖子,因为我们可以检查帖子是否已经存在,并在需要时执行交替逻辑。


但是-是不是“应该”排队工作?有时我们可能确实希望将其排入队列,但并非一直如此。为什么不使用命令呢?如果您想编写一些可以作为命令,事件或队列执行的业务逻辑,该怎么办?
萨布丽娜·莱格特

1
作业无需排队。您可以通过在ShouldQueueLaravel提供的作业上实现接口来指定。如果要在命令或事件中编写业务逻辑,只需在这些事件/命令中触发作业即可。Laravels的工作非常灵活,但最终它们只是普通的服务类。
史蒂夫·鲍曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.