管理Laravel中的关系,遵守存储库模式

时间:2013-09-15 21:20:33

标签: oop laravel laravel-4 repository-pattern eloquent

在阅读了T. Otwell关于Laravel优秀设计模式的书之后,在Laravel 4中创建应用程序时,我发现自己为应用程序中的每个表创建了存储库。

我最终得到了以下表结构:

  • 学生:id,姓名
  • 课程:id,name,teacher_id
  • 教师:id,姓名
  • 作业:id,name,course_id
  • 分数(充当学生和作业之间的枢轴):student_id,assignment_id,得分

我有所有这些表的查找,创建,更新和删除方法的存储库类。每个存储库都有一个与数据库交互的Eloquent模型。根据Laravel的文档在模型中定义关系:http://laravel.com/docs/eloquent#relationships

创建新课程时,我所做的就是在课程资源库中调用create方法。该课程有作业,因此在创建作业时,我还想在课程中为每个学生在乐谱表中创建一个条目。我是通过Assignment Repository完成的。这意味着赋值存储库使用Assignment和Student模型与两个Eloquent模型进行通信。

我的问题是:由于此应用程序的大小可能会增加并且会引入更多关系,因此在存储库中与不同的Eloquent模型进行通信是一种好的做法,还是应该使用其他存储库来完成(我的意思是调用其他存储库) Assignment存储库)还是应该在Eloquent模型中一起完成?

此外,将分数表用作作业与学生之间的转轴还是应该在其他地方进行,这是一种好习惯吗?

4 个答案:

答案 0 :(得分:204)

我正在使用Laravel 4完成一个大型项目,并且必须回答您现在提出的所有问题。在阅读了Leanpub上所有可用的Laravel书籍以及大量的谷歌搜索后,我想出了以下结构。

  1. 每个数据表的一个Eloquent Model类
  2. 每个Eloquent Model的一个Repository类
  3. 可以在多个存储库类之间进行通信的Service类。
  4. 所以我想说我正在建立一个电影数据库。我至少会有以下以下Eloquent Model类:

    • 电影
    • 演播室
    • 演员
    • 评分

    存储库类将封装每个Eloquent Model类,并负责数据库上的CRUD操作。存储库类可能如下所示:

    • MovieRepository
    • StudioRepository
    • DirectorRepository
    • ActorRepository
    • ReviewRepository

    每个存储库类都会扩展一个BaseRepository类,该类实现以下接口:

    interface BaseRepositoryInterface
    {
        public function errors();
    
        public function all(array $related = null);
    
        public function get($id, array $related = null);
    
        public function getWhere($column, $value, array $related = null);
    
        public function getRecent($limit, array $related = null);
    
        public function create(array $data);
    
        public function update(array $data);
    
        public function delete($id);
    
        public function deleteWhere($column, $value);
    }
    

    Service类用于将多个存储库粘合在一起,并包含应用程序的真实“业务逻辑”。控制器与Service类进行通信,以进行创建,更新和删除操作。

    因此,当我想在数据库中创建一个新的Movie记录时,我的MovieController类可能有以下方法:

    public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
    {
        $this->movieRepository = $movieRepository;
        $this->movieService = $movieService;
    }
    
    public function postCreate()
    {
        if( ! $this->movieService->create(Input::all()))
        {
            return Redirect::back()->withErrors($this->movieService->errors())->withInput();
        }
    
        // New movie was saved successfully. Do whatever you need to do here.
    }
    

    由您决定如何将数据发送到控制器,但是假设postCreate()方法中的Input :: all()返回的数据如下所示:

    $data = array(
        'movie' => array(
            'title'    => 'Iron Eagle',
            'year'     => '1986',
            'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
        ),
        'actors' => array(
            0 => 'Louis Gossett Jr.',
            1 => 'Jason Gedrick',
            2 => 'Larry B. Scott'
        ),
        'director' => 'Sidney J. Furie',
        'studio' => 'TriStar Pictures'
    )
    

    由于MovieRepository不应该知道如何在数据库中创建Actor,Director或Studio记录,我们将使用我们的MovieService类,它可能看起来像这样:

    public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
    {
        $this->movieRepository = $movieRepository;
        $this->actorRepository = $actorRepository;
        $this->directorRepository = $directorRepository;
        $this->studioRepository = $studioRepository;
    }
    
    public function create(array $input)
    {
        $movieData    = $input['movie'];
        $actorsData   = $input['actors'];
        $directorData = $input['director'];
        $studioData   = $input['studio'];
    
        // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.
    
        // Create the new movie record
        $movie = $this->movieRepository->create($movieData);
    
        // Create the new actor records and associate them with the movie record
        foreach($actors as $actor)
        {
            $actorModel = $this->actorRepository->create($actor);
            $movie->actors()->save($actorModel);
        }
    
        // Create the director record and associate it with the movie record
        $director = $this->directorRepository->create($directorData);
        $director->movies()->associate($movie);
    
        // Create the studio record and associate it with the movie record
        $studio = $this->studioRepository->create($studioData);
        $studio->movies()->associate($movie);
    
        // Assume everything worked. In the real world you'll need to implement checks.
        return true;
    }
    

    所以我们留下的是一个美好,明智的关注点分离。存储库只知道它们从数据库中插入和检索的Eloquent模型。控制器不关心存储库,它们只是将从用户收集的数据交给用户并将其传递给适当的服务。该服务不关心如何它收到的数据被保存到数据库中,它只是将控制器给出的相关数据移交给相应的存储库。

