2015-04-22 12 views
25

Ho uno BaseController che fornisce la base per la maggior parte dei metodi HTTP per il mio server API, ad es. il metodo store:Laravel 5: digitare una classe FormRequest all'interno di un controller che si estende da BaseController

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

poi estendo su questa BaseController in un controllore più specifico, come il UserController, così:

UserController.php

class UserController extends BaseController { 

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

} 

Funziona alla grande. Tuttavia, ora desidero estendere UserController per iniettare la nuova classe di Laravel 5 FormRequest, che si occupa di cose come la convalida e l'autenticazione per la risorsa User. Mi piacerebbe farlo in questo modo, sovrascrivendo il metodo store e usando l'injection di tipo hint di Laravel per la sua classe Form Request.

UserController.php

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

Qualora la UserFormRequest estende da Request, che si estende da 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' 
     ]; 
    } 

} 

Il problema è che il BaseController richiede un oggetto Illuminate\Http\Request mentre io passo un oggetto UserFormRequest. Perciò io ottengo questo errore:

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 

Quindi, come posso digitare accennare iniettare l'UserFormRequest, pur aderendo alla richiesta requisito del BaseController? Non posso forzare il BaseController a richiedere un UserFormRequest, perché dovrebbe funzionare per qualsiasi risorsa.

ho potuto utilizzare un'interfaccia come RepositoryFormRequest sia nel BaseController e UserController, ma il problema è che laravel più inietta il UserFormController attraverso il suo tipo suggerendo iniezione di dipendenza.

+0

È possibile spostare i metodi dal BaseController a un tratto e quindi utilizzarlo sugli altri controller. – Ravan

+0

Stai usando i controller di risorse RESTful di Laravel? E quindi cosa attaccare ai nomi preimpostati come 'store'? – Chris

+0

@Chris si sono. Non sono sicuro di seguire la tua domanda, potresti riformulare la frase? – Tom

risposta

7

A differenza di molti linguaggi orientati agli oggetti 'reali', questo tipo di progetto di tipo hinting nei metodi override non è solo possibile in PHP, si veda:

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 
} 

Ciò produce lo stesso tipo di errore il tuo codice . Non inserirò il metodo store() nel tuo BaseController. Se ritieni di ripetere il codice, prendi in considerazione di introdurre per esempio una classe di servizio o forse un tratto.

Utilizzando una classe di servizio

Qui di seguito una soluzione che si avvale di un servizio di classe in più. Questo potrebbe essere eccessivo per la tua situazione. Ma se si aggiungono altre funzionalità al metodo store()store() (come la convalida), potrebbe essere utile.È inoltre possibile aggiungere ulteriori metodi allo StoringService come destroy(), update(), create(), ma in questo caso si consiglia di denominare il servizio in modo diverso.

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

} 

Usando un tratto

È inoltre possibile utilizzare un tratto, ma è necessario rinominare il metodo del tratto store() poi:

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

} 

Il vantaggio di questa soluzione è che se si non è necessario aggiungere funzionalità aggiuntive al metodo store(), è possibile solo use il tratto senza ridenominazione e non è necessario scrivere un ulteriore metodo store().

Utilizzando eredità

A mio parere, l'eredità non è così adatto per il tipo di riutilizzo del codice che avete bisogno qui, almeno non in PHP. Se si desidera utilizzare l'ereditarietà solo per questo problema di riutilizzo del codice, assegnare il metodo store() al proprio nome BaseController, assicurarsi che tutte le classi abbiano il proprio metodo store() e chiamare il metodo nello BaseController. Qualcosa di simile a questo:

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

Non pensi che sia strano creare un metodo 'store' e un metodo' createResource', che hanno uno scopo identico, ma nomi molto diversi? Sembra più una soluzione alternativa che una soluzione? – Tom

+1

Sì, non è una buona soluzione. Come ho detto, l'ereditarietà in PHP non è il modo migliore per risolvere questo problema di codice ripetuto. Questo lavoro comunque è il più simile a quello che hai provato. Ma ci sono sicuramente soluzioni migliori. –

+0

@ Tom, ho aggiunto un'altra soluzione che utilizza una classe di servizio aggiuntiva. –

