Laravel 5:在从 BaseController 扩展的控制器中对 FormRequest 类进行类型提示

Posted

技术标签:

【中文标题】Laravel 5:在从 BaseController 扩展的控制器中对 FormRequest 类进行类型提示【英文标题】:Laravel 5: Type-hinting a FormRequest class inside a controller that extends from BaseController 【发布时间】:2015-06-30 00:22:02 【问题描述】:

我有一个BaseController,它为我的 API 服务器的大多数 HTTP 方法提供了基础,例如store 方法:

BaseController.php

/**
 * Store a newly created resource in storage.
 *
 * @return Response
 */
public function store(Request $request)

    $result = $this->repo->create($request);

    return response()->json($result, 200);

然后我在一个更具体的控制器中扩展这个BaseController,比如UserController,像这样:

UserController.php

class UserController extends BaseController 

    public function __construct(UserRepository $repo)
    
        $this->repo = $repo;
    


这很好用。但是,我现在想扩展 UserController 以注入 Laravel 5 的新 FormRequest 类,它负责 User 资源的验证和身份验证等事情。我想这样做,通过覆盖 store 方法并为其 Form Request 类使用 Laravel 的类型提示依赖注入。

UserController.php

public function store(UserFormRequest $request)

    return parent::store($request);

UserFormRequest 扩展自 Request,而 Request 本身扩展自 FormRequest

UserFormRequest.php

class UserFormRequest 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 [
            'name'  => 'required',
            'email' => 'required'
        ];
    


问题是BaseController 需要Illuminate\Http\Request 对象,而我传递了UserFormRequest 对象。因此我得到这个错误:

in UserController.php line 6
at HandleExceptions->handleError('2048', 'Declaration of Bloomon\Bloomapi3\Repositories\User\UserController::store() should be compatible with Bloomon\Bloomapi3\Http\Controllers\BaseController::store(Illuminate\Http\Request $request)', '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php', '6', array('file' => '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php')) in UserController.php line 6

那么,如何在仍然遵守 BaseController 的请求要求的同时键入提示注入 UserFormRequest?我不能强制 BaseController 要求 UserFormRequest,因为它适用于任何资源。

我可以在BaseControllerUserController 中使用类似RepositoryFormRequest 的接口,但问题是Laravel 不再通过其类型提示依赖注入来注入UserFormController

【问题讨论】:

您可以将 BaseController 中的方法移动到 Trait,然后在其他控制器上使用它。 你在使用 Laravel 的 RESTful 资源控制器吗?那么像store这样的预设名称要坚持什么? @Chris 是的,我是。我不确定我是否听懂了您的问题,请您改述一下吗? 呃,这是方法类型提示中的一个巨大设计缺陷......让整个事情看起来几乎没用。 【参考方案1】:

与许多“真正的”面向对象语言相比,这种覆盖方法中的类型提示设计在 PHP 中是不可能的,请参阅:

class X 
class Y extends X 

class A 
    function a(X $x) 


class B extends A 
    function a(Y $y)  // error! Methods with the same name must be compatible with the parent method, this includes the typehints

这会产生与您的代码相同类型的错误。我只是不会在您的BaseController 中添加store() 方法。如果您觉得自己在重复代码,请考虑引入例如服务类或特征。

使用服务类

下面是使用额外服务类的解决方案。对于您的情况,这可能有点矫枉过正。但是,如果您向 StoringServices store() 方法添加更多功能(如验证),它可能会很有用。您还可以向StoringService 添加更多方法,例如destroy()update()create(),但您可能希望以不同的方式命名服务。

class StoringService 

    private $repo;

    public function __construct(Repository $repo)
    
        $this->repo = $repo;
    

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request)
    
        $result = $this->repo->create($request);

        return response()->json($result, 200);
    


class UserController 

    // ... other code (including member variable $repo)

    public function store(UserRequest $request)
    
        $service = new StoringService($this->repo); // Or put this in your BaseController's constructor and make $service a member variable
        return $service->store($request);
    


使用特征

你也可以使用 trait,但是你必须重命名 trait 的 store() 方法:

trait StoringTrait 

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request)
    
        $result = $this->repo->create($request);

        return response()->json($result, 200);
    


class UserController 

    use 
        StoringTrait::store as baseStore;
    

    // ... other code (including member variable $repo)

    public function store(UserRequest $request)
    
        return $this->baseStore($request);
    


此解决方案的优点是,如果您不必为 store() 方法添加额外的功能,则只需 use 特征而不重命名,并且您不必编写额外的 store() 方法。

使用继承

在我看来,继承不太适合您在这里需要的那种代码重用,至少在 PHP 中不适合。但是如果你只想对这个代码重用问题使用继承,给你的BaseController中的store()方法起一个名字,确保所有的类都有自己的store()方法并调用BaseController中的方法。像这样的:

BaseController.php

/**
 * Store a newly created resource in storage.
 *
 * @return Response
 */
protected function createResource(Request $request)

    $result = $this->repo->create($request);

    return response()->json($result, 200);

UserController.php

public function store(UserFormRequest $request)

    return $this->createResource($request);

【讨论】:

