Laravel polymorphic relations: Passing model to controller - php

I want to use a single controller to save my comments for multiple models. So I created the CommentController, with the following store method:
public function store(Teacher $teacher, Request $request)
{
$input = $request->all();
$comment = new Comment();
$comment->user_id = Auth::user()->id;
$comment->body = $input['body'];
$teacher->comments()->save($comment);
return redirect()->back();
}
In my view, I have:
{!! Form::open([
'route' => ['teachers.comments.store', $teacher->id]
]) !!}
This is working. If I want to use the same CommentController to store the comments for a school, how should I modify the store method of the controller?

Adam's solution is great, but I would not hard-code the model's namespace that way. Instead, what I would do is make use of Laravel's Relation::morphMap(), you can check it out here: https://laravel.com/docs/5.6/eloquent-relationships#polymorphic-relations
That way, you will also make your database entries more readable. I recommend using a service provider to map the morphs.
Also, the Model base class has a getMorphClass() method, so instead of
$comment->commentable_type = 'App\\Models\\'.$model;
I would use
$comment->commentable_type = $model->getMorphClass();
That way you integrate Laravel's logic into your code.

Im not sure if this is the Laravel convension, but i have done the following:
Made a route:
Route::post('/Comment/{model}/{id}', [
// etc
]);
Then in the controller get the model and check against an array of allowed models, pass the id through and attach:
public function store(Request $request, $model, $id) {
$allowed = ['']; // list all models here
if(!in_array($model, $allowed) {
// return redirect back with error
}
$comment = new Comment();
$comment->user_id = $request->user()->id;
$comment->commentable_type = 'App\\Models\\'.$model;
$comment->commentable_id = $id;
$comment->body = $request->body;
$comment->save();
return redirect()->back();
}
Like I say, there is most likely a much better way to accomplish, but this is how I've done it. It keeps it short and sweet and checks if the model can take a comment.

I implemented this way if you want, according to me it's the one of the bests way to do that.
// Route::post('/comments/{model}/{id}', 'CommentController#store');
class CommentController extends Controller {
protected $model;
public function __construct()
{
$this->model = Relation::getMorphedModel(
request()->route()->parameter('model')
);
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
dd($this->model); // return 'App\Post' or null
}
}

Related

Class name problem on laravel 5.8 with Request->input

I'm trying to follow a tutorial to create a simple web scraper here using Laravel, but symfony threw a "Class name must be a valid object or string" error on line 49. In phpstorm it did gave me a light warning of field accessed with magic method on $website->title
I've tried to declare $title as a public var in my App/Website.php but it still gave me this error.
here's the snippet of the code with error
public function store(Request $request)
{
$this->validate($request,[
'title'=>'required',
'url'=>'required',
'logo'=>'required'
]);
$website = new Website();
$website->title = new $request->input('title');
$website->url = $request->input('url');
$website->logo = $this->uploadFile('logo', public_path('uploads/'), $request)["filename"];
$website->save();
return redirect()->route('websites.index');
}
/**
* Display the specified resource.
*
and here's my App/Website Class:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Website extends Model
{
protected $table = "website";
public $title;
/**
* #var array|string|null
*/
public $url;
public $logo;
}
It should've saved the title, url and logo to a sql db i named scraper but it keeps throwing this error. Please help.
Edit 2: I apologize it seems i copied the code shown by symfony, my actual WebsiteController is like this copied wrong code again, here's the actual actual code:
public function store(Request $request)
{
$this->validate($request,[
'title'=>'required',
'url'=>'required',
'logo'=>'required'
]);
$website = new Website();
$website->title = new $request->input('title');
$website->url = $request->input('url');
$website->logo = $this->uploadFile('logo', public_path('uploads/'), $request)["filename"];
$website->save();
return redirect()->route('websites.index');
}
Here new should not be there ,$website->title = new $request->input('title'); , You are not making object from any class. i guess you miss type.
and you can still get your data by just $request->title; i prefer this because it short your code and still readable.
Do it like below
$website->title = $request->input('title');
Or
$website->title = $request->title;
and also for saving data into db you don't need to declare variable.
simply in model add protected $fillable=['title','url','logo'];
or if you using save() method you don't even need to add $fillable

Laravel 5 Global CRUD Class

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.

In Laravel why am I getting a No query results for model [TodoList]?

