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

26

我有一个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);
}

我接着在一个更具体的控制器中,比如说UserController,继承这个BaseController,像这样:

UserController.php

class UserController extends BaseController {

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

}

这很好用。然而,我现在想扩展UserController以注入Laravel 5的新FormRequest类,它可以处理像验证和认证User资源这样的事情。我想这样做,通过重写存储方法并使用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对象。因此我得到了这个错误:

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的Request要求的情况下进行类型提示注入UserFormRequest?我不能强制BaseController需要一个UserFormRequest,因为它应该适用于任何资源。

我可以在BaseController和UserController中都使用一个接口,比如RepositoryFormRequest,但问题是Laravel不再通过其类型提示依赖注入注入UserFormController。


你可以将BaseController中的方法移动到Trait中,并在其他控制器上使用它。 - Ravan Scafi
你是否正在使用Laravel的RESTful资源控制器?那么是否要坚持使用预设名称,如“store”? - Chris
@Chris 是的,我是。不太确定你的问题,能否请你重新表述一下? - Tom
呃,这是方法类型提示中的一个巨大设计缺陷...让整个东西看起来几乎毫无用处。 - Lazlo
6个回答

10

与许多“真实的”面向对象语言相反,在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()方法。如果你觉得自己在重复代码,请考虑引入一个服务类或者使用trait等方式。

使用服务类

下面是一个使用额外服务类的解决方案。对于你的情况来说,这可能有些过度设计。但如果StoringServicestore()方法(例如验证)添加了更多功能,它可能会很有用。你也可以向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,但必须重命名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()方法添加额外的功能,您可以直接使用该特征而无需重命名,并且不必编写额外的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 方法,它们有着相同的目的但名称却大相径庭,这是不是很奇怪?这似乎更像是一种变通而非解决方案? - Tom
1
是的,这不是一个好的解决方案。就像我说的,PHP中的继承并不是解决这个重复代码问题的最佳方式。然而,这种方法是最接近你尝试过的。但是肯定有更好的解决方案。 - Jeroen Noten
@Tom,我添加了另一种解决方案,它使用了一个额外的服务类。 - Jeroen Noten
在您的服务示例中,将UserRequest对象传递给期望Request对象而不是UserRequest对象的StorageService方法,是否无关紧要?相同的问题也适用于特质解决方案。 - Tom
由于UserRequest扩展了Request,因此每个UserRequest实例也是Request的实例,因此该实例将匹配类型提示,这不应该是一个问题。 - Jeroen Noten

4

您可以将控制器中的逻辑移动到 trait、service 或 facade 中。

您不能覆盖现有函数并强制其使用不同类型的参数,否则会导致问题。例如,如果您稍后编写以下内容:

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

由于 UserController 期望的是 UserController,而不是从 foo() 的角度来看是合法参数的 OtherRequest(它继承自 Request),所以会与你的 UserControllerOtherRequest 冲突。


如果我将方法从BaseController移动到trait中,那么如何解决BaseController(或新trait)的store方法期望一个Request对象而不是由UserController的store方法传递的FormRequest的问题? - Tom
如果您在trait中使用相同的方法,您将不得不在控制器中更改它的名称 use ControllerTrait { ControllerTrait::store as traitStore; }。个人建议使用类似于 $this->helper->store($request) 的方式。如果每个子类都会使用它,甚至可以在您的 BaseController 中创建它。 - Daniel Antos

4

正如其他人所提到的,由于许多原因,您无法按照自己的意愿进行操作。正如所述,您可以通过特征或类似方法来解决此问题。我提供了一种替代方法。

猜测您正在尝试遵循Laravel的“RESTful资源控制器”提出的命名约定,这有点强制您在控制器上使用特定方法,例如store

查看ResourceRegistrar.php的源代码,我们可以看到在getResourceMethods方法中,Laravel会对您传递的选项数组和默认值进行差异或交集处理。但是,这些默认值受到保护,包括store

这意味着您无法向Route::resource传递任何内容以强制覆盖路由名称。所以让我们排除这个方案。

一个更简单的方法是为此路由设置一个不同的方法。可以通过执行以下操作实现:

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

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

这需要我完全重写保存函数,对吗?我无法重用样板保存逻辑,这会导致重复的代码。 - Tom
1
Laravel 提供了易于使用的特性,但代价是灵活性较差。如果您正在寻找解耦合、大部分独立的组件,请考虑使用 Symfony2。 - Mysteryos

2
UserController::store() 的声明应与 BaseController::store() 兼容,这意味着(除其他事项外),BaseControllerUserController的给定参数应完全相同。
实际上,您可以强制使用UserFormRequest要求BaseController,这不是最美观的解决方案,但它可以工作。
通过覆盖,您无法用 UserFormRequest 替换 Request ,那为什么不同时为两种方法提供可注入 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()时注入它。


2
我发现绕过该问题最简单和最干净的方法是在父级方法名前加下划线。例如:
BaseController:
- _store(Request $request){...} - _update(Request $request){...}
UserController:
- store(UserFormRequest $request){return parent::_store($request);} - update(UserFormRequest $request){return parent::_update($request);}
我认为创建服务提供程序有些过度了。我们试图规避的不是Liskov替换原则,而只是缺乏适当的PHP反射。毕竟,类型提示方法本身就是一种hack。
这将强制你在每个子控制器中手动实现存储和更新。我不知道这是否会影响您的设计,但在我的设计中,我为每个控制器使用自定义请求,因此我必须这样做。

0
如果我使用DI将UserFormRequest发送到UserController的__construct中,那么BaseController的所有函数都将使用UserFormRequest进行验证。
class UserController extends BaseController
{
    protected $service;

    public function __construct(IUserService $service, UserFormRequest  $validation)
    {

        parent::__construct($service, $validation);
    }
}

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