使用数据库事务的Laravel控制器-模型异常处理结构

5

关于架构,从模型到控制器抛出异常时,下面两种方法中哪一种是好的实践?

结构 A:

UserController.php

public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
    $isError = false;
    $message = 'Success';

    try {
        $message = $userModel->updateUserInfo($request->only(['username', 'password']));
    } catch (SomeCustomException $e) {
        $isError = true;
        $message = $e->getMessage();
    }

    return json_encode([
        'isError' => $isError,
        'message' => $message
    ]);
}

UserModel.php

public function updateUserInfo($request)
{
    $isError = false;
    $message = 'Success';

    $username = $request['username'];
    $password = $request['password'];

    try {
        $this->connect()->beginTransaction();

        $this->connect()->table('users')->where('username', $username)->update(['password' => $password]);

        $this->connect()->commit();
    } catch (\Exception $e) {
        $this->connect()->rollback();
        $isError = true;
        $message = $e->getMessage();        
    }

    return [
        'isError' => $isError,
        'message' => $message
    ];
}

结构B:

UserController.php

public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
    $isError = false;
    $message = 'Success';

    try {
        $userModel->updateUserInfo($request->only(['username', 'password']));
    } catch (SomeCustomException $e) {
        $isError = true;
        $message = $e->getMessage();
    } catch (QueryException $e) {
        $isError = true;
        $message = $e->getMessage();    
    }

    return json_encode([
        'isError' => $isError,
        'message' => $message
    ]);
}

UserModel.php

public function updateUserInfo($request)
{
    $username = $request['username'];
    $password = $request['password'];

    try {
        $this->connect()->beginTransaction();

        $this->connect()->table('users')->where('username', $username)->update(['password' => $password]);

        $this->connect()->commit();
    } catch (\Exception $e) {
        $this->connect()->rollback();
        throw new QueryException();
    }
}

结构A中,模型捕获任何异常,如果出现错误或不存在,则回滚事务并返回给控制器。然后,控制器只是返回从模型中获得的任何内容。
而在结构B中,模型捕获任何异常,如果出现异常,则回滚事务,然后抛出QueryException。然后,控制器从模型中捕获抛出的QueryException并返回是否有错误。 结构B仍然有catch语句的原因是模型应该执行回滚操作。如果我在这里从模型中删除try-catch,让控制器直接捕获异常,那么回滚将在控制器上处理,我认为这会使控制器的功能变得混乱。
请告诉我你的想法。 谢谢!
4个回答

5

我认为B方案更好的原因:

  1. 你的模型(Model)只应包括逻辑部分:这包括与数据库的通信(事务和回滚),而不是你想要打印给用户的错误消息的格式化部分。

  2. 保持你的模型干净整洁:它是MVC结构中最重要的部分。如果你把它搞砸了,将很难找到任何错误。

  3. 将错误处理外包:如果你把它放在控制器(Controller)中,你可以选择在那里处理它(也许你想为这个方法提供一些特殊格式的输出或者你需要调用其他函数)或者你在App\Exceptions\Handler中处理它。在这种情况下,你可以在这里呈现这个错误消息,而不必在控制器中执行。

所以,如果你不需要任何特殊的函数调用,并想使用Laravel的全部功能,则建议你采用C架构

UserController.php

public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
    $userModel->updateUserInfo($request->only(['username', 'password']));
    return response()->json(['message' => 'updated user.']); 
}

UserModel.php

public function updateUserInfo($request)
{
    $username = $request['username'];
    $password = $request['password'];
    try {
        $this->connect()->beginTransaction();

        $this->connect()->table('users')->where('username', $username)->update(['password' => $password]);

        $this->connect()->commit();
    } catch (\Exception $e) {
        $this->connect()->rollback();
        throw new QueryException();
    }
}

App\Exceptions\Handler

public function render($request, Exception $exception)
{
    //catch everything what you want
    if ($exception instanceof CustomException) {
        return response()->json([
          'message' => $exception->getMessage()
        ], 422);
    }

    return parent::render($request, $exception);
}

您可以将数据库操作(模型)、展示逻辑(控制器)和错误处理(处理程序)进行清晰的分离。C语言结构允许您在其他控制器函数中重复使用错误处理程序,以应对类似情况。
这是我的看法,但我也很乐意讨论任何您认为这种方法不是最佳解决方案的情况。

