I'm currently rebuilding my vanilla-PHP-App with Laravel and I have the following problem.
I have multiple database-tables, that represent word categories (noun, verb, adverb, ...). For each table I created a separate Model, a route::resource and a separate resource-Controller. For example:
NomenController.php
public function show($id)
{
$vocab = Nomen::find($id);
return view('glossarium.vocab_update', compact('vocab'));
}
and
VerbController.php
public function show($id)
{
$vocab = Verb::find($id);
return view('glossarium.vocab_update', compact('vocab'));
}
...which are essentially the same except the Model class.
I don't want to create a separate Controller for each model, that does exactly the same. What would be the most simple and elegant way to solve this?
Should I just create a VocabController.php and add a parameter for the Model-name like:
Route::resource('/vocab/{category}', 'VocabController');
and then add a constructor method in this controller like
public function __construct ($category) {
if ($category == 'nomen') {
$this->vocab = App\Nomen;
}
else if ($category == 'verb') {
$this->vocab = App\Verb;
}
}
I wonder if there is a simpler method to do that. Can I somehow do this with Route Model Binding?
Thanks in advance
Simply create a trait like this in App\Traits, (you can name it anything... Don't go with mine though... I feel its pretty lame... :P)
namespace App\Traits;
trait CommonControllerFunctions {
public function show($id) {
$modelObject = $this->model;
$model = $modelObject::find($id);
return view('glossarium.vocab_update', compact('model'));
}
}
and in your NomenController and VerbController, do this:
use App\Traits\CommonControllerFunctions;
class NomenController {
use CommonControllerFunctions;
protected $model = Nomen::class;
}
and
use App\Traits\CommonControllerFunctions;
class VerbController {
use CommonControllerFunctions;
protected $model = Verb::class;
}
Note: Please note that this example is just a work-around for your particular situation only... Everyone practices code differently, so this method might not be approved by all...
I think the simpliest way it to create only one controller, eg VocabController with methods nomen, verb and whatever you want.
Routes:
Route::get('/vocab/nomen/{nomen}', 'VocabController#item');
Route::get('/vocab/verb/{verb}', 'VocabController#item');
And the model binding:
Route::model('nomen', 'App\Nomen');
Route::model('verb', 'App\Varb');
Then your method shoud look like that:
public function item($item)
{
return view('glossarium.vocab_update', $item);
}
Keep in mind, that $item is already fetched model from the database.
Related
I am somewhat new to OOP, although I know about interfaces and abstract classes a bit. I have a lot of resource controllers that are somewhat similar in the bigger scheme of things, they all look like the example below, the only main difference is the index and what I pass to the index view.
What I simply need to know is, can I OO things up a bit with my resource controllers? For example, create one "main" resource controller in which I simply pass the correct instances using an interface for example? I tried playing around with this but I got an error that the interface wasn't instantiable, so I had to bind it. But that means I could only bind an interface to a specific controller.
Any advice, tips and pointers will help me out :)
class NotesController extends Controller
{
public function index()
{
$notes = Note::all();
return view('notes.index', compact('notes'));
}
public function create()
{
return view('notes.create');
}
public function show(Note $note)
{
return view('notes.show', compact('note'));
}
public function edit(Note $note)
{
return view('notes.edit', compact('note'));
}
public function store(Request $request, User $user)
{
$user->getNotes()->create($request->all());
flash()->success('The note has been stored in the database.', 'Note created.');
return Redirect::route('notes.index');
}
public function update(Note $note, Request $request)
{
$note->update($request->all());
flash()->success('The note has been successfully edited.', 'Note edited.');
return Redirect::route('notes.index');
}
public function delete($slug)
{
Note::where('slug', '=', $slug)->delete();
return Redirect::to('notes');
}
}
Note: Totally my opinion!
I would keep them how you have them. It makes them easier to read and understand later. Also will save you time when you need to update one to do something different from the rest. We tried this in a project I worked on and while granted it wasn't the best implementation, it is still a pain point to this day.
Up to you though. I'm sure people have done that in a way that they love and works great. Just hasn't been the case in my experience. I doubt anyone would look at your code though and criticize you for not doing it.
In Case you need to bind different Model instanses then you may use Contextual Binding, for example, put the following code in AppServiceProvider's register() method:
$this->app->when('App\Http\Controllers\MainController')
->needs('Illuminate\Database\Eloquent\Model')
->give(function () {
$path = $this->app->request->path();
$resource = trim($path, '/');
if($pos = strpos($path, '/')) {
$resource = substr($path, 0, $pos);
}
$modelName = studly_case(str_singular($resource));
return app('App\\'.$modelName); // return the appropriate model
});
In your controller, use a __construct method to inject the model like this:
// Put the following at top of the class: use Illuminate\Database\Eloquent\Model;
public function __construct(Model $model)
{
$this->model = $model;
}
Then you may use something like this:
public function index()
{
// Extract this code in a separate method
$array = explode('\\', get_class($this->model));
$view = strtolower(end($array));
// Load the result
$result = $this->model->all();
return view($view.'.index', compact('result'));
}
Hope you got the idea so implement the rest of the methods.
Before anyone asks, I've looked into CRUD generators and I know all about the Laravel Resource routes, but that's not exactly what I'm pulling for here.
What I'm looking to do is create one Route with a couple parameters, and one global class that (uses/extends?) the Model controller for simple CRUD operations. We have 20 or so Models and creating a Resource Controller for each table would be more time consuming than finding a way to create a global CRUD class to handle all "api" type calls and any ajax json request like a create / update / destroy statement.
So my question is what is the cleanest and best way to structure a class to handle all CRUD requests for every Model we have without having to have a resource controller for every model? I've tried researching this and can't seem to find any links except ones to CRUD generators and links describing the laravel Resource route.
The easiest way would be to do the following:
Add a route for your resource controller:
Route::resource('crud', 'CrudController', array('except' => array('create', 'edit')));
Create your crud controller
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Models\User;
use App\Models\Product;
use Input;
class CrudController extends Controller
{
const MODEL_KEY = 'model';
protected $modelsMapping = [
'user' => User::class,
'product' => Product::class
];
protected function getModel() {
$modelKey = Input::get(static::MODEL_KEY);
if (array_key_exists($modelKey, $this->modelsMapping)) {
return $this->modelsMapping[$modelKey];
}
throw new \InvalidArgumentException('Invalid model');
}
public function index()
{
$model = $this->getModel();
return $model::all();
}
public function store()
{
$model = $this->getModel();
return $model::create(array_except(Input::all(), static::MODEL_KEY));
}
public function show($id)
{
$model = $this->getModel();
return $model::findOrFail($id);
}
public function update($id)
{
$model = $this->getModel();
$object = $model::findOrFail($id);
return $object->update(array_except(Input::all(), static::MODEL_KEY));
}
public function destroy($id)
{
$model = $this->getModel();
return $model::remove($id);
}
}
Use your new controller :) You have to pass the model parameter that will contain the model key - it must be one of the allowed models in the whitelist. E.g. if you want to get a User with id=5 do
GET /crud/5?model=user
Please keep in mind that it's as simple as possible, you might need to make the code more sophisticated to match your needs.
Please also keep in mind that this code has not been tested - let me know if you see any typos or have some other issues. I'll be more than happy to get it running for you.
Unless you want to implement CRUD manually, consider to integrate a ready-made datagrid such as phpGrid.
Check out integration walkthrough: http://phpgrid.com/example/phpgrid-laravel-5-twitter-bootstrap-3-integration/ No models are required and the code is minimum. It can almost do anything.
A basic working CRUD:
// in a controller
public function index()
{
$dg = new \C_DataGrid("SELECT * FROM orders", "orderNumber", "orders");
$dg->enable_edit("FORM", "CRUD");
$dg->display(false);
$grid = $dg -> get_display(true);
return view('dashboard', ['grid' => $grid]);
}
You need one generic class for all CRUD operations and there are many ways to achieve that and one rule for all may not fit but you may try the approach that I'm going to describe now. This is an abstract idea, you need to implement it, so at first, think the URI for all CRUD operations. In this case you must follow a convention and it could be something like this:
example.com/user/{id?} // get all or one by id (if id is available in the URI)
example.com/user/create // Show an empty form
example.com/user/edit/10 // Show a form populated with User model
example.com/user/save // Create a new User
example.com/user/save/10 // Update an existing User
example.com/user/delete/10 // Delete an existing User
In ths case the user could be something else to specify the name of the model for example, example.com/product/create and keeping that on mind, you need to declare routes as given below:
Route::get('/{model}/{id?}', 'CrudController#read');
Route::get('/{model}/create', 'CrudController#create');
Route::get('/{model}/edit/{id}', 'CrudController#edit');
Route::post('/{model}/save/{id?}', 'CrudController#save');
Route::post('/{model}/delete/{id}', 'CrudController#delete');
Now, in your app\Providers\RouteServiceProvider.php file modify the boot method and make it look like this:
public function boot(Router $router)
{
$model = null;
$router->bind('model', function($modelName) use (&$model, &$router)
{
$model = app('\App\User\\'.ucfirst($modelName));
if($model)
{
if($id = $router->input('id'))
{
$model = $model->find($id);
}
return $model ?: abort(404);
}
});
parent::boot($router);
}
Then declare your CrudController as given below:
class CrudController extends Controller
{
protected $request = null;
public function __construct(Request $request)
{
$this->request = $request;
}
public function read($model)
{
return $model->exists ? $model : $model->all();
}
// Show either an empty form or a form
// populated with the given model atts
public function createOrEdit($model)
{
$classNameArray = explode('\\', get_class($model));
$className = strtolower(array_pop($classNameArray));
$view = view($className . '.form');
$view->formAction = "$className/save";
if(is_object($model) && $model->exists)
{
$view->model = $model;
$view->formAction .= "/{$model->id}";
}
return $view;
}
public function save($model)
{
// Validation required so do it
// Make sure each Model has $fillable specified
return $this->model->fill($this->request)->save();
}
public function delete($model)
{
return $this->model->delete();
}
}
Since same form is used to creating and updating a model, use something like this to create a form:
<form action="{{url($formAction)}}" method="POST">
<input
type="text"
class="form-control"
name="first_name" value="{{old('first_name', #$model->first_name)}}"
/>
<input type="Submit" value="Submit" />
{!!csrf_field()!!}
</form>
Remember that, each form should be in a directory corresponding to the model, for user add/edit, form should be in views/user/form.blade.php and for product model use views/product/form.blade.php and so on.
This will work and don't forget to add validation before saving a model and validation could be done inside the model using model events or however you want. This is just an idea but probably not the best way to it.
So, lets say I do have a simple Controller that handles Books.
App\Http\Controllers\SimpleBooksController
Inside routes.php I register a route for it:
Route::get('books/{id}','SimpleBooksController#doSimpleStuff');
But the world of books is not so simple. So I would like to have another Controller that handles the really complex book stuff.
In my head I imagine that something like this would be really useful:
class ComplexBooksController extends SimpleBooksController
So that routes that are not explicitly handled by the child class fall back to the parent class.
Now let's say each book knows if it is complex or not: $book->isComplex
And what I would like to do would be something like this
if (!$book->isComplex) {
// route 'books/{id}' to SimpleBooksController#doSimpleStuff
} else {
// route 'books/{id}' to ComplexBooksController#doComplexStuff
}
So, how could one accomplish this?
* edit *
The way I am currently handling this, is by setting the Controllers in routes.php statically, but let them listen to parameterized routes:
Route::get('books/simple/{id}', 'SimpleBooksController#doSimpleStuff');
Route::get('books/complex/{id}', 'ComplexBooksController#doComplexStuff');
According to the responses documentation, you should be able to use an anonymous function and return a redirect to the correct controller.
This assumes you have the data in a books table with isComplex:
Route::get('books/{id}', function($id) {
$result = DB::select('SELECT is_complex FROM books WHERE id = ?', [$id]);
if (empty($result)) abort(404);
$book = $result[0];
if ($book->is_complex)
return redirect()->action('SimpleBooksController#doSimpleStuff');
else
return redirect()->action('ComplexBooksController#doComplexStuff');
});
Your approaching this in the wrong way. You should not be trying to handle it with different controllers and routes.
A route for books should be the same - regardless of the book.
Your controller then makes a determination if the book is simple or complex - and calls the appropriate Command to handle this.
Route:
Route::get('books/{id}','BooksController#doSimpleStuff');
Controller:
class BooksController extends Controller
{
public function showBook($book)
{
if ($book->isComplex()) {
$this->dispatch(new HandleComplexBook($book));
} else {
$this->dispatch(new HandleSimpleBook($book));
}
}
}
Then you just have two commands - one for simple books, and one for complex books
class HandleComplexBook extends Command implements SelfHandling {
protected $book;
public function __construct(Book $book)
{
$this->book = $book;
}
public function handle()
{
// Handle the logic for a complex book here
}
}
class HandleSimpleBook extends Command implements SelfHandling {
protected $book;
public function __construct(Book $book)
{
$this->book = $book;
}
public function handle()
{
// Handle the logic for a simple book here
}
}
Usually to eager load a relationship I would do something like this:
Model::with('foo', 'bar', 'baz')...
A solution might be to set $with = ['foo','bar','baz'] however that will always load these three relations whenever I call Model
Is it possible to do something like this: Model::with('*')?
No it's not, at least not without some additional work, because your model doesn't know which relations it supports until they are actually loaded.
I had this problem in one of my own Laravel packages. There is no way to get a list of the relations of a model with Laravel. It's pretty obvious though if you look at how they are defined. Simple functions which return a Relation object. You can't even get the return type of a function with php's reflection classes, so there is no way to distinguish between a relation function and any other function.
What you can do to make it easier is defining a function that adds all the relationships.
To do this you can use eloquents query scopes (Thanks to Jarek Tkaczyk for mentioning it in the comments).
public function scopeWithAll($query)
{
$query->with('foo', 'bar', 'baz');
}
Using scopes instead of static functions allows you to not only use your function directly on the model but for example also when chaining query builder methods like where in any order:
Model::where('something', 'Lorem ipsum dolor')->withAll()->where('somethingelse', '>', 10)->get();
Alternatives to get supported relations
Although Laravel does not support something like that out of the box you can allways add it yourself.
Annotations
I used annotations to determine if a function is a relation or not in my package mentioned above. Annotations are not officially part of php but a lot of people use doc blocks to simulate them.
Laravel 5 is going to use annotations in its route definitions too so I figuered it not to be bad practice in this case. The advantage is, that you don't need to maintain a seperate list of supported relations.
Add an annotation to each of your relations:
/**
* #Relation
*/
public function foo()
{
return $this->belongsTo('Foo');
}
And write a function that parses the doc blocks of all methods in the model and returns the name. You can do this in a model or in a parent class:
public static function getSupportedRelations()
{
$relations = [];
$reflextionClass = new ReflectionClass(get_called_class());
foreach($reflextionClass->getMethods() as $method)
{
$doc = $method->getDocComment();
if($doc && strpos($doc, '#Relation') !== false)
{
$relations[] = $method->getName();
}
}
return $relations;
}
And then just use them in your withAll function:
public function scopeWithAll($query)
{
$query->with($this->getSupportedRelations());
}
Some like annotations in php and some don't. I like it for this simple use case.
Array of supported relations
You can also maintain an array of all the supported relations. This however needs you to always sync it with the available relations which, especially if there are multiple developers involved, is not allways that easy.
protected $supportedRelations = ['foo','bar', 'baz'];
And then just use them in your withAll function:
public function scopeWithAll($query)
{
return $query->with($this->supportedRelations);
}
You can of course also override with like lukasgeiter mentioned in his answer. This seems cleaner than using withAll. If you use annotations or a config array however is a matter of opinion.
There's no way to know what all the relations are without specifying them yourself. How the other answers posted are good, but I wanted to add a few things.
Base Model
I kind of have the feeling that you want to do this in multiple models, so at first I'd create a BaseModel if you haven't already.
class BaseModel extends Eloquent {
public $allRelations = array();
}
"Config" array
Instead of hard coding the relationships into a method I suggest you use a member variable. As you can see above I already added $allRelations. Be aware that you can't name it $relations since Laravel already uses that internally.
Override with()
Since you wanted with(*) you can do that too. Add this to the BaseModel
public static function with($relations){
$instance = new static;
if($relations == '*'){
$relations = $instance->allRelations;
}
else if(is_string($relations)){
$relations = func_get_args();
}
return $instance->newQuery()->with($relations);
}
(By the way, some parts of this function come from the original Model class)
Usage
class MyModel extends BaseModel {
public $allRelations = array('foo', 'bar');
}
MyModel::with('*')->get();
I wouldn't use static methods like suggested since... it's Eloquent ;)
Just leverage what it already offers - a scope.
Of course it won't do it for you (the main question), however this is definitely the way to go:
// SomeModel
public function scopeWithAll($query)
{
$query->with([ ... all relations here ... ]);
// or store them in protected variable - whatever you prefer
// the latter would be the way if you want to have the method
// in your BaseModel. Then simply define it as [] there and use:
// $query->with($this->allRelations);
}
This way you're free to use this as you like:
// static-like
SomeModel::withAll()->get();
// dynamically on the eloquent Builder
SomeModel::query()->withAll()->get();
SomeModel::where('something', 'some value')->withAll()->get();
Also, in fact you can let Eloquent do it for you, just like Doctrine does - using doctrine/annotations and DocBlocks. You could do something like this:
// SomeModel
/**
* #Eloquent\Relation
*/
public function someRelation()
{
return $this->hasMany(..);
}
It's a bit too long story to include it here, so learn how it works: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html
Since i've met with a similar problem, and found a good solution that isn't described here and doesn't require filling some custom arrays or whatever, i'll post it for the future.
What i do, is first create a trait, called RelationsManager:
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class
];
public static function getAllRelations($type = null) : array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string)$method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string)$method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll() : Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Project extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$projects = Project::withAll()->get();
Some notes - my example relation classes list doesn't include morph relations, so if you want to get them as well - you need to add them to $relationClasses variable. Also, this solution only works with PHP 7.
You could attempt to detect the methods specific to your model using reflection, such as:
$base_methods = get_class_methods('Illuminate\Database\Eloquent\Model');
$model_methods = get_class_methods(get_class($entry));
$maybe_relations = array_diff($model_methods, $base_methods);
dd($maybe_relations);
Then attempt to load each in a well-controlled try/catch. The Model class of Laravel has a load and a loadMissing methods for eager loading.
See the api reference.
You can create method in your Model
public static function withAllRelations() {
return static::with('foo', 'bar', 'baz');
}
And call Model::withAllRelations()
Or
$instance->withAllRelations()->first(); // or ->get()
You can't have a dynamic loading of relationships for a certain model. you need to tell the model which relations to support.
composer require adideas/laravel-get-relationship-eloquent-model
https://packagist.org/packages/adideas/laravel-get-relationship-eloquent-model
Laravel get relationship all eloquent models!
You don't need to know the names of the methods in the model to do this. Having one or many Eloquent models, thanks to this package, you can get all of its relationships and their type at runtime
The Best Solution
first create a trait, called RelationsManager:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use ReflectionClass;
use ReflectionMethod;
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class,
HasOneThrough::class,
HasManyThrough::class,
MorphTo::class,
MorphOne::class,
MorphMany::class,
MorphToMany::class,
];
public static function getAllRelations($type = null): array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string) $method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string) $method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll(): Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Company extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$companies = Company::withAll()->get();
this solution only works with PHP 7 Or Higher.
Done
I have this question about Laravel:
I have a my model and my RestfulAPI controller.
Into the store() method I would check if I have an element that already has the field 'myField' (myField id different from 'id') equal to what I have to create. If it already exist then I would like to update, otherwise I would simply create (save())..
Have I to use find() method?
From my experience, you'll have to traverse table and check for uniqueness.
You can create your helper function and use something like array_unique function. Maybe it is worth checking how Validator class is checking that users entry is unique.
Currently we have firstOrCreate or firstOrNew, but I don't think they really fit your needs. For instance, firstOrCreate will try to locate a row by all attributes, not just some, so an update in this case wouldn't make sense. So I think you really would have to find it, but you can create a BaseModel and create a createOrUpdate method that could look like this:
This is untested code
class BaseModel extends Eloquent {
public function createOrUpdate($attributes, $keysToCheck = null)
{
// If no attributes are passed, find using all
$keysToCheck = $keysToCheck ?: $attributes;
if ($model = static::firstByAttributes(array_only($keysToCheck, $attributes))
{
$model->attributes = $attributes;
$model->save();
}
else
{
$model = static::create($attributes);
}
return $model;
}
}
This is an implementation of it:
class Post extends BaseModel {
public function store()
{
$model = $this->createOrUpdate(Input::all(), ['full_name']);
return View::make('post.created', ['model' => $model]);
}
}