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,因为它适用于任何资源。
我可以在BaseController
和UserController
中使用类似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()
方法。如果您觉得自己在重复代码,请考虑引入例如服务类或特征。
使用服务类
下面是使用额外服务类的解决方案。对于您的情况,这可能有点矫枉过正。但是,如果您向 StoringService
s 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
扩展了Request
,UserRequest
的每个实例也是Request
的一个实例,所以实例会匹配typehint,这应该没有问题。【参考方案2】:
你可以将你的逻辑从 BaseController 移动到 trait、service、facade。
你不能覆盖现有的函数并强制它使用不同类型的参数,它会破坏东西。例如,如果你以后会这样写:
function foo(BaseController $baseController, Request $request)
$baseController->store($request);
它会与您的UserController
和OtherRequest
中断,因为UserController
需要UserController
,而不是OtherRequest
(它扩展了Request
并且是foo()
角度的有效参数)。
【讨论】:
如果我将方法从 BaseController 移动到 trait,如何解决 BaseController(或新 trait)的store
方法需要 Request
对象而不是FormRequest
被 UserController
的 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()
兼容,这意味着(除其他外)BaseController
和UserController
的给定参数应该完全相同。
您实际上可以强制 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。
这将迫使您在每个子控制器中手动实现 store
和 update
。我不知道这对你的设计是否有影响,但在我的设计中,我为每个控制器使用自定义请求,所以无论如何我都必须这样做。
【讨论】:
以上是关于Laravel 5:在从 BaseController 扩展的控制器中对 FormRequest 类进行类型提示的主要内容,如果未能解决你的问题,请参考以下文章
php - Laravel:如何在从下拉列表中选择更改后加载 Ajax 数据?
在从laravel ajax返回的json中的每个斜杠中获取额外的反斜杠“\”
$request->user() 在 Laravel 5.5 中返回 null