如何在Laravel应用程序中使用存储库模式(详细教程)

327 阅读6分钟

存储库可以被定义为领域和数据映射层之间的一个抽象层,它通过一个类似于集合的接口来访问领域对象,在两者之间提供一个调解的途径。

现代PHP框架,如LaravelSymfony,通过对象关系映射器(ORM)与数据库交互;Symfony使用Doctrine作为其默认的ORM,Laravel使用Eloquent

两者在数据库互动方面采取了不同的方法。在Eloquent中,模型是为每个数据库表生成的,形成互动的基础。而Doctrine则使用Repository模式,每个Entity都有一个相应的repository,包含与数据库交互的辅助函数。虽然Laravel没有提供开箱即用的功能,但在Laravel项目中使用Repository模式也是可能的。

存储库模式的一个关键好处是,它允许我们使用依赖性反转原则(或代码的抽象,而不是具体的)。这使得我们的代码对变化更加稳健,例如,如果后来决定切换到一个Eloquent不支持的数据源。

这也有助于保持代码的条理性和避免重复,因为数据库相关的逻辑被保存在一个地方。虽然这种好处在小项目中不会立即显现出来,但在需要维护多年的大型项目中会变得更加明显。

在这篇文章中, 我将向你展示如何在你的Laravel应用程序中实现Repository模式.为了做到这一点, 我们将建立一个API来管理一个公司从客户那里收到的订单.

开始使用

创建一个新的Laravel项目并使用以下命令cd到目录中:

laravel new order_api
cd order_api

设置数据库

在本教程中, 我们将使用MySQL作为我们的数据库.要做到这一点, 在*.env*文件中, 更新数据库相关的参数,如下图所示:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=order_api
DB_USERNAME=<YOUR_DATABASE_USERNAME>
DB_PASSWORD=<YOUR_DATABASE_PASSWORD>

最后,使用你喜欢的数据库管理程序,创建一个名为order_api 的新数据库。

生成数据库的初始数据

我们正在建立一个订单管理应用程序,所以让我们通过运行以下命令为它创建模型:

php artisan make:model Order -a

-a 参数让Artisan知道我们要为订单模型创建一个迁移文件、播种机、工厂和控制器。

上面的命令将创建五个新文件:

  • 一个控制器,位于app/Http/Controllers/OrderController.php
  • 数据库/factories/orderFactory.php中创建一个数据库工厂
  • 一个迁移文件,位于数据库/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php
  • 一个位于app/Models/Order.php中的模型
  • 数据库/seeders/OrderSeeder.php中的一个播种器文件和

数据库/migrations/YYYY_MM_DD_HHMMSS_create_orders_table.php中,更新up 函数,使其与以下内容相匹配:

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->text('details');
        $table->string('client');
        $table->boolean('is_fulfilled')->default(false);
        $table->timestamps();
    });
 }

按照迁移文件中的规定,order 表将有以下列:

  1. 一个ID。这将是该表的主键。
  2. 订单的详细信息。
  3. 下订单的客户的名字。
  4. 订单是否已经完成。
  5. 当订单被创建和更新时,created_atupdated_at ,由timestamps 函数提供。

接下来,让我们更新OrderFactory ,以便它能生成一个假的订单来作为数据库的种子。在database/factories/OrderFactory.php中,更新definition 函数,使之与以下内容相匹配:

public function definition() 
{
    return [
        'details'       => $this->faker->sentences(4, true),
        'client'         => $this->faker->name(),
        'is_fulfilled' => $this->faker->boolean(),
    ];
}

接下来,打开数据库/seeders/OrderSeeder.php,更新run 函数,以匹配以下内容:

public function run() 
{
    Order::factory()->times(50)->create();
}

这使用OrderFactory ,在数据库中创建50个订单。

不要忘记添加这个导入。

use App\Models\Order;

src/database/seeders/DatabaseSeeder.php 中,在run 函数中添加以下内容:

$this->call(
    [
        OrderSeeder::class
    ]
); 

当Artisan的db:seed 命令被运行时,这将运行QuoteSeeder

最后,运行你的迁移,用下面的命令对数据库进行播种:

