Подтвердить что ты не робот

Laravel 5: Type-hinting класс FormRequest внутри контроллера, который простирается от BaseController

У меня есть BaseController, который обеспечивает основу большинства HTTP-методов для моего сервера API, например. метод 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, чтобы добавить новый класс FormRequest класса Laravel 5, который позаботится о таких вещах, как проверка и аутентификация для ресурса User. Я хотел бы сделать это, например, путем перезаписи метода хранилища и использования инъекции зависимостей типа Laravel для класса Form Request.

UserController.php

public function store(UserFormRequest $request)
{
    return parent::store($request);
}

Где UserFormRequest простирается от 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

Итак, как я могу ввести подсказку, введя UserFormRequest, все еще придерживаясь требования запроса BaseController? Я не могу заставить BaseController потребовать UserFormRequest, потому что он должен работать для любого ресурса.

Я мог бы использовать интерфейс типа RepositoryFormRequest как в BaseController, так и в UserController, но тогда проблема заключается в том, что Laravel больше не вводит UserFormController через свой тип, навязывающий инъекцию зависимостей.

4b9b3361

Ответ 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
}

Это приводит к той же ошибке, что и ваш код. Я бы просто не добавил метод store() в ваш BaseController. Если вы чувствуете, что повторяете код, подумайте о том, чтобы ввести, например, класс обслуживания или, может быть, черту.

Использование класса обслуживания

Ниже решения, которое использует дополнительный класс обслуживания. Это может быть излишним для вашей ситуации. Но если вы добавите больше функций в метод StoringService 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 constructor and make $service a member variable
        return $service->store($request);
    }

}

Использование признака

Вы также можете использовать признак, но вы должны переименовать метод 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. Но если вы хотите использовать наследование только для этой проблемы повторного использования кода, дайте метод store() в вашем BaseController другом имени, убедитесь, что все классы имеют свой собственный метод 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);
}

Ответ 2

Вы можете переместить свою логику с BaseController на свойство, сервис, фасад.

Вы не можете переопределить существующую функцию и заставить ее использовать аргумент другого типа, это сломает материал. Например, если вы позже напишете это:

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

Он сломается с вашими UserController и OtherRequest, потому что UserController ожидает UserController, а не OtherRequest (который расширяет Request и является допустимым аргументом из перспективы foo()).

Ответ 3

Как уже упоминалось, вы не можете делать то, что хотите сделать по целому ряду причин. Как уже упоминалось, вы можете решить эту проблему с чертами или схожими. Я представляю альтернативный подход.

Полагаю, похоже, что вы пытаетесь следовать соглашению об именах, указанному Laravel RESTful Resource Controllers, что заставляет вас использовать конкретный метод на контроллере, в данном случае store.

Посмотрев на источник ResourceRegistrar.php, мы увидим, что в методе getResourceMethods Laravel выполняет либо diff, либо пересекается с массивом параметров, который вы передаете, и против значений по умолчанию. Тем не менее, эти значения по умолчанию защищены и включают store.

Это означает, что вы не можете передать что-либо в Route::resource, чтобы принудительно переопределить имена маршрутов. Так что пусть это выйдет.

Более простой подход состоял бы в том, чтобы просто настроить другой метод только для этого маршрута. Этого можно добиться, выполнив:

Route::post('user/save', '[email protected]');
Route::resource('users', 'UserController');

Примечание. В соответствии с документацией пользовательские маршруты должны появиться до вызова Route:: resource.

Ответ 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

Самый простой и самый чистый способ, который я нашел, чтобы обойти эту проблему, - это префикс родительских методов с помощью подчеркивания. Например:

BaseController:

  • _store(Request $request) { ... }
  • _update(Request $request) { ... }

UserController:

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

Я чувствую, что создание провайдеров услуг является излишним. То, что мы пытаемся обойти здесь, - это не принцип замещения Лискова, а просто отсутствие правильного отражения PHP. Методы подсказки типов сами по себе являются взломом.

Это заставит вас вручную реализовать store и update в каждом дочернем контроллере. Я не знаю, что это надоедливо для вашего дизайна, но в моем случае я использую пользовательские запросы для каждого контроллера, поэтому мне все равно пришлось это делать.