I´m building my API (makes and models) and I want to have nested resources (not sure if this is correct Restfully speaking)
/makes/ferrari/models
/makes/ferrari/models/f40
I defined the following route
Route::resource('makes.models', 'ModelsController');
and the ModelsController.php
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$data = Models::all();
return response()->json($data);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$data = Models::find($id);
return response()->json($data);
}
and the models model (yeah I need to change the name)
class Models extends Model
{
public function make()
{
return $this->belongsTo('App\Make');
}
}
my problem is that even if the route works it returns all models in the db (not only ferraris) where should I define that relationship? is not automatic?
I have 2 tables makes (id, name), models (id, name, make_id)
thank you!
The resource route will define the following routes:
Method Path Action
GET /makes/{make}/models index
GET /makes/{make}/models/create create
POST /makes/{make}/models store
GET /makes/{make}/models/{id} show
GET /makes/{make}/models/{id}/edit edit
PUT /makes/{make}/models/{id} update
DELETE /makes/{make}/models/{id} destroy
Your request /makes/ferrari/models will not match any of those routes (as your show parameter only takes one parameter). You may request for /makes/models/1 to call show, but you are practically missing the route for this, as the nested route does not provide it.
If you say that you always get all items, you are very likely hitting the index action instead of show.
If you want to query your models with /makes/ferrari/models/f40, you would need a route like this:
Route::get('/makes/{make}/model/{model}', 'ModelsController#show');
Which is already part of the resource route created for you.
Now, in your show controller, use the make and model parameters to find the correct dataset:
public function show($make, $model)
{
$data = Model::with('makes')
->whereName($model)
->whereHas('makes', function ($query) use ($make) {
$query->where('name', '=', $make);
})->get();
return response()->json($data);
}
Laravel doesn't automatically do that for you.
Update: Route model binding
You might want to check out https://laravel.com/docs/5.3/routing#route-model-binding for a more sophisticated way of doing this. You can set your route key name in both of your models overwriting the getRouteKeyName() method and returning 'name' in this case, telling Laravel to use the name column instead of the id.
You can also bind parameters in your routes specifically to a custom resolution logic by doing something like
$router->bind('model', function ($value) {
return Model::where('name', $value)->first();
});
and then every time you use {model} in your routes, it will use the name instead of the id.
Use slugs
However, be advised that you have to make absolutely sure that the names stored in the database for model and make are sluggified so that they are suited for use in URLs. If necessary, you may possibly do that in your bind as shown above, returning
return str_slug(Model::where('name', $value)->first());
This is untested, however, so it might or might not work.
Hope that helps :-)
When using nested resources, all your controller actions will receive an additional first parameter (the parent resource identifier). So you need to update your controller actions accordingly:
public index($make) {
$make = Make::with('models')->where('name', $make)->firstOrFail();
return view('models.index', compact('make'));
}
public show($make, $model) {
$make = Make::with('models')
->where('name', $make)
->firstOrFail();
$model = $make->models()
->where('name', $make)
->firstOrFail();
return view('models.show', compact('make', 'model'));
}
It should be the same with your other controller actions.
Note that I made assumptions regarding the structure of your database.
Related
I have a Location Model, which contains two properties: ID and Name.
To edit this Model, I have set up this route:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following
Gate::before(function ($user, $permission) {
if ($user->permissions->pluck('name')->contains($permission)) {
return true;
}
});
Where permission is a Model that contains an ID and a name, mapped via a permission_user table.
I have these permissions set up:
edit_los_angeles
edit_new_york
edit_boston
plenty_of_other_permissions_not_related_to_location
After all this rambling, my actual question:
How can I tie these permissions to the edit the location?
The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').
Instead, I would need something like this, I guess:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_boston');
...obviously this would not work.
What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?
Thank you very much for your help in advance!
I am using Laravel 6.0 :-)
Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.
Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.
class LocationController {
public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
...
}
}
For ease of use, i would also make a policy.
class LocationPolicy
{
public function edit(User $user, Location $location) {
return $user->permissions->pluck('name')
->contains($location->permission_slug); // assuming we have a binding
}
}
Remember to register policy in the AuthServiceProvider.php.
protected $policies = [
Location::class => LocationPolicy::class,
];
Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.
class EditLocationRequest
{
public function authorize(): bool
{
return $this->user()->can('edit', $this->location);
}
}
Now you should be able to only have a single route definition.
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
EDIT
Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.
use AuthorizesRequests;
public function edit() {
$this->authorize('edit', $location);
}
If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.
Edit: to answer the question about implementation of middleware,
The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.
I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of
public function canEditLocation(Location $location): bool {
return $this->locations
->where('location_id', '=', $location->id)
->count() > 0;
}
And the actual middleware something along these lines:
public function handle($request, Closure $next, $location)
{
if (! $request->user()->canEditLocation($location)) {
\\handle failed permission as appropriate here.
}
return $next($request);
}
My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters
I'm migrating a Laravel 5.7 app to Lumen, and introducing at the same time Laravel API Resources
In my old codebase, I had:
$tournaments = Auth::user()->tournaments();
With
public function tournaments()
{
return $this->hasMany('App\Tournament');
}
But now, in Lumen, I use API Resources, so don't know how to get the same result, but with all the decorated extra fields that provide API resource.
I have:
class TournamentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'user' => User::findOrFail(Auth::user()->id)->email,
'championships' => ChampionshipResource::collection($this->whenLoaded('championships')),
'competitors_count' => $this->competitors->count()
];
}
}
Any Idea?
API Resources just format the way the data is returned. It doens't affect your relationships. The only thing you need to do is pass an object/collection (depending on the case) to the API Resource class.
Resource Collections
If you are returning a collection of resources or a paginated response, you may use the collection method when
creating the resource instance in your route or controller:
use App\User;
use App\Http\Resources\User as UserResource;
Route::get('/user', function () {
return UserResource::collection(User::all());
});
As you can see, just use it:
TournamentsController.php
use App\Http\Resources\TournamentResource;
//
public function index()
{
$tournaments = auth()->user()->tournaments;
return TournamentResource::collection($tournaments);
}
Check the documentation regarding this aspect. Also, to load the child items (championship), you can Eager Load/Lazy Eager Load the relationship items.
Observation:
In relationships, when you use it like a method (auth()->user()->tournaments()) you are accesing the relationship itself, usefull when you want to keep constraining the relation. When you use it as an attribute (auth()->user->tournaments) you are accesing the results of the query.
Check this answer for a better explanation.
If you migrating from Laravel to Lumen first thing you need to make sure that you have enable the eloquent in your app/bootstrap.php file.
Please follow this guide to make sure you are following the same. The above code should work once these are followed.
Two of my tables (clients and products) have a ManyToMany relation using Laravel's blongToMany and a pivot table.
Now I want to check if a certain client has a certain product.
I could create a model to check in the pivot table but since Laravel does not require this model for the belongsToMany method I was wondering if there is another way to check if a certain relationship exists without having a model for the pivot table.
I think the official way to do this is to do:
$client = Client::find(1);
$exists = $client->products->contains($product_id);
It's somewhat wasteful in that it'll do the SELECT query, get all results into a Collection and then finally do a foreach over the Collection to find a model with the ID you pass in. However, it doesn't require modelling the pivot table.
If you don't like the wastefulness of that, you could do it yourself in SQL/Query Builder, which also wouldn't require modelling the table (nor would it require getting the Client model if you don't already have it for other purposes:
$exists = DB::table('client_product')
->whereClientId($client_id)
->whereProductId($product_id)
->count() > 0;
The question is quite old but this may help others looking for a solution:
$client = Client::find(1);
$exists = $client->products()->where('products.id', $productId)->exists();
No "wastefulness" as in #alexrussell's solution and the query is more efficient, too.
Alex's solution is working one, but it will load a Client model and all related Product models from DB into memory and only after that, it will check if the relationship exists.
A better Eloquent way to do that is to use whereHas() method.
1. You don't need to load client model, you can just use his ID.
2. You also don't need to load all products related to that client into memory, like Alex does.
3. One SQL query to DB.
$doesClientHaveProduct = Product::where('id', $productId)
->whereHas('clients', function($q) use($clientId) {
$q->where('id', $clientId);
})
->count();
Update: I did not take into account the usefulness of checking multiple relations, if that is the case then #deczo has a way better answer to this question. Running only one query to check for all relations is the desired solution.
/**
* Determine if a Client has a specific Product
* #param $clientId
* #param $productId
* #return bool
*/
public function clientHasProduct($clientId, $productId)
{
return ! is_null(
DB::table('client_product')
->where('client_id', $clientId)
->where('product_id', $productId)
->first()
);
}
You could put this in you User/Client model or you could have it in a ClientRepository and use that wherever you need it.
if ($this->clientRepository->clientHasProduct($clientId, $productId)
{
return 'Awesome';
}
But if you already have defined the belongsToMany relationship on a Client Eloquent model, you could do this, inside your Client model, instead:
return ! is_null(
$this->products()
->where('product_id', $productId)
->first()
);
#nielsiano's methods will work, but they will query DB for every user/product pair, which is a waste in my opinion.
If you don't want to load all the related models' data, then this is what I would do for a single user:
// User model
protected $productIds = null;
public function getProductsIdsAttribute()
{
if (is_null($this->productsIds) $this->loadProductsIds();
return $this->productsIds;
}
public function loadProductsIds()
{
$this->productsIds = DB::table($this->products()->getTable())
->where($this->products()->getForeignKey(), $this->getKey())
->lists($this->products()->getOtherKey());
return $this;
}
public function hasProduct($id)
{
return in_array($id, $this->productsIds);
}
Then you can simply do this:
$user = User::first();
$user->hasProduct($someId); // true / false
// or
Auth::user()->hasProduct($someId);
Only 1 query is executed, then you work with the array.
The easiest way would be using contains like #alexrussell suggested.
I think this is a matter of preference, so unless your app is quite big and requires a lot of optimization, you can choose what you find easier to work with.
Hello all) My solution for this problem: i created a own class, extended from Eloquent, and extend all my models from it. In this class i written this simple function:
function have($relation_name, $id) {
return (bool) $this->$relation_name()->where('id','=',$id)->count();
}
For make a check existing relation you must write something like:
if ($user->have('subscribes', 15)) {
// do some things
}
This way generates only a SELECT count(...) query without receiving real data from tables.
To check the existence of a relationship between 2 models, all we need is a single query against the pivot table without any joins.
You can achieve it using the built-in newPivotStatementForId method:
$exists = $client->products()->newPivotStatementForId($product->id)->exists();
use trait:
trait hasPivotTrait
{
public function hasPivot($relation, $model)
{
return (bool) $this->{$relation}()->wherePivot($model->getForeignKey(), $model->{$model->getKeyName()})->count();
}
}
.
if ($user->hasPivot('tags', $tag)){
// do some things...
}
This has time but maybe I can help someone
if($client->products()->find($product->id)){
exists!!
}
It should be noted that you must have the product and customer model, I hope it helps,
In my laravel app I have multiple user accounts who have resources that are assigned to them. Say, for example, a "payment". To edit or view a payment a user would visit the /payment/edit/{payment} route (where payment is the payment ID).
Although I have an auth filter to stop un-logged in users from accessing this page there is nothing to stop, for example, user 1 from editing user 2's payment.
Is there a filter I can user that checks which user a payment (or any other resource) belongs to prevent this kind of issue?
[I am using Laravel's model bindings which automatically fetches the model specified by the route rather than me get it in the controller using eloquent.]
No such filter exists by default, however you can easily create one (depending on how your database is set up). Within app/filters.php, you may do something like this:
Route::filter('restrictPermission', function($route)
{
$payment_id = $route->parameter('payment');
if (!Auth::user()->payments()->find($payment_id)) return Redirect::to('/');
});
This compares the currently logged in user's payment_id (in your database) to the {payment} argument passed into the route. Obviously, depending on how your database is set up (for instance if the payment_id is in a separate table) you need to change the conditional.
Then, apply the filter to your route:
Route::get('/payment/edit/{payment}', array('before' => 'restrictPermission'));
One way is to place a where statement in every relevant query. Although not very pretty, it works.
$payment = Payment::where('user_id', '=', Auth::user()->id)->find($id);
It's also possible to use url filters like seeARMS is suggesting, however I think it's not very elegant. The most logical place to nest such logic is in the model itself. One possibility is to use model events, but this gives you only the option to intercept update, insert or delete statements, not selects. This might change in the future. Maybe you could use boot() event, but I'm not sure if this is gonna work.
Last but not least you could use query scopes.
class Payment extends Eloquent {
public function scopeAuthuser($query)
{
return $query->where('user_id', '=', Auth::user()->id);
}
}
and in the queries you attach the scope
Payment::authuser()->find($id);
You could do this on a base Model and extend from it, so you have that method in all your relevant models.
Consider using Laravel Policies:
https://laravel.com/docs/6.x/authorization#policy-methods
<?php
namespace App\Policies;
use App\Post;
use App\User;
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* #param \App\User $user
* #param \App\Post $post
* #return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
By policies you can control if given record could be edited by logged user or not.
Cheers!
I understand that you can bind Models to route parameters in Laravel, but is there a way they can be bound to optional query string parameters.
For example, given a URL of:
http://myapi.example.com/products?category_id=345
That uses a route of:
Route::resource('products', 'ProductController');
Is there a way to bind our optional query string parameter category_id to our Category model, so that it can be automatically injected into the ProductController?
At this moment, I don't think that is possible with a resource route because it automatically maps some RESTful routes to a given resource (as per the docs). If you want a route with optional parameters, you will have to use some of the other options for writing routes in Laravel and make sure you place it before declaring the resource controller.
This is possible, but not as your question asks.
Is there a way to bind our optional query string parameter category_id
to our Category model, so that it can be automatically injected into
the ProductController?
You bind the query string parameter to the product model, not the category model.
Below is a quick and dirty method of sending filtered data to the view based on query string input. This filters the category relation.
There may be syntax errors as I just quickly knocked an answer out - but the concept works.
ProductController
class ProductController extends BaseController
{
protected $productModel;
function __construct(Product $productModel)
{
$this->$productModel = $productModel;
}
public function index()
{
$products = $this->productModel
->join('categories', 'categories.id', '=', 'products.category_id');
->select(// select your columns)
$filterCategory = Input::get('category_id');
if ($filterCategory)
{
->where('category_id', '=', $filterCategory)
}
$data = $products->get();
return View::make( 'shop.product.index')->with($data);
}
}
A better solution would be to abstract this code away from the controller and override the newQuery method of the model.
It was in fact Glad to Help's answer in one of my similar questions who introduced me to the newQuery method. Laravel 4: Select row if a relation exists by querying relation