php artisan migrate --seed

如果你打开订单表,你会看到新播种的订单:

List of Orders

创建存储库

在我们为Order 模型创建存储库之前,让我们定义一个接口来指定存储库必须声明的所有方法。我们的控制器(以及我们将来可能建立的任何订单组件)将依赖于接口,而不是直接依赖存储库类。

这使得我们的代码很灵活,因为如果将来有必要做出改变,控制器仍然不受影响。例如,如果我们决定将订单管理外包给第三方应用程序,我们可以建立一个符合OrderRepositoryInterface's signature的新模块,并交换绑定声明,我们的控制器将完全按照预期工作--不需要触及控制器中的任何一行代码。

应用程序目录下,创建一个名为Interfaces的新文件夹。然后,在Interfaces中,创建一个名为OrderRepositoryInterface.php的新文件,并在其中添加以下代码:

<?php

namespace App\Interfaces;

interface OrderRepositoryInterface 
{
    public function getAllOrders();
    public function getOrderById($orderId);
    public function deleteOrder($orderId);
    public function createOrder(array $orderDetails);
    public function updateOrder($orderId, array $newDetails);
    public function getFulfilledOrders();
}

接下来,在app文件夹中,创建一个新的文件夹,称为Repositories。在这个文件夹中,创建一个名为OrderRepository.php的新文件并添加以下代码:

<?php

namespace App\Repositories;

use App\Interfaces\OrderRepositoryInterface;
use App\Models\Order;

class OrderRepository implements OrderRepositoryInterface 
{
    public function getAllOrders() 
    {
        return Order::all();
    }

    public function getOrderById($orderId) 
    {
        return Order::findOrFail($orderId);
    }

    public function deleteOrder($orderId) 
    {
        Order::destroy($orderId);
    }

    public function createOrder(array $orderDetails) 
    {
        return Order::create($orderDetails);
    }

    public function updateOrder($orderId, array $newDetails) 
    {
        return Order::whereId($orderId)->update($newDetails);
    }

    public function getFulfilledOrders() 
    {
        return Order::where('is_fulfilled', true);
    }
}

除了接口提供的灵活性外,以这种方式封装查询还有一个好处,即我们不必在整个应用程序中重复查询。

如果将来我们决定在getAllOrders() 函数中只检索未完成的订单,我们只需要在一个地方进行修改,而不是追踪所有声明Order::all() 的地方,同时冒着错过一些的风险。

创建控制器

有了我们的资源库,让我们为我们的控制器添加一些代码。打开app/Http/Controllers/OrderController.php,更新代码,使之与以下内容一致:

<?php

namespace App\Http\Controllers;

use App\Interfaces\OrderRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class OrderController extends Controller 
{
    private OrderRepositoryInterface $orderRepository;

    public function __construct(OrderRepositoryInterface $orderRepository) 
    {
        $this->orderRepository = $orderRepository;
    }

    public function index(): JsonResponse 
    {
        return response()->json([
            'data' => $this->orderRepository->getAllOrders()
        ]);
    }

    public function store(Request $request): JsonResponse 
    {
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json(
            [
                'data' => $this->orderRepository->createOrder($orderDetails)
            ],
            Response::HTTP_CREATED
        );
    }

    public function show(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');

        return response()->json([
            'data' => $this->orderRepository->getOrderById($orderId)
        ]);
    }

    public function update(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $orderDetails = $request->only([
            'client',
            'details'
        ]);

        return response()->json([
            'data' => $this->orderRepository->updateOrder($orderId, $orderDetails)
        ]);
    }

    public function destroy(Request $request): JsonResponse 
    {
        $orderId = $request->route('id');
        $this->orderRepository->deleteOrder($orderId);

        return response()->json(null, Response::HTTP_NO_CONTENT);
    }
}

该代码通过构造函数注入一个OrderRepositoryInterface 实例,并在每个控制器方法中使用相关对象的方法。

首先,在index() 方法中,它调用orderRepository 中定义的getAllOrders() 方法来检索订单列表并返回JSON格式的响应。

