ChatGPT解决这个技术问题 Extra ChatGPT

MVC(Laravel)在哪里添加逻辑

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

通常我会看到将这种类型的逻辑放入控制器中。在您想在很多地方重现此功能之前,这一切都很好。当您开始研究部分内容、创建 API 并生成虚拟内容时,保持内容 DRY 成为一个问题。

我见过的管理它的方法是事件、存储库、库和添加到模型。以下是我对每一个的理解:

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

我认为这很好用的唯一条件是你只使用服务(理想情况下,让 Eloquent 无法从控制器中以某种方式访问)。

最终,如果您的请求通常遵循您的模型结构,这似乎只会创建一堆额外的不必要的文件。

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

事件:从某种意义上说,我认为这是最优雅的系统,因为您知道模型事件总是会在 Eloquent 方法上调用,因此您可以像往常一样编写控制器。我可以看到这些变得混乱,如果有人有使用事件进行关键耦合的大型项目的示例,我希望看到它。

模型:传统上我会拥有执行 CRUD 并处理关键耦合的类。这实际上使事情变得容易,因为您知道围绕 CRUD 的所有功能 + 在那里必须完成的任何事情。

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

每种方法的优点/缺点是什么?我错过了什么吗?

你能最小化你的问题吗?
您也可以check this
“当我可以执行 $post->is_published = 1 时,我怎么知道我需要在库中调用方法 publishPost()?”文档?
关于 eloquent 和 ORMS 的优点之一是在没有大量文档的情况下使用它们更容易吗?
感谢您发布此信息。我在同样的问题上挣扎,发现你的帖子和回答非常有帮助。最终,我决定 Laravel 不会为任何超出快速而肮脏的 Ruby-on-Rails 网站的东西提供良好的架构。到处都是垃圾,很难找到类函数和到处都是大量的自动魔法垃圾。 ORM has never worked 如果您正在使用它,那么您可能应该使用 NoSQL。

s
samrap

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

对于添加逻辑的位置,我认为参考 Single Responsibility Principle 很重要。另外,我的回答认为您正在从事一个中/大型项目。如果它是 throw-something-on-a-page 项目,请忘记此答案并将其全部添加到控制器或模型中。

简短的回答是:对您有意义的地方(通过服务)。

长答案:

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

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

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

我认为模型应该代表一个实体。使用 Laravel,我只使用模型类添加 fillableguardedtable 和关系(这是因为我使用存储库模式,否则模型也会有 save、{5 }、find 等方法)。

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

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

事件 (Observer Pattern): 事件对于可以在任何给定时间在任何类中抛出的东西很有用。例如,考虑向用户发送通知。当您需要时,您可以触发事件以在应用程序的任何类中发送通知。然后,您可以拥有像 UserNotificationEvents 这样的类来处理您为用户通知而触发的所有事件。

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

举个例子:不久前,我开发了类似谷歌表单的东西。我从 CustomFormService 开始,以 CustomFormServiceCustomFormRenderCustomFieldServiceCustomFieldRenderCustomAnswerServiceCustomAnswerRender 结束。为什么?因为这对我来说很有意义。如果你和一个团队一起工作,你应该把你的逻辑放在对团队有意义的地方。

使用服务与控制器/模型的优势在于您不受单个控制器或单个模型的限制。您可以根据应用程序的设计和需求创建任意数量的服务。再加上在应用程序的任何类中调用服务的优势。

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

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

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

我建议您阅读以下文章,因为它们可能会更好地为您解释:

Breaking the Mold by Dayle Rees(CodeBright 的作者):这是我将所有内容放在一起的地方,尽管我更改了一些内容以满足我的需要。

Decoupling your code in Laravel using Repositories and Services by Chris Goosey:这篇文章很好地解释了什么是服务和存储库模式以及它们如何结合在一起。

Laracasts 也有 Repositories SimplifiedSingle Responsibility,它们是带有实际示例的好资源(即使您必须付费)。