答案 1 :(得分:68)

请记住,你在征求意见:D

这是我的:

TL; DR:是的,没关系。

你做得很好!

我经常做你正在做的事情,发现它很有效。

但是,我经常围绕业务逻辑组织存储库,而不是使用每个表的repo。这很有用,因为它是一个以应用程序如何解决“业务问题”为中心的观点。

课程是一个“实体”,具有属性(标题,标识等)甚至其他实体(作业,具有自己的属性和可能的​​实体)。

您的“课程”存储库应该能够返回课程和课程的属性/作业(包括作业)。

幸运的是,你可以用Eloquent来实现这一点。

(我经常最终得到每个表的存储库,但是一些存储库比其他存储库使用得多,因此有更多的方法。例如,您的“课程”存储库可能比您的Assignments存储库更全面。 ,如果您的应用程序更多地围绕课程而不是课程的作业集合。

棘手的部分

我经常在我的存储库中使用存储库来执行一些数据库操作。

任何实现Eloquent以处理数据的存储库都可能返回Eloquent模型。在这种情况下,如果您的课程模型使用内置关系来检索或保存分配(或任何其他用例),那就没问题了。我们的“实施”是围绕Eloquent建立的。

从实际的角度来看,这是有道理的。我们不太可能将数据源更改为Eloquent无法处理的内容(对于非sql数据源)。

ORMS

至少对我而言,这种设置中最棘手的部分是确定Eloquent是否真的帮助或伤害了我们。 ORM是一个棘手的主题,因为虽然它们从实际的角度来帮助我们,但它们还将您的“业务逻辑实体”代码与执行数据检索的代码相结合。

无论您的存储库负责实际处理数据还是处理实体(业务域实体)的检索/更新,这都会让您感到困惑。

此外,它们充当您传递给视图的对象。如果您以后不得不在存储库中使用Eloquent模型,则需要确保传递给视图的变量以相同的方式运行或使用相同的方法,否则更改数据源将会更改视图,你(部分)失去了将逻辑抽象到存储库的目的 - 项目的可维护性下降为。

无论如何,这些都是一些不完整的想法。如上所述,他们只是我的意见,这恰好是去年在Ruby Midwest阅读Domain Driven Design和观看"uncle bob's" keynote等视频的结果。

答案 2 :(得分:5)

我喜欢根据我的代码正在做什么以及它负责什么来考虑它,而不是“正确或错误”。这就是我分担责任的方式:

  • 控制器是HTTP层,并将请求路由到底层api(也称为控制流)
  • 模型表示数据库模式,并告诉应用程序数据的外观,它可能具有的关系,以及可能需要的任何全局属性(例如返回连接的名字和姓氏的名称方法)
  • 存储库代表更复杂的查询和与模型的交互(我不对模型方法进行任何查询)。
  • 搜索引擎 - 帮助我构建复杂搜索查询的类。

考虑到这一点,每次使用存储库都是有意义的(无论你是否创建interfaces.etc。都是另一个主题)。我喜欢这种方法,因为这意味着当我需要做某些工作时,我确切地知道要去哪里。

我还倾向于构建一个基本存储库,通常是一个定义主要默认值的抽象类 - 基本上是CRUD操作,然后每个子项可以根据需要扩展和添加方法,或者重载默认值。注入模型也有助于这种模式非常强大。

答案 3 :(得分:5)

将存储库视为数据的一致文件柜(而不仅仅是您的ORM)。我们的想法是,您希望以一致易用的API来获取数据。

如果您发现自己只是在做Model :: all(),Model :: find(),Model :: create(),那么您可能无法从抽象存储库中获益。另一方面,如果您想为查询或操作做更多的业务逻辑,您可能需要创建一个存储库,以便更容易使用API​​来处理数据。

我认为您在询问存储库是否是处理连接相关模型所需的一些更详细语法的最佳方法。根据具体情况,我可以做一些事情:

  1. 将一个新的子模型从父模型(一个或一个多个)挂起,我会向子存储库添加一个类似createWithParent($attributes, $parentModelInstance)的方法,这只会添加{{ 1}}进入属性的$parentModelInstance->id字段并调用create。

  2. 附加了很多关系,我实际上是在模型上创建函数,这样我就可以运行$ instance-> attachChild($ childInstance)。请注意,这需要两侧的现有元素。

  3. 在一次运行中创建相关模型,我创建了一些我称之为网关的东西(它可能与Fowler的定义有点不同)。我可以调用$ gateway-> createParentAndChild($ parentAttributes,$ childAttributes)而不是一堆可能会改变的逻辑,或者会使我在控制器或命令中的逻辑复杂化。