4

È possibile spostare la logica da BaseController di tratto, il servizio, facciata.

Non è possibile sovrascrivere la funzione esistente e imporre l'utilizzo di un diverso tipo di argomento, altrimenti si interromperà. Ad esempio, se in seguito si sarebbe scrivere questo:

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

Sarebbe rompere con il tuo UserController e OtherRequest perché UserController aspetta UserController, non OtherRequest (che si estende Request ed è argomento valido dal punto di vista foo()).

+0

Se sposto il metodo da BaseController a una caratteristica, come risolve il problema in cui il metodo BaseController (o il nuovo carattere) 'store' si aspetta un oggetto' Request' piuttosto che un 'FormRequest' come passato dal Il metodo di store 'UserController'? – Tom

+0

Se si utilizza la caratteristica con lo stesso metodo, sarà necessario cambiarne il nome nel controller 'use ControllerTrait {ControllerTrait :: store as traitStore; } '. Personalmente vorrei andare con qualcosa come: '$ this-> helper-> store ($ request)'. Puoi persino crearlo nel tuo BaseController se tutti i bambini lo useranno. –

4

Come altri hanno già detto, non è possibile fare ciò che si vuole fare per una serie di motivi. Come già detto, puoi risolvere questo problema con tratti o simili. Sto presentando un approccio alternativo.

A quanto pare, sembra che tu stia cercando di seguire la convenzione di denominazione proposta da Laravel RESTful Resource Controllers, che ti costringe a utilizzare un particolare metodo su un controller, in questo caso, store.

Guardando la fonte di ResourceRegistrar.php possiamo vedere che nel metodo getResourceMethods, Laravel fa un diff o intersecano con l'array di opzioni che si passa dentro e contro i valori predefiniti. Tuttavia, quelle impostazioni predefinite sono protette e includono store.

Ciò significa che non è possibile passare nulla a Route::resource per forzare l'override dei nomi di percorso. Quindi escludiamolo.

Un approccio più semplice sarebbe semplicemente impostare un metodo diverso solo per questa rotta. Ciò può essere ottenuto facendo:

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

Nota: Secondo la documentazione, i percorsi personalizzati devono venire prima della chiamata Strada :: risorsa.

+0

Ciò mi richiederebbe di riscrivere completamente la funzione di salvataggio, giusto? Non riesco a riutilizzare la logica di salvataggio di boilerplate, con conseguente ripetizione del codice – Tom

+1

Laravel offre facilità d'uso, il che va a discapito della flessibilità. Se stai cercando componenti disaccoppiati per lo più indipendenti, guarda in symfony2. – Mysteryos

2

La dichiarazione di UserController::store() dovrebbe essere compatibile con BaseController::store(), che significa (tra le altre cose) che i parametri riportati sia per il BaseController nonché UserController dovrebbe essere esattamente lo stesso.

In realtà, è possibile forzare il BaseController a richiedere un UserFormRequest, non è la soluzione più carina, ma funziona.

Sovrascrivendo non è possibile sostituire Request con UserFormRequest, quindi perché non utilizzare entrambi? Fornendo a entrambi i metodi un parametro facoltativo per l'iniezione dell'oggetto UserFormRequest. Che si tradurrebbe in:

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

} 

questo modo è possibile ignorare il parametro quando si utilizza BaseController::store() e iniettare quando si usa UserController::store().

2

Il modo più semplice e più pulito che ho trovato per aggirare il problema era di anteporre i metodi parent con un carattere di sottolineatura. Ad esempio:

BaseController:

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

UserController:

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

Mi sento come la creazione di fornitori di servizi è un eccesso. Quello che stiamo cercando di aggirare qui non è il principio di sostituzione di Liskov, ma semplicemente la mancanza di una corretta riflessione di PHP. Dopotutto, i metodi di suggerimento del tipo sono, di per sé, un hack.

In questo modo sarà necessario implementare manualmente uno store e update in ogni controller secondario. Non so se questo è fastidioso per il tuo progetto, ma nel mio, io uso richieste personalizzate per ogni controller, quindi ho dovuto farlo comunque.

Problemi correlati