很好的解释。这就是我目前所处的位置 - 在当前项目中,我将我的业务逻辑放入模型中,它实际上运行良好。我们肯定需要稍微捏造一下 SOLID,但是,它还没有真正给我们带来麻烦。它很快,有点脏,但到目前为止,我们的项目非常易于维护,因为它非常干燥。我现在绝对可以坚持使用它们,因为它们完成了工作,但是在任何未来的项目中,我可能只会使用任何标准的东西,听起来像是存储库已经成为。
我很高兴你找到了一种对你有意义的方法。请注意您今天所做的假设。我在一个项目上工作了 3 年多,最终得到了具有 5000 多行代码的控制器和模型。祝你的项目好运。
也有点脏,但我正在考虑使用特征来避免模型变得庞大。这样我可以把它们分开一点
本文很好地阐述了何时使用服务是有意义的。在您的表单示例中,使用服务确实有意义,但他解释了他是如何做到的,即当逻辑与模型直接相关时,他将其放入该模型中。 justinweiss.com/articles/where-do-you-put-your-code
我真的很喜欢这个解释。我有一个问题:您提到不要将验证放在控制器中,那么您认为哪里是进行验证的最佳位置?许多人建议将其放入扩展的 Request 类(以及我们目前所做的)中,但是如果我不仅想验证 http 请求,还想验证 artisan 命令等,这真的是一个好地方吗?
S
Sabrina Leggett

我想对我自己的问题发表回应。我可以谈论这个几天,但我会尽量让这个发布快,以确保我得到它。

我最终利用了 Laravel 提供的现有结构,这意味着我主要将文件保存为模型、视图和控制器。我还有一个库文件夹,用于存放并非真正模型的可重用组件。

我没有将我的模型包装在服务/库中。提供的所有理由并没有 100% 让我相信使用服务的好处。虽然我可能是错的,但据我所见,它们只会导致我需要在使用模型时创建和切换大量几乎空的文件,并且还确实降低了使用 eloquent 的好处(尤其是在检索模型时,例如,使用分页、范围等)。

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

访问器和修改器:Laravel 有很好的访问器和修改器。如果我想在帖子从草稿移动到已发布时执行操作,我可以通过创建函数 setIsPublishedAttribute 并在其中包含逻辑来调用它

覆盖创建/更新等:您始终可以覆盖模型中的 Eloquent 方法以包含自定义功能。这样您就可以在任何 CRUD 操作上调用功能。编辑:我认为在较新的 Laravel 版本中覆盖创建存在一个错误(所以我使用现在在引导中注册的事件)

验证:我以同样的方式挂钩我的验证,例如,如果需要,我将通过覆盖 CRUD 函数和访问器/突变器来运行验证。有关更多信息,请参阅 Esensi 或 dwightwatson/validating。

魔术方法:我使用模型的 __get 和 __set 方法在适当的地方挂钩功能

扩展 Eloquent:如果您想对所有更新/创建执行操作,您甚至可以扩展 eloquent 并将其应用于多个模型。

事件:这是一个直截了当且普遍同意的地方。我认为事件的最大缺点是异常难以追踪(可能不是 Laravel 新事件系统的新案例)。我还喜欢按照事件的执行而不是调用的时间对事件进行分组……例如,有一个 MailSender 订阅者,它侦听发送邮件的事件。

添加 Pivot/BelongsToMany 事件:我最苦恼的一件事是如何将行为附加到 belongsToMany 关系的修改中。例如,每当用户加入群组时执行操作。我几乎已经为此完善了一个自定义库。我还没有发布它,但它是功能性的!将尝试尽快发布链接。编辑我最终把我所有的支点都变成了正常的模型,我的生活变得轻松多了......

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

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

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

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


有趣和有效的想法。但我很好奇 - 如果您的业务逻辑与与 Eloquent 相关联的模型相关联,那么您将如何对它进行单元测试,而后者又与数据库相关联?
code.tutsplus.com/tutorials/… 或者如果您想进一步分解,可以使用我所说的事件
@JustAMartin 您确定不能在单元测试中只使用数据库吗?不做的理由是什么?许多人同意在单元测试中使用数据库通常是可以的。 (包括 Martin Fowler,martinfowler.com/bliki/UnitTest.html:“我不会将使用双精度作为外部资源的绝对规则。如果与资源的对话对您来说足够稳定且足够快,那么没有理由不在您的单元测试中这样做” )
@AlexP11223 是的,这是有道理的。我尝试将 SQLite 集成为我的测试数据库,总的来说它运行良好,尽管 SQLite 有一些严重的限制,必须在 Laravel 迁移和自定义查询(如果有的话)中加以考虑。当然,这些不是严格的单元测试,而是功能测试,但这样更有效。尽管如此,如果你想完全隔离地测试你的模型(作为一个严格的单元测试),它可能需要大量的额外代码(模拟等)。
对我来说,一个只有不到 100 个控制器调用数据访问层的项目听起来很棒。对于比这更大的项目,我强烈建议通过服务层传递所有数据访问调用。这确保了随着需求的变化(他们总是这样做),代码中有一个“单点”,所有对“获取 x 类型的数据”的调用都被处理。您不想追查对该模型的 50 多次调用并确定它是否以受新业务规则影响的方式使用。
A
Alex P.

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

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

