Laravel 5: Type-Hinting einer FormRequest-Klasse innerhalb eines Controllers, der sich von BaseController aus erstreckt

Lesezeit: 9 Minuten

Ich habe ein BaseController das bildet die Grundlage für die meisten HTTP-Methoden für meinen API-Server, z store Methode:

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);
}

Darauf erweitere ich dann BaseController in einem spezifischeren Controller, wie z UserControllerso:

UserController.php

class UserController extends BaseController {

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

}

Das funktioniert super. Nun möchte ich aber verlängern UserController Laravel 5 neu zu spritzen FormRequest -Klasse, die sich um Dinge wie Validierung und Authentifizierung für die kümmert User Ressource. Ich würde das gerne so machen, indem ich die Store-Methode überschreibe und Laravels Type Hint Dependency Injection für seine Form Request-Klasse verwende.

UserController.php

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

Bei dem die UserFormRequest erstreckt sich von Requestdie sich selbst aus erstreckt 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'
        ];
    }

}

Das Problem ist, dass die BaseController benötigt ein Illuminate\Http\Request Objekt, während ich a passiere UserFormRequest Objekt. Daher bekomme ich diesen Fehler:

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

Wie kann ich also einen Hinweis eingeben, der die UserFormRequest injiziert, während ich mich weiterhin an die Request-Anforderung des BaseControllers halte? Ich kann den BaseController nicht zwingen, eine UserFormRequest anzufordern, da es für jede Ressource funktionieren sollte.

Ich könnte eine Schnittstelle wie verwenden RepositoryFormRequest in beiden BaseController und die UserControlleraber dann ist das Problem, dass Laravel das nicht mehr spritzt UserFormController durch seine Typhinweis-Abhängigkeitsinjektion.

  • Sie können die Methoden von Ihrem BaseController zu einem Trait verschieben und dann auf den anderen Controllern verwenden.

    – Ravan Scafi

    22. April 2015 um 11:26 Uhr

  • Verwenden Sie die RESTful Resource Controller von Laravel? Und so was hält man gerne an den voreingestellten Namen fest store?

    – Chris

    22. April 2015 um 12:03 Uhr

  • @Chris ja bin ich. Ich bin mir nicht sicher, ob ich deiner Frage folgen kann, könntest du sie bitte umformulieren?

    – Tom

    23. April 2015 um 5:43 Uhr

  • Ugh, das ist ein riesiger Designfehler beim Methoden-Type-Hinting … Lässt das Ganze fast nutzlos erscheinen.

    – Laslo

    9. Mai 2015 um 18:04 Uhr

Benutzer-Avatar
Jeroen Noten

Im Gegensatz zu vielen „echten“ objektorientierten Sprachen ist diese Art von Type-Hinting-Design in überschriebenen Methoden in PHP einfach nicht möglich, siehe:

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
}

Dies erzeugt die gleiche Art von Fehler wie Ihr Code. Ich würde einfach keine setzen store() Methode in Ihrer BaseController. Wenn Sie das Gefühl haben, Code zu wiederholen, sollten Sie beispielsweise eine Serviceklasse oder vielleicht eine Eigenschaft einführen.

Verwenden einer Dienstklasse

Nachfolgend eine Lösung, die eine zusätzliche Serviceklasse nutzt. Dies könnte für Ihre Situation übertrieben sein. Aber wenn Sie mehr Funktionalität hinzufügen StoringServices store() Methode (wie Validierung), könnte es nützlich sein. Sie können dem auch weitere Methoden hinzufügen StoringService wie destroy(), update(), create()aber dann möchten Sie den Dienst wahrscheinlich anders benennen.

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);
    }

}

Verwenden einer Eigenschaft

Sie können auch ein Merkmal verwenden, aber Sie müssen das Merkmal umbenennen store() Methode dann:

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);
    }

}

Der Vorteil dieser Lösung besteht darin, dass Sie dem keine zusätzliche Funktionalität hinzufügen müssen store() Methode können Sie einfach use das Trait ohne Umbenennung und Sie müssen kein Extra schreiben store() Methode.

Vererbung verwenden

Meiner Meinung nach ist Vererbung nicht so geeignet für die Art der Code-Wiederverwendung, die Sie hier benötigen, zumindest nicht in PHP. Wenn Sie jedoch nur die Vererbung für dieses Problem der Codewiederverwendung verwenden möchten, geben Sie die store() Methode in Ihrer BaseController einen anderen Namen, stellen Sie sicher, dass alle Klassen ihren eigenen haben store() Methode und rufen Sie die Methode in der auf BaseController. Etwas wie das:

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);
}

  • Finden Sie es nicht seltsam, eine zu erstellen? store und ein createResource Methode, die einen identischen Zweck haben, aber sehr unterschiedliche Namen haben? Es scheint eher ein Workaround als eine Lösung zu sein?

    – Tom

    23. April 2015 um 5:40 Uhr

  • Ja, das ist keine gute Lösung. Wie ich bereits sagte, ist die Vererbung in PHP nicht der beste Weg, um dieses Problem des sich wiederholenden Codes zu lösen. Diese Umgehung ist jedoch dem, was Sie versucht haben, am ähnlichsten. Aber es gibt sicher bessere Lösungen.

    – Jeroen Noten

    23. April 2015 um 10:49 Uhr

  • @Tom, ich habe eine weitere Lösung hinzugefügt, die eine zusätzliche Serviceklasse verwendet.

    – Jeroen Noten

    23. April 2015 um 11:05 Uhr

  • In Ihrem Servicebeispiel spielt es keine Rolle, dass Sie a bestehen UserRequest Widerspruch gegen die StorageService‘s-Methode, die a erwartet Request Objekt, anstatt a UserRequest Objekt? Das gleiche Problem gilt für die Merkmalslösung.

    – Tom

    23. April 2015 um 12:27 Uhr

  • Seit UserRequest erweitert Requestjede Instanz von UserRequest ist auch eine Instanz von Requestsodass die Instanz mit dem Typhinweis übereinstimmt, sollte dies kein Problem darstellen.

    – Jeroen Noten

    24. April 2015 um 13:17 Uhr

