MVC(Laravel)在哪里添加逻辑

时间:2014-05-11 16:22:25

标签: php design-patterns laravel laravel-4

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

通常我会将这种逻辑放入控制器中。在你想要在很多地方重现这个功能之前,这一切都很好。当你开始进入局部,创建一个API并生成虚拟内容时,这就成了一个让事情变得干燥的问题。

我看到管理它的方法是事件,存储库,库和添加到模型。以下是我对每个人的理解:

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

我认为这种方法运作良好的唯一条件是,如果你只使用服务(理想情况下,不管怎么说,Eloquent都不能从控制器中获取)。

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

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

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

模型:传统上我会有一些执行CRUD并处理关键耦合的类。这实际上让事情变得简单,因为你知道CRUD的所有功能+无论如何都要做到这一点。

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

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

4 个答案:

答案 0 :(得分:138)

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

对于添加逻辑的位置,我认为引用Single Responsibility Principle非常重要。另外,我的回答是你正在研究中/大型项目。如果它是一个扔在页面上的东西项目,请忘记这个答案并将其全部添加到控制器或模型中。

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

答案很长:

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

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

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

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

存储库(存储库模式):一开始我对此非常困惑。而且,就像你一样,我想"好吧,我使用MySQL就是那样。"。

但是,我已经平衡了使用存储库模式的优点和缺点,现在我使用它。我认为现在,就在这个时刻,我只需要使用MySQL。但是,如果从现在起三年后我需要改变像MongoDB这样的东西,大部分工作都已完成。所有这些都是以一个额外的接口和$app->bind(«interface», «repository»)为代价的。

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

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

举个例子:不久前,我开发了像Google Forms这样的东西。我从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的作者):这是我把它放在一起的地方,即使我改变了一些东西以满足我的需要。

Chris Goosey的

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

Laracasts也有Repositories SimplifiedSingle Responsibility这些都是很好的资源和实际例子(即使你需要付费)。

答案 1 :(得分:17)

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

  1. Controller获取用户请求的操作并发送参数并将所有内容委托给服务类。
  2. 服务类执行与操作相关的所有逻辑:输入验证,事件记录,数据库操作等......
  3. 模型包含字段,数据转换和属性验证定义的信息。
  4. 我就是这样做的:

    这是控制器创建内容的方法:

    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

答案 2 :(得分:17)

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

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

我没有在服务/图书馆中包装我的模型。所提供的所有理由并非100%让我相信使用服务的好处。虽然我可能错了,但据我所知,它们只会产生大量额外的空文件,我需要在处理模型时进行创建和切换,并且真正降低使用雄辩的好处(特别是当涉及到RETRIEVING模型时)例如,使用分页,范围等)。

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

  • 访问者和变异者: Laravel拥有出色的访问者和变异者。如果我想在帖子从草稿移动到已发布时执行操作,我可以通过创建函数setIsPublishedAttribute并在其中包含逻辑来调用它
  • 覆盖创建/更新等:您始终可以覆盖模型中的Eloquent方法以包含自定义功能。这样您就可以在任何CRUD操作上调用功能。编辑:我认为在新的Laravel版本中覆盖创建有一个错误(因此我使用现在在启动时注册的事件)
  • 验证:我以相同的方式挂钩验证,例如,如果需要,我将通过覆盖CRUD函数和访问器/ mutator来运行验证。有关详细信息,请参阅Esensi或dwightwatson /验证。
  • 魔术方法:我使用模型的__get和__set方法在适当的情况下挂钩功能
  • 扩展口才:如果您想要对所有更新/创建采取行动,您甚至可以扩展口才并将其应用于多个模型。
  • 活动:这是一个直截了当且通常也同意这样做的地方。事件的最大缺点我认为异常很难追踪(可能不是Laravel新事件系统的新案例)。我还喜欢按照他们所做的事情而不是在他们被调用时对我的事件进行分组......例如,有一个MailSender订阅者,它可以监听发送邮件的事件。
  • 添加Pivot / BelongsToMany事件:我最长时间努力的事情之一是如何将行为附加到belongsToMany关系的修改上。例如,每当用户加入组时执行动作。我差不多完成了一个自定义库。我还没有发表它但它功能齐全!将尝试尽快发布链接。 编辑我最终将我的所有支柱都变成了正常的模型,我的生活变得如此简单......

解决使用模型时人们的顾虑:

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

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

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

答案 3 :(得分:6)

在我看来,Laravel已经有很多选项供您存储业务逻辑。

简短回答:

  • 使用laravel的Request个对象自动验证您的输入,然后将数据保留在请求中(创建模型)。由于所有用户输入都可直接在 请求中使用,因此我认为在此处执行此操作是有意义的。
  • 使用laravel的Job个对象来执行需要单个组件的任务,然后只需调度它们。我认为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
{
    /**
     * 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。我认为可读性和可维护性应该总是胜过复杂和不必要的设计模式。

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