我就是这样做的:

这是控制器创建某些东西的方法:

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,我认为这比存储库更好,但问题是您必须迁移整个项目(您不必与事件有关),而且看起来像它最终只是镜像了模型结构,所以你刚刚得到了所有这些额外的文件。为什么不直接将其包含在模型中?至少这样你就没有多余的文件了。
这是一个有趣的问题@SabrinaGelbart,我被教导让模型代表数据库实体并且不包含任何逻辑。这就是我创建那些名为服务的额外文件的原因:保存所有逻辑和任何额外操作。我不确定您之前描述的事件的全部含义是什么,但我认为使用服务并使用 Laravel's events 我们可以使所有服务方法在开始和结束时触发事件。这样,任何事件都可以与逻辑完全解耦。你怎么看?
我也被教过关于模型的知识……如果能很好地解释为什么(可能是依赖问题)会很好吗?
我喜欢这种方法!我一直在互联网上搜索以了解我应该如何处理模型的逻辑,查看了存储库,但它似乎太复杂且无用,无法使用。服务是个好主意。我的问题是在 app 文件夹中创建 Services 文件夹后,您是否必须将其包含在 bootstrap/start.php 或任何引导位置,因为我查看了您的 git 找不到它? @RubensMariuzzo。它是否会在整个应用程序中自动变为可用?所以我们可以使用 CongregationService::getCongregations(); ??
如果您所做的只是一个 $congregation->save();,那么您可能不需要存储库。但是,您可能会看到您的数据访问需求随着时间的推移而增加。您可能开始对 $congregation->destroyByUser()$congregationUsers->findByName($arrayOfSelectedFields); 等有需求。为什么不将您的服务与数据访问需求分离。让您的应用程序的其余部分使用从存储库返回的对象/数组,并仅处理操作/格式化/等...您的存储库将增长(但将它们拆分为不同的文件,最终项目的复杂性必须驻留在某个地方)。
S
Steve Bauman

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

简短的回答:

使用 Laravel 的 Request 对象自动验证您的输入,然后将数据持久化在请求中(创建模型)。由于所有用户输入都直接在请求中可用,我相信在这里执行此操作是有意义的。

使用 Laravel 的 Job 对象来执行需要单个组件的任务,然后简单地分发它们。我认为乔布斯包含服务类。他们执行一项任务,例如业务逻辑。

长(呃)答案:

在需要时使用存储库:存储库必然会过度膨胀,而且大多数情况下,只是将其用作模型的 accessor。我觉得它们肯定有一些用处,但除非你正在开发一个大型应用程序,需要足够的灵活性才能完全放弃 Laravel,否则请远离存储库。稍后您会感谢自己,您的代码将更加直接。

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

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

除了上述之外,请不要在像 Eloquent 这样的一流 ORM 之上打模式。您只是增加了不必要的复杂性,它根本不会使您受益。

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

我觉得 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。我认为可读性和可维护性应该总是胜过复杂和不需要的设计模式。

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


但是 - 工作不是“应该”排队吗?有时我们可能确实希望它排队,但并非一直如此。为什么不使用命令呢?如果您想编写一些可以作为命令或事件或队列执行的业务逻辑怎么办?
作业不需要排队。您可以通过在 Laravel 提供的作业 ShouldQueue 上实现接口来指定。如果您想在命令或事件中编写业务逻辑,只需在这些事件/命令中触发作业。 Laravel 作业非常灵活,但最终它们只是简单的服务类。
很好的一点! dispatchSync() 方法也可以立即使用作业,该方法应忽略 ShouldQueue