Sie können Ihre Logik von BaseController zu Trait, Service, Facade verschieben.

Sie können eine vorhandene Funktion nicht überschreiben und sie dazu zwingen, einen anderen Argumenttyp zu verwenden, da dies zu Problemen führen würde. Wenn Sie zum Beispiel später Folgendes schreiben würden:

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

Es würde mit dir brechen UserController und OtherRequest Weil UserController erwartet UserControllernicht OtherRequest (was verlängert Request und ist gültiges Argument von foo() Perspektive).

  • Wenn ich die Methode vom BaseController zu einem Trait verschiebe, wie löst das das Problem, wo der BaseController (oder das neue Trait) ist store Methode erwartet a Request Objekt statt a FormRequest wie vorbei an der UserController‘s Store-Methode?

    – Tom

    23. April 2015 um 5:42 Uhr

  • Wenn Sie Trait mit der gleichen Methode verwenden, müssen Sie den Namen im Controller ändern use ControllerTrait { ControllerTrait::store as traitStore; }. Ich persönlich würde so etwas nehmen wie: $this->helper->store($request). Sie können dies sogar in Ihrem erstellen BaseController wenn jedes Kind es benutzt.

    – Daniel Antos

    23. April 2015 um 7:18 Uhr

Wie andere bereits erwähnt haben, können Sie aus einer Vielzahl von Gründen nicht tun, was Sie tun möchten. Wie erwähnt, kann man dieses Problem mit Traits oder ähnlichem lösen. Ich stelle einen alternativen Ansatz vor.

Vermutlich klingt es so, als würden Sie versuchen, der von Laravel aufgestellten Namenskonvention zu folgen RESTful Resource Controllerswas Sie in diesem Fall dazu zwingt, eine bestimmte Methode auf einem Controller zu verwenden, store.

Blick auf die Quelle von ResourceRegistrar.php wir können das in der sehen getResourceMethods -Methode führt Laravel entweder einen Unterschied aus oder schneidet sich mit dem Options-Array, das Sie übergeben, und gegen die Standardwerte. Diese Standardwerte sind jedoch geschützt und enthalten store.

Das bedeutet, dass Sie nichts weitergeben können Route::resource um eine Überschreibung der Routennamen zu erzwingen. Also schließen wir das aus.

Ein einfacherer Ansatz wäre, einfach nur für diese Route eine andere Methode einzurichten. Dies kann erreicht werden, indem Sie Folgendes tun:

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

Hinweis: Gemäß der Dokumentation müssen die benutzerdefinierten Routen vor dem Aufruf Route::resource kommen.

  • Dazu müsste ich die Speicherfunktion komplett neu schreiben, richtig? Ich kann die Boilerplate-Speicherlogik nicht wiederverwenden, was zu sich wiederholendem Code führt

    – Tom

    23. April 2015 um 5:43 Uhr

  • Laravel bietet Benutzerfreundlichkeit, die auf Kosten der Flexibilität geht. Wenn Sie nach entkoppelten, weitgehend unabhängigen Komponenten suchen, schauen Sie sich Symfony2 an.

    – Rätsel

    28. April 2015 um 4:23 Uhr

Benutzer-Avatar
Amir

Die Erklärung von UserController::store() sollte kompatibel sein BaseController::store()was (unter anderem) bedeutet, dass die angegebenen Parameter sowohl für die BaseController ebenso gut wie UserController sollte genau gleich sein.

Sie können den BaseController tatsächlich zwingen, eine UserFormRequest anzufordern, es ist nicht die schönste Lösung, aber es funktioniert.

Durch Überschreiben gibt es keine Möglichkeit, sie zu ersetzen Request mit UserFormRequest, warum also nicht beide verwenden? Geben Sie beiden Methoden einen optionalen Parameter zum Injizieren der UserFormRequest Objekt. Was dazu führen würde:

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);
    }

}

Auf diese Weise können Sie den Parameter bei der Verwendung ignorieren BaseController::store() und injizieren Sie es bei der Verwendung UserController::store().

Der einfachste und sauberste Weg, dieses Problem zu umgehen, bestand darin, den übergeordneten Methoden einen Unterstrich voranzustellen. Zum Beispiel:

BasisController:

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

BenutzerController:

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

Ich habe das Gefühl, dass die Schaffung von Dienstleistern ein Overkill ist. Was wir hier zu umgehen versuchen, ist nicht das Substitutionsprinzip von Liskov, sondern einfach das Fehlen einer angemessenen PHP-Reflexion. Type-Hinting-Methoden sind an sich schon ein Hack.

Dies zwingt Sie, a manuell zu implementieren store und update in jedem untergeordneten Controller. Ich weiß nicht, ob das für Ihr Design störend ist, aber in meinem verwende ich benutzerdefinierte Anforderungen für jeden Controller, also musste ich es trotzdem tun.

1054290cookie-checkLaravel 5: Type-Hinting einer FormRequest-Klasse innerhalb eines Controllers, der sich von BaseController aus erstreckt

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy