如何在 Lumen 中使用 DRY 和服务层?

How to using DRY and service layer in Lumen?

我正在 Lumen 框架中创建 api,最近我阅读了有关 DRY 和服务层的内容。直到今天我都没有在我的代码中使用这些,所有的逻辑都在控制器中。所以我想开始使用它,但我遇到了一些问题。

这是我的控制器的一部分(UsersController.php),因为整个代码太长了。

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UsersController extends Controller
{
    private $request;

    public function __construct(Request $request) {
        $this->request = $request;
    }

    public function destroy($id) {
        $user = User::find($id);

        if (!$user) {
            return response()->json([
                'error' => 'User not found'
            ], 404);
        }

        if ($user->role === 'admin') {
            return response()->json([
                'error' => 'You cant edit admin'
            ], 403);
        }

        $user->delete();

        return response()->json([], 204);
    }
}

看完这段代码后,我试图改变两件事。

  1. 获取用户和 return 错误可以在 UserService.php 中完成(我在其他方法中也有此代码,所以这就是为什么我认为这很好这种方法在服务中的想法)。但是正如你所看到的,当出现错误时我想 return 响应,当我这样做时,我的代码试图在 json 响应上使用 delete 方法,而不是在用户模型上。抛出异常在我看来是不好的,因为不符合DRY原则。知道如何解决吗?

UserService.php

<?php
namespace App\Services;

use App\User;

class UserService
{
    public function getUserById($id)
    {
        $user = User::find($id);

        if (!$user) {
            return response()->json([
                'error' => 'User not found'
            ], 404);
        }

        if ($user->role === 'admin') {
            return response()->json([
                'error' => 'You cant edit admin'
            ], 403);
        }

        return $user;
    }
}

已修改 UsersController。php/destroy

public function destroy($id) {
    $user = $this->userService->getUserById($id);
    $user->delete(); // not working because sometimes it can return json response

    return response()->json([], 204);
}
  1. 我在控制器、中间件等中使用了太多 json 响应,我想通过创建新的 class 来统一它,但我不知道如何正确使用它。我的意思是 returning json 在 ResponderService.php 中的响应可能不会在控制器等其他地方停止执行。或者也许我应该将其创建为助手?

ResponderService.php

<?php
namespace App\Services;

class ResponderService
{
    private function base($data, $status_code)
    {
        $data['status_code'] = $status_code;

        return response()->json($data, $status_code);
    }

    public function error($message, $status_code)
    {
        $data['error'] = $message;
        $data['status'] = 'error';
        $this->base($data, $status_code);
    }
}

我也阅读了有关存储库的内容,但我认为这种模式在我的项目中并不适用。如果您有其他可以改进控制器代码的建议,我愿意接受。

我没有发现在您的场景中使用异常有任何问题。

<?php
namespace App\Services;

use App\User;

class UserService
{
    public function getUserById($id)
    {
        $user = User::find($id);

        if (!$user) {
            throw UserNotFoundException('User not found');
        }

        if ($user->role === 'admin') {
            throw EditAdminException("You can't edit admin.");
        }

        return $user;
    }
}

如果需要,这些异常是您在 app\Exception 中定义的自定义异常。然后 getUserById() 方法只能 return 一个 User 否则一个异常冒泡并且 return 一个 JSON 对客户端的响应。

Laravel也已经有了一个简单的方法来处理第一个异常。你可以这样做:

<?php
namespace App\Services;

use App\User;

class UserService
{
    public function getUserById($id)
    {
        $user = User::findOrFail($id);

        if ($user->role === 'admin') {
            throw EditAdminException("You can't edit admin.");
        }

        return $user;
    }
}

如果找不到 User,Laravel 将处理抛出 Illuminate\Database\Eloquent\ModelNotFoundException

这样您就不必担心为 Exceptions 已经可以为您做的事情创建 ResponderService

如果您想对资源的响应进行标准化,您可以利用 Eloquent 资源作为您的 API 的转换层:https://laravel.com/docs/5.7/eloquent-resources

最后,如果您发现要从多个位置删除资源并且不想重复响应,则可以将响应放在事件中:https://laravel.com/docs/5.7/eloquent#events

文档显示了处理事件的复杂方式,但我个人只会在您的模型开始感到臃肿时才这样做。

您可以将此作为创建事件和观察者的更简单替代方法 class:

public static function boot()
{
    parent::boot();

    static::deleted(function ($model) {
        return response()->json([], 204);
    });
}

该方法适用于您的用户模型。顺便说一句,我发现的方法是查看 Eloquent\Model.

上的 HasEvents 特征

现在,综上所述,我实际上会将所有删除逻辑放入您的 UserService 并将方法从 getUserById 重命名为 deleteById。替代方案有点奇怪,因为您是说如果用户是管理员,您不希望能够通过 ID 获取用户。

实际上,您要做的是封装删除用户的逻辑,因此只需将其全部移动到服务的方法中,或者更好的是,只需在模型上使用 delete 事件,然后把所有的逻辑放在那里。这样您甚至不必引入服务。

编辑

根据您在下面的评论,我认为您可能误解了如何在 Laravel 中使用异常。

在一个新的 Laravel 项目中,app\Exeptions\Handler 中有一个 class 可以捕获您应用中所有未处理的异常。 class 首先检查异常是否是 ModelNotFoundException,然后 return 是 json 响应。

否则,它将捕获的异常传递给其父级的 render 方法。

所以基本上当你想创建一个自定义异常时,你只需创建一个 class 来扩展 Exception 并实现一个 handle 方法。

这是一个示例异常 class:

<?php

namespace App\Exceptions;

use Exception;

class TicketNotPayableException extends Exception
{
    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render()
    {
        return response()->json([
            'errors' => [
                [
                    'title' => 'Ticket Not Payable Exception',
                    'description' =>
                        'This ticket has already been paid.'
                ],
            ],
            'status' => '409'
        ], 409);
    }
}

现在响应完全可以重用,我的代码中不需要一堆 try-catch 块。 Laravel 的异常处理程序将捕获它,并调用 render 方法。

所以,如果我想在服务中封装支付机票的逻辑,我只需要 throw App\Exceptions\TicketNotPayableException; 然后我的控制器只需要做类似的事情: $ticketPaymentService->pay($ticket); 并且有无需试探。如果异常被抛出,它将冒泡,被处理程序捕获,并且将调用 render 方法,这将 return 适当的 JSON 响应 - 不需要Responder.