接下来,store() 方法调用orderRepository 中的createOrder()方法来创建一个新订单。这将需要创建的订单的细节作为一个数组,之后返回一个成功的响应。

在控制器的show() 方法中,它从路由中检索唯一的订单Id ,并将其作为一个参数传递给getOrderById() 。这将从数据库中获取具有匹配Id的订单的详细信息,并返回JSON格式的响应。

然后,为了更新一个已经创建的订单的细节,它调用资源库中的updateOrder() 方法。这需要两个参数:订单的唯一ID和需要更新的细节。

最后,destroy() 方法从路由中检索一个特定订单的唯一ID,并从存储库中调用deleteOrder() 方法来删除它。

添加路由

为了将控制器中定义的每个方法映射到特定的路由,请在rouse/api.php中添加以下代码:

Route::get('orders', [OrderController::class, 'index']);
Route::get('orders/{id}', [OrderController::class, 'show']);
Route::post('orders', [OrderController::class, 'store']);
Route::put('orders/{id}', [OrderController::class, 'update']);
Route::delete('orders/{id}', [OrderController::class, 'delete']);

记住要包括import 语句的OrderController

use App\Http\Controllers\OrderController;

绑定接口和实现

我们需要做的最后一件事是在Laravel的服务容器中把OrderRepositoryOrderRepositoryInterface 绑定起来; 我们通过一个服务提供者来实现。使用以下命令创建一个:

php artisan make:provider RepositoryServiceProvider

打开app/Providers/RepositoryServiceProvider.php,并更新register ,使其与以下内容一致:

public function register() 
{
    $this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
 }

记住要为OrderRepositoryOrderRepositoryInterface 包括import 语句:

use App\Interfaces\OrderRepositoryInterface;
use App\Repositories\OrderRepository;

最后,在config/app.php中把新的服务提供者添加到providers 数组中:

'providers' => [
    // ...other declared providers
    App\Providers\RepositoryServiceProvider::class,
];

测试应用程序

使用下面的命令运行该应用程序:

php artisan serve

默认情况下,被服务的应用程序将在http://127.0.0.1:8000/。使用PostmancURL,我们可以向我们新创建的API发出请求。

运行下面的命令,使用cURL测试/api/orders 端点:

curl --silent http://localhost:8000/api/orders | jq

使用jq将响应格式化为JSON。

你会在终端看到类似于下面这个例子的JSON输出,为了帮助阅读,这个输出被截断了:

{
  "data": [
    {
      "id": 1,
      "details": "Sit ullam cupiditate dolorem in. Magnam suscipit eaque occaecati facilis amet illum. Dolor perspiciatis velit laboriosam. Enim fugiat excepturi qui natus incidunt dolorem debitis ut.",
      "client": "Cydney Conn V",
      "is_fulfilled": 0,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 2,
      "details": "Eum iste eum molestiae est. Voluptatibus veritatis earum commodi. Quod et laboriosam ratione dolor adipisci. Nam et debitis nobis ea sit.",
      "client": "Willow Herzog",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    },
    {
      "id": 3,
      "details": "At maxime architecto repellat quidem id. Saepe provident quo eos officiis et tenetur. Et expedita maxime atque. Et consequuntur sequi aperiam possimus odio est ab.",
      "client": "Mr. Peyton Nolan DVM",
      "is_fulfilled": 1,
      "created_at": "2021-09-09T09:18:28.000000Z",
      "updated_at": "2021-09-09T09:18:28.000000Z"
    }
  ]
}

这就是如何在Laravel应用程序中使用存储库模式的方法

在这篇文章中, 我们了解了Repository模式以及如何在Laravel应用程序中使用它.我们也看到了它为大型项目提供的一些好处 - 其中之一是松散耦合的代码,我们是在为抽象的代码编码,而不是具体实现。

不过,我最后还是要提醒一下。对于小项目来说,这种方法会让人感觉到大量的工作和模板化的代码,而这些回报可能不会立即显现。因此,在采用这种方法之前,你必须适当考虑项目的规模。

本教程的整个代码库可在GitHub上找到。请自由地进一步探索。编码愉快!