I'm having problems with my routes in laravel when I go to "/todos and /todos/id " everything works perfectly but when I try using the "/todos/create" I get a No query results for model [TodoList]
I'm new to this please help me... i really dont want to give up because i really love this mvc
here's my routes
Route::get('/', 'TodoListController#index');
Route::get('todos', 'TodoListController#index');
Route::get('/todos/{id}', 'TodoListController#show');
Route::get('db', function() {
$result = DB::table('todo_lists')->where('name', 'Your List')->first();
return $result->name;
});
Route::resource('todos', 'TodoListController');
model
<?php
class TodoList extends Eloquent {}
Controller
public function index()
{
$todo_lists = TodoList::all();
return View::make('todos.index')->with('todo_lists', $todo_lists);
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
$list = new TodoList();
$list->name = "another list";
$list->save();
return "I am here by accident";
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store()
{
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
$list = TodoList::findOrFail($id);
return View::make('todos.show')->withList($list);
}
my views
#extends('layouts.main') #section('content')
<h1>All todo list</h1>
<ul>
#foreach ($todo_lists as $list)
<li>{{{ $list->name }}}</li>
#endforeach
</ul>
#stop
The issue is with the explicit route you defined for /todos/{id}. Since this route is defined before the resource route, it is catching the route for todos/create, and treating the text create as the {id} parameter for the show method.
Delete the explicit get routes for todos and /todos/create and your issue will be fixed. Both of these routes are handled by the resource route.
Your model doesn't have the fillable attribute. Try adding
protected $fillable = [
'name'
];
You also need to define a table
protected $table = 'todolist';
Extra
You're also manually adding some routes that are already defined by your resource controller like your show route.
You are also storing models in your create method. You should be showing a form in your create method and storing results in your store method.

Saving from eloquent constructor in laravel

So, I'm currently working on a browser game in Laravel. So far I love the framework, but I haven't really got much experience, and I just can't get this to work.
Basically I'm trying to update all users whenever they are instantieted, as there is no reason update them when they are not used. But calling this function from the constructor doesn't update the user, it only works when I call the function outside the constructor.
Have I missed anything, or is it just not possible?
Thanks in advance!
<?php
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password', 'remember_token');
public function __construct($arguments = array())
{
parent::__construct($arguments);
$this->updateHp();
}
public function updateHp()
{
$this->hp_last = time();
$this->save();
}
}
Eloquent is a static class, data is fetched on query (find, first, get) and when you create a model you have just a blank model, with no data on it. This is, as example, the point where you have some data available:
public static function find($id, $columns = array('*'))
{
if (is_array($id) && empty($id)) return new Collection;
$instance = new static;
return $instance->newQuery()->find($id, $columns);
}
Before one of those query methods, you have void.
So you probably cannot do that during __construct because your model is still blank (all nulls). This is what you can do to make it, somehow, automatic:
First, during boot, create some creating and updating listeners:
public static function boot()
{
static::creating(function($user)
{
$user->updateHp($user);
});
static::updating(function($user)
{
$user->updateHp($user);
});
parent::boot();
}
public function updateHp()
{
$this->hp_last = time();
$this->save();
}
Then, every time you save() a model it will, before saving, fire your method:
$user = User::where('email', 'acr#antoniocarlosribeiro.com')->first();
$user->activation_code = Uuid::uuid4();
$user->save();
If you want to make it somehow automatic for all your users. You can hook it to a login event. Add this code to your global.php file:
Event::listen('user.logged.in', function($user)
{
$user->updateHp();
})
Then in your login method you'll have to:
if ($user = Auth::attempt($credentials))
{
Event::fire('user.logged.in', array($user));
}
In my opinion you shouldn't do that. If you use the code:
$user = new User();
you would like to be run:
$this->hp_last = time();
$this->save();
and what exactly should happen in this case? New user without id should be created with property hp_last ?
I think that's not the best idea.
You should leave it in the function then you can use:
$user = new User();
$user->find(1);
$user->updateHp();
That makes much more sense for me.

Laravel & Mockery: Unit testing the update method without hitting the database

Alright so I'm pretty new to both unit testing, mockery and laravel. I'm trying to unit test my resource controller, but I'm stuck at the update function. Not sure if I'm doing something wrong or just thinking wrong.
Here's my controller:
class BooksController extends \BaseController {
// Change template.
protected $books;
public function __construct(Book $books)
{
$this->books = $books;
}
/**
* Store a newly created book in storage.
*
* #return Response
*/
public function store()
{
$data = Input::except(array('_token'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
return Redirect::route('books.create')
->withErrors($validator->errors())
->withInput();
}
$this->books->create($data);
return Redirect::route('books.index');
}
/**
* Update the specified book in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
$book = $this->books->findOrFail($id);
$data = Input::except(array('_token', '_method'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
// Change template.
return Redirect::route('books.edit', $id)->withErrors($validator->errors())->withInput();
}
$book->update($data);
return Redirect::route('books.show', $id);
}
}
And here are my tests:
public function testStore()
{
// Add title to Input to pass validation.
Input::replace(array('title' => 'asd', 'content' => ''));
// Use the mock object to avoid database hitting.
$this->mock
->shouldReceive('create')
->once()
->andReturn('truthy');
// Pass along input to the store function.
$this->action('POST', 'books.store', null, Input::all());
$this->assertRedirectedTo('books');
}
public function testUpdate()
{
Input::replace(array('title' => 'Test', 'content' => 'new content'));
$this->mock->shouldReceive('findOrFail')->once()->andReturn(new Book());
$this->mock->shouldReceive('update')->once()->andReturn('truthy');
$this->action('PUT', 'books.update', 1, Input::all());
$this->assertRedirectedTo('books/1');
}
The issue is, when I do it like this, I get Mockery\Exception\InvalidCountException: Method update() from Mockery_0_Book should be called exactly 1 times but called 0 times. because of the $book->update($data) in my controller. If I were to change it to $this->books->update($data), it would be mocked properly and the database wouldn't be touched, but it would update all my records when using the function from frontend.
I guess I simply just want to know how to mock the $book-object properly.
Am I clear enough? Let me know otherwise. Thanks!
Try mocking out the findOrFail method not to return a new Book, but to return a mock object instead that has an update method on it.
$mockBook = Mockery::mock('Book[update]');
$mockBook->shouldReceive('update')->once();
$this->mock->shouldReceive('findOrFail')->once()->andReturn($mockBook);
If your database is a managed dependency and you use mock in your test it causes brittle tests.
Don't mock manage dependencies.
Manage dependencies: dependencies that you have full control over.

Categories