我赞同mimo的结构C建议,不过要注意的是不要在JSON响应中添加“isError”字段。HTTP已经提供了一种内置的方法来确定响应是否为错误。可以参考HTTP状态码,简单来说:1xx:请稍等; 2xx:好的,给你; 3xx:别来烦我; 4xx:你出错了; 5xx:我出错了。 - Jeremy Giberson
@JeremyGiberson,我刚刚添加了与作者相同的JSON响应,但你是对的。更好的方法是设置错误代码,例如422,这样您就可以处理它(例如在JS中使用then()catch())。 - cre8

2
首先,针对您的例子,您甚至不需要使用Transaction。您只执行了一个查询,所以为什么需要回滚呢?您想要回滚哪个查询?当您需要一组更改被完全处理才能考虑操作完成和有效时,应该使用事务。如果第一个成功了,但其后的任何一个出现错误,您可以将所有内容回滚,就好像从未做过任何事情一样。
其次,让我们来谈谈最佳实践或最佳做法。Laravel建议使用Thin controller和Thick model。因此,您的所有业务逻辑都应该在model中或者更好地在repository中。Controller将充当代理人。它将从repository或model中收集数据并将其传递给视图。
或者,laravel提供了一些很好且方便的方式来组织您的代码。您可以在model中使用Event和Observers进行并发操作。
最佳实践因用户的知识和经验而异。因此,谁知道,您问题的最佳答案可能还没有出现。

1
我更倾向于使控制器和与模型交互的系统的任何其他部分尽可能地对模型的内部工作不可知。因此,例如,我会尽量避免在模型之外意识到QueryException,并尽可能将其视为普通的PHP对象。
此外,我会避免使用自定义JSON响应结构,而是使用HTTP状态码。如果有意义的话,也许更新用户信息的路由会返回更新后的资源,或者仅返回200 OK就足够了。
// UserModel.php
public function updateUserInfo($request)
{
    $username = $request['username'];
    $password = $request['password'];

    try {
        $this->connect()->beginTransaction();

        $this->connect()->table('users')->where('username', $username)->update(['password' => $password]);

        $this->connect()->commit();

        return $this->connect()->table('users')->where('username', $username)->first();
        // or just return true;
    } catch (\Exception $e) {
        $this->connect()->rollback();

        return false;
    }
}

// UserController.php    
public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
    $updated = $userModel->updateUserInfo($request->only(['username', 'password']));

    if ($updated) {
        return response($updated);
        // HTTP 200 response. Returns JSON of updated user.
        // Alternatively,
        // return response('');
        // (200 OK response, no content)
    } else {
        return response('optional message', 422);
        // 422 or any other status code that makes more sense in the situation.
    }

完全偏离主题,我猜这是一个例子,但以防万一,提醒不要存储明文密码。


1
我不理解为什么你没有观看Jeffry的课程,但是在更新用户时你不需要尝试/捕获部分。您的控制器方法:
public function update(UpdateUserRequest $request, User $user) : JsonResponse
{
   return response()->json($user->update($request->all()))
}

你请求规则方法:

public function rules(): array
{
    return [
        'username' => 'required|string',
        'password' => 'required|min:6|confirmed',
    ];
}

你的异常处理程序渲染方法:
public function render($request, Exception $exception)
{
    if ($request->ajax() || $request->wantsJson()) {
        $exception = $this->prepareException($exception);

        if ($exception instanceof \Illuminate\Http\Exception\HttpResponseException) {
            return $exception->getResponse();
        } elseif ($exception instanceof \Illuminate\Auth\AuthenticationException) {
            return $this->unauthenticated($request, $exception);
        } elseif ($exception instanceof \Illuminate\Validation\ValidationException) {
            return $this->convertValidationExceptionToResponse($exception, $request);
        }

        // we prepare custom response for other situation such as modelnotfound
        $response = [];
        $response['error'] = $exception->getMessage();

        if (config('app.debug')) {
            $response['trace'] = $exception->getTrace();
            $response['code'] = $exception->getCode();
        }

        // we look for assigned status code if there isn't we assign 500
        $statusCode = method_exists($exception, 'getStatusCode')
            ? $exception->getStatusCode()
            : 500;

        return response()->json($response, $statusCode);
    }
    return parent::render($request, $exception);
}

现在,如果您遇到异常,Laravel将以状态码不等于200的Json格式返回给您,否则将返回成功结果!

我正在使用事务处理来处理依赖于其他查询的查询操作,比如插入一条数据后,获取新插入数据的ID,然后用这个ID来在另一个表中插入另一条数据,等等... - basagabi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接