I am still used to MVC concept but i understand the basic concept of it.
I found this code on a "PHP" blog.
<?php
class Todo_Controller extends Base_Controller
{
public function action_list() {
$todos = Todo::all();
return View::make("list")
->with("todos", $todos);
}
public function action_view($id) {
$todo = Todo::where_id($id)->first();
return View::make("view")
->with("todo", $todo);
}
public function action_delete($id) {
$todo = Todo::where_id($id)->first();
$todo->delete();
return View::make("deleted");
}
public function action_new() {
return View::make("add");
}
public function action_add() {
$todo = new Todo();
$todo->title = Input::get("title");
$todo->description = Input::get("description");
$todo->save();
return View::make("success");
}
}
That is a controller but I notice action_list(), action_view() and action_delete() are running SQL but it is doing it in a controller.
Why is that? shouldn't that be in the model? Isn't the purpose of a model to do anything data related?
The reason why I am asking this is because I have seen a lot of laravel tutorials both paid and unpaid ones doing this and I am asking myself, why mix the business logic with the data schema?
You can use the repository pattern to extract the data querying from your controller.
class TodoRepository {
public function get_todo($id)
{
return Todo::find($id);
}
public function get_all_todos()
{
return Todo:all();
}
public function create_todo($todo)
{
return Todo::create([
'title' => $todo['title'],
'description' => $todo['description']
]);
}
public function delete_todo($todo)
{
return Todo::find($todo)->delete();
}
}
Then you inject the repository into your controller. That way if you change databases, or ditch eloquent then you just write a new repository with the same interface and you simply change out the injection.
class Todo_Controller extends Base_Controller
{
private $todos;
public function __construct(TodoRepository $todos)
{
$this->todos = $todos;
}
public function action_list() {
return View::make("list")
->with("todos", $this->todos->get_all_todos());
}
public function action_view($id) {
return View::make("view")
->with("todo", $this->todos->get_todo($id));
}
public function action_delete($id) {
$this->todos->delete_todo($id);
return View::make("deleted");
}
public function action_new() {
return View::make("add");
}
public function action_add() {
$todo = $this->todos->create_todo(Input->get('title', 'description');
return View::make("success");
}
}
This was your controller doesn't care how you get_all_todos or delete_todo, it simply asks the repository to get/modify the data then it returns the result.
Related
I Have
Worker -> HasOne-> Document ->morphMany ->FIles
->HasOne-> Medical ->morphMany ->FIles
->HasOne-> Course ->morphMany ->FIles
I want to use hasManyTrough to get files data from worker, I defined
public function medical_detail()
{
return $this->hasOne(MedicalDetail::class);
}
Tried this :
public function document_files()
{
return $this->hasManyThrough(File::class, Document::class);
}
In worker but It expects document_id, how can I use ir for polymirhic relationship?
Also did this
public function document_files()
{
return $this->hasManyThrough(
File::class,
Document::class,
'worker_id',
'filable_id',
'id',
'id'
);
}
THese are my models
File model
protected $fillable = ['file_type', 'file_name', 'fiable_id', 'filable_type'];
public function filable()
{
return $this->morphTo();
}
Worker MOdel
public function course()
{
return $this->hasOne(Course::class);
}
public function document()
{
return $this->hasOne(Document::class);
}
Document model
protected $fillable = ['doc_name', 'worker_id'];
public function worker()
{
return $this->belongsTo(Worker::class);
}
public function files()
{
return $this->morphMany(File::class, 'filable');
}
This is one alternative I added, but gives me No query results for model [App\\Models\\Worker] 1 . SInce files table has $table->morphs('filable'); This defined so I added filable_id. These are my models: docuemnt,file
I'm trying to refactor my code to be more reusable.
I created a trait CrudControllerTrait to implement the index,show,store,update,destroy methods.
But I found 2 problems:
BrandController.php
public function store(BrandNewRequest $request)
{
$requestData = $request->validated();
return new BrandResource($this->brands->store($requestData));
}
ProductController.php
public function store(ProductNewRequest $request)
{
$requestData = $request->validated();
return new ProductResource($this->products->store($requestData));
}
The trait method would be:
public function store(xxxxx $request)
{
$requestData = $request->validated();
return new xxxxxResource($this->repository()->store($requestData));
}
Problem1: The hint type. How can I abstract them? If I remove it shows that errror:
"message": "Too few arguments to function App\\Http\\Controllers\\BrandController::store(), 0 passed and exactly 1 expected"
Problem2: Return the resource. How can create the new resource? On the collection I can solve it doing this:
public function index()
{
$models = $this->repository()->index();
return $this->resource()::collection($models);
}
The resource is on the controller who uses the trait:
public function resource()
{
return BrandResource::class;
}
But with single resource didn't know how to do it...
The idea is, that I have so much controllers using the same pattern: BrandController, ProductController, etc. I'd love to reuse these 5 crud methods on the same trait...
The only way I found is creating an abstract method.
trait CrudRepositoryTrait
{
abstract function model();
public function index()
{
return $this->model()::with($this->with())->get();
}
public function find($id)
{
return $this->model()::findOrFail($id);
}
public function store($data)
{
$request = $this->dtoRequest($data);
return $this->model()::create($request);
}
(...)
}
And then, an example how to use this treat:
class ProductRepository implements ProductRepositoryContract
{
use CrudRepositoryTrait;
function model()
{
return Product::class;
}
(...)
}
By this way I could reuse a lot of code.
I have three models. I want to avoid that users can change the todo's from todolists belonging to other users.
class User extends Authenticatable
{
public function todolists()
{
return $this->hasMany('App\Todolist');
}
public function todos()
{
return $this->hasManyThrough('App\Todo', 'App\Todolist');
}
}
class Todolist extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
public function todos()
{
return $this->hasMany('App\Todo');
}
}
class Todo extends Model
{
protected $casts = [
'completed' => 'boolean',
];
public function todolist()
{
return $this->belongsTo('App\Todolist');
}
}
To avoid users can view other users' todolists and todo items, I have implemented the following:
public function getTodosForTodolist(Todolist $todolist)
{
if (Auth::user()->id == $todolist->user_id) {
$todos = Todo::where('todolist_id', $todolist->id )->get();
return view('todo/index', ['todos' => $todos);
}
else {
abort(403, 'Unauthorized action.');
}
}
Next step is to prevent that users can edit other users' todo items. Currently in the TodoController I have simply the following:
public function edit(Todo $todo)
{
if (Auth::user()->todos->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
}
This gives the following error:
Property [id] does not exist on this collection instance.
The error is because the current user has multiple todos. So I changed my code as follows.
public function edit(Todo $todo)
{
if (Auth::user()->todos->first()->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
abort('403', 'Unauthorized action.');
}
This works but it just feels very wrong to do this as such.
What would be a better way to accomplish that users' can view/edit/delete items belonging to other users?
I suggest that you use policies for your Todo and TodoList models and a scope to restrict todos to one user to prevent duplicated code within your app:
class ToDoListPolicy
{
public function view(User $user, TodoList $post)
{
return $user->id === $todolist->user_id;
}
}
class ToDoPolicy
{
public function edit(User $user, Todo $toDo)
{
$toDo->loadMissing('todolist');
return $user->id === $toDo->todolist->user_id;
}
}
Register them in your AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
TodoList::class => ToDoListPolicy::class,
Todo::class => ToDoPolicy::class
];
}
and then use them in your actions:
public function getTodosForTodolist(Todolist $toDoList)
{
$this->authorize('view', $toDoList);
$toDoList->loadMissing('todos');
return view('todo.index', ['todos' => $toDoList->todos);
}
class ToDoController extends Controller
{
public function edit(Todo $toDo)
{
$this->authorize('edit', $toDo);
return view('todo.edit', compact('toDo'));
}
}
And a scope to restrict the query to a specific user:
class Todo extends Model {
// ...
public function scopeByUser(Builder $query, ?User $user = null)
{
if (! $user) {
$user = Auth::user();
}
$query->whereHas('todolist', function (Builder $toDoListQuery) use ($user) {
$toDoListQuery->where('user_id', $user->id);
});
}
}
Answer to your questions in the comments.
Q1: I had to put Auth::user()->can('view', $todolist); in an if-else clause for it to work. Guess this is the way it works?
Q2: what is the difference between $this->authorize('edit', $todo) and Auth::user()->can('edit', $todo)?
Sorry, that was a mistake on my side. Auth::user()->can() returns a boolean whereas $this->authorize() (which is a method of the AuthorizesRequests trait usually included in the BaseController) throws an exception if the authorization failed.
If you want to let each user work only with his/her own Todos then adding a Global Scope is what you are looking for. This implementation will let your application feel that Todos ( of users other than the logged one ) does not exist.
Global Scopes can be used for many models which means it will reduce boiler plate code.
https://laravel.com/docs/7.x/eloquent#global-scopes
I would like to get the model relations, in array;
My model look like:
class User extends Model
{
public function profile() {
return $this->haOne(Profile::class);
}
public function settings() {
return $this->morphOne(Settings::class, 'settingsable');
}
public function addresses() {
return $this->hasMany(Addresses::class);
}
}
And my code:
$user = User::with(['profile', 'settings', 'addresses'])->find(1);
$user->getRelations(); // return ['profile', 'settings', 'addresses'];
If I have more then 10 relation, I don't want to list all.
I would like to get like this:
$relations = ['profile', 'settings', 'addresses'];
Is this posible?
You could try adding a scope to the model, and so, you have to only write them once.
class User extends Model
{
public function profile() {
return $this->haOne(Profile::class);
}
public function settings() {
return $this->morphOne(Settings::class, 'settingsable');
}
public function addresses() {
return $this->hasMany(Addresses::class);
}
public function scopeWithRelations(Builder $query){
return $query->with([...]);
}
}
$users = User::withRelations()->get();
This way you only have to write them once there, and everywhere in the code you'll use the scope.
Not exactly 100% what you're asking, but this could be a solution.
Below is the one of the model. I would like to delete a Telco entry only if no other model is referencing it? What is the best method?
namespace App;
use Illuminate\Database\Eloquent\Model;
class Telco extends Model
{
public function operators()
{
return $this->hasMany('App\Operator');
}
public function packages()
{
return $this->hasMany('App\Package');
}
public function topups()
{
return $this->hasMany('App\Topup');
}
public function users()
{
return $this->morphMany('App\User', 'owner');
}
public function subscribers()
{
return $this->hasManyThrough('App\Subscriber', 'App\Operator');
}
}
You can use deleting model event and check if there any related records before deletion and prevent deletion if any exists.
In your Telco model
protected static function boot()
{
parent::boot();
static::deleting(function($telco) {
$relationMethods = ['operators', 'packages', 'topups', 'users'];
foreach ($relationMethods as $relationMethod) {
if ($telco->$relationMethod()->count() > 0) {
return false;
}
}
});
}
$relationships = array('operators', 'packages', 'topups', 'users', 'subscribers');
$telco = Telco::find($id);
$should_delete = true;
foreach($relationships as $r) {
if ($telco->$r->isNotEmpty()) {
$should_delete = false;
break;
}
}
if ($should_delete == true) {
$telco->delete();
}
Well, I know this is ugly, but I think it should work. If you prefer to un-ugly this, just call every relationship attributes and check whether it returns an empty collection (meaning there is no relationship)
If all relationships are empty, then delete!
After seeing the answers here, I don't feel copy pasting the static function boot to every models that need it. So I make a trait called SecureDelete. I put #chanafdo's foreach, inside a public function in SecureDelete.
This way, I can reuse it to models that need it.
SecureDelete.php
trait SecureDelete
{
/**
* Delete only when there is no reference to other models.
*
* #param array $relations
* #return response
*/
public function secureDelete(String ...$relations)
{
$hasRelation = false;
foreach ($relations as $relation) {
if ($this->$relation()->withTrashed()->count()) {
$hasRelation = true;
break;
}
}
if ($hasRelation) {
$this->delete();
} else {
$this->forceDelete();
}
}
}
Add use SecureDelete to the model that needs it.
use Illuminate\Database\Eloquent\Model;
use App\Traits\SecureDelete;
class Telco extends Model
{
use SecureDelete;
public function operators()
{
return $this->hasMany('App\Operator');
}
// other eloquent relationships (packages, topups, etc)
}
TelcoController.php
public function destroy(Telco $telco)
{
return $telco->secureDelete('operators', 'packages', 'topups');
}
In addition, instead of Trait, you can also make a custom model e.g BaseModel.php that extends Illuminate\Database\Eloquent\Model, put the function secureDelete there, and change your models to extends BaseModel.