你不觉得创建一个store 和一个createResource 方法很奇怪吗,它们的目的相同,但名称却大相径庭?这似乎更像是一种解决方法而不是解决方案? 是的,这不是一个好的解决方案。正如我所说,PHP 中的继承并不是解决重复代码问题的最佳方法。但是,此解决方法与您尝试的方法最相似。但肯定有更好的解决方案。 @Tom,我添加了另一个使用额外服务类的解决方案。 在您的服务示例中,将UserRequest 对象传递给需要Request 对象而不是UserRequest 对象的StorageService 方法并不重要?同样的问题也适用于特征解决方案。 由于UserRequest扩展了RequestUserRequest的每个实例也是Request的一个实例,所以实例会匹配typehint,这应该没有问题。【参考方案2】:

你可以将你的逻辑从 BaseController 移动到 trait、service、facade。

你不能覆盖现有的函数并强制它使用不同类型的参数,它会破坏东西。例如,如果你以后会这样写:

function foo(BaseController $baseController, Request $request) 
    $baseController->store($request);

它会与您的UserControllerOtherRequest 中断,因为UserController 需要UserController,而不是OtherRequest(它扩展了Request 并且是foo() 角度的有效参数)。

【讨论】:

如果我将方法从 BaseController 移动到 trait,如何解决 BaseController(或新 trait)的 store 方法需要 Request 对象而不是FormRequestUserController 的 store 方法传递? 如果您使用相同的方法使用 trait,您将不得不在控制器 use ControllerTrait ControllerTrait::store as traitStore; 中更改它的名称。我个人会选择类似的东西:$this->helper->store($request)。如果每个孩子都会使用它,您甚至可以在 BaseController 中创建它。【参考方案3】:

正如其他人所提到的,出于多种原因,您无法做自己想做的事。如前所述,您可以使用特征或类似方法解决此问题。我提出了一种替代方法。

猜测一下,听起来您正在尝试遵循 Laravel 的 RESTful Resource Controllers 提出的命名约定,这有点强迫您在控制器上使用特定的方法,在本例中为 store

查看ResourceRegistrar.php 的来源,我们可以看到在getResourceMethods 方法中,Laravel 与您传入的选项数组和默认值进行比较或相交。但是,这些默认值受到保护,包括 store

这意味着您不能将任何内容传递给Route::resource 来强制覆盖路由名称。所以让我们排除这种可能性。

更简单的方法是为此路线简单地设置不同的方法。这可以通过以下方式实现:

Route::post('user/save', 'UserController@save');
Route::resource('users', 'UserController');

注意:根据文档,自定义路由必须在 Route::resource 调用之前。

【讨论】:

这需要我完全重写保存功能,对吧?我无法重复使用样板保存逻辑,导致重复代码 Laravel 易于使用,但以牺牲灵活性为代价。如果您正在寻找解耦的大部分独立组件,请查看 symfony2。【参考方案4】:

UserController::store() 的声明应该与BaseController::store() 兼容,这意味着(除其他外)BaseControllerUserController 的给定参数应该完全相同。

您实际上可以强制 BaseController 要求 UserFormRequest,这不是最漂亮的解决方案,但它确实有效。

通过覆盖,您无法将Request 替换为UserFormRequest,那么为什么不同时使用两者呢?为这两种方法提供一个可选参数以注入 UserFormRequest 对象。这将导致:

BaseController.php

class BaseController 

  public function store(Request $request, UserFormRequest $userFormRequest = null)
  
      $result = $this->repo->create($request);
      return response()->json($result, 200);
  


UserController.php

class UserController extends BaseController 

    public function __construct(UserRepository $repo)
    
        $this->repo = $repo;
    

    public function store(UserFormRequest $request, UserFormRequest $userFormRequest = null)
    
        return parent::store($request);
    


这样你可以在使用BaseController::store()时忽略参数,在使用UserController::store()时注入。

【讨论】:

【参考方案5】:

我发现规避该问题的最简单、最简洁的方法是在父方法前加上下划线。例如:

基础控制器:

_store(Request $request) ... _update(Request $request) ...

用户控制器:

store(UserFormRequest $request) return parent::_store($request); update(UserFormRequest $request) return parent::_update($request);

我觉得创建服务提供者有点矫枉过正。我们在这里试图规避的不是 Liskov 替换原则,而只是缺少适当的 PHP 反射。毕竟,类型提示方法本身就是一种 hack。

这将迫使您在每个子控制器中手动实现 storeupdate。我不知道这对你的设计是否有影响,但在我的设计中,我为每个控制器使用自定义请求,所以无论如何我都必须这样做。

【讨论】:

以上是关于Laravel 5:在从 BaseController 扩展的控制器中对 FormRequest 类进行类型提示的主要内容,如果未能解决你的问题,请参考以下文章

php - Laravel:如何在从下拉列表中选择更改后加载 Ajax 数据?

在从laravel ajax返回的json中的每个斜杠中获取额外的反斜杠“\”

$request->user() 在 Laravel 5.5 中返回 null

laravel 5 简单的 ajax 从数据库中检索记录

Laravel 5.2:邮件没有从cron-job发送,从邮件响应中收到错误

使用 Laravel 删除文件