Laravel's Eloquent table 'inheritence' with parents - php

I have a Laravel model acl_groups that has a JSON column inherits. What should I do, the "laravel way" to query the inherited groups when checking if a group can do something? The rights are stored in another JSON column, allow/deny so I can just do a in_array to check a single group if they have access.

On your model you can set a getter
public function getInheritsAttribute($v)
{
return $v ? json_decode($v, true) : [];
}
OR
if you dont want a getter you can try a pseudo getter
public function getPseudoAttribute()
{
return $this->inherits ? json_decode($this->inherits, true) : [];
}
Kind of maybe did mistake on second one.
And on other model the same thing
so when you call $item->inherits = you will get an array
First you may try to prepare the array like removing same keys or values
and after just check
if (array_key_exists('thing_to_check', $item->inherits)) {
return true;
}
This is not a working code, it is just an idea how you can do you.
Take a look at Cartalyst Sentinel how they check the permissions for groups and users.

Related

Laravel Eloquent - Relationships and methods. When to use ->users() vs ->users [duplicate]

There is some basic understanding/theory here that I am missing.I don't understand the difference between these function calls:
$distributors = $store->distributors();
$distributors = $store->distributors;
$distributors = $store->distributors()->get();
$distributors = $store->distributors->get();
What I am trying to accomplis here is to get a list of the distributors for a store (a many to many relationship), and they get each distributors list of beers into one giant list.
foreach ($distributors as $distributor)
{
$available_beers = array_merge($distributor->beers(), $available_beers);
}
I don't know if that is the best way to do this and I can't get it to work. Similar to the first list of methods, I don't know if I need ->$beers or ->$beers()
Update
Thanks to everyone who answered! This will be a good reference for me going forward. My biggest lesson was the difference between getting a collection back, vs getting the query builder/relationship object back. For future reference to those who find this question, here is what I set up in my controller:
$store = $this->store->find($id)->first();
$distributors = $store->distributors;
$beers = [];
foreach ($distributors as $distributor){
$beers = array_merge($distributor->beers->lists('name', 'id'), $beers);
}
Short answer
$model->relation() returns the relationship object
$model->relation returns the result of the relationship
Long answer
$model->relation() can be explained pretty simple. You're calling the actual function you defined your relation with. Yours for distributor probably looks somewhat like this:
public function distributors(){
return $this->hasMany('Distributor');
}
So when calling $store->distributors() you just get the return value of $this->hasMany('Distributor') which is an instance of Illuminate\Database\Eloquent\Relations\HasMany
When do you use it?
You usually would call the relationship function if you want to further specify the query before you run it. For example add a where statement:
$distributors = $store->distributors()->where('priority', '>', 4)->get();
Of course you can also just do this: $store->distributors()->get() but that has the same result as $store->distributors.
Which brings me to the explanation of the dynamic relationship property.
Laravel does some things under the hood to allow you to directly access the results of a relationship as property. Like: $model->relation.
Here's what happens in Illuminate\Database\Eloquent\Model
1) The properties don't actually exist. So if you access $store->distributors the call will be proxied to __get()
2) This method then calls getAttribute with the property name getAttribute('distributors')
public function __get($key)
{
return $this->getAttribute($key);
}
3) In getAttribute it checks if the relationship is already loaded (exists in relations). If not and if a relationship method exists it will load the relation (getRelationshipFromMethod)
public function getAttribute($key)
{
// code omitted for brevity
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
4) In the end Laravel calls getResults() on the relation which then results in a get() on the query builder instance. (And that gives the same result as $model->relation()->get().
The direct answer to your question:
$store->distributors() will return the actual relationship object (\Illuminate\Database\Eloquent\Relations\BelongsToMany).
$store->distributors will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors()->get() will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors->get() should return an error since you're calling get() on a Collection object and the first parameter is not optional. If not an error, it should at least return null.
More information:
Given the following model:
class Store extends Eloquent {
public function distributors() {
return $this->belongsToMany('Distributor');
}
}
Calling the relationship method ($store->distributors()) will return to you the relationship (\Illuminate\Database\Eloquent\Relations\BelongsToMany) object. This is basically a query object which you can continue to modify, but you still need to call some type of method to get the results (e.g. get(), first(), etc).
However, accessing the relationship attribute ($store->distributors) will return to you a collection (\Illuminate\Database\Eloquent\Collection) object containing the results from executing the relationship query.
By default, the relationship attribute is created and assigned a value the first time it is accessed (known as "lazy loading"). So, the first time you access $store->distributors, behind the scenes it is executing the relationship query, storing the results in the $store->distributors attribute, and then returning those results. However, it only does this once. The next time you access $store->distributors, the attribute already contains the data, so that is what you are accessing.
To illustrate this:
// the following two statements will run the query twice
$r1 = $store->distributors()->get();
$r2 = $store->distributors()->get();
// the following two statements will run the query once.
// the first statement runs the query, populates $store->distributors, and assigns the variable
// the second statement just accesses the data now stored in $store->distributors
$r3 = $store->distributors;
$r4 = $store->distributors;
// at the end, $r1 == $r2 == $r3 == $r4
Relationships can also be "eager" loaded, using the with() method on the query. This is done to alleviate all of the extra queries that may be needed for lazy loading (known as the n+1 problem). You can read more about that here.
When you work with relationships with Eloquent the property is a collection (Illuminate\Database\Eloquent\Collection) of your relation white the method is a start of a new query.
Say your model looks like this:
class User extends Eloquent {
public function roles()
{
return $this->belongsToMany('Role');
}
}
If you try to access $user->roles, Eloquent will run the query and fetch all roles related to that user thanks to magic methods and returns an instance of Illuminate\Database\Eloquent\Collection. That class has a method called get, that's why $user->roles->get() works for you.
If you try to access the method, $user->roles(), you will instead get a query builder object so you can fine tune your query.
$user->roles()->whereIn('role_id', [1, 3, 4])->get();
That would only return roles where role_id is 1, 3 or 4.
So, the property returns a complete query and it results (Illuminate\Database\Eloquent\Collection) while the method lets you customize your query.
$distributors = $store->distributors();
Result of a method (function)
$distributors = $store->distributors;
Value of property (variable)
$distributors = $store->distributors()->get();
Take the first one, where it's the result of a method, if the method returns an object, this is a method in that object that was returned.
$distributors = $store->distributors->get();
If the property is an object, then it's calling a method in that property that's an object.
Re ->$beers vs ->$beers() that's a dynamic name of a property/method depending on what you're for. Just make a really rough guess at what you're doing, in your class you're going to have
$this->beers = array('bud','miller','sam');
and in your code using the $store object, you're actually going to go something like
$drink_type = 'beers';
$drink_list = $store->$drink_type;
And that will return $this->beers from $store, the same as writing $store->beers;
Imagine that the store class looks like this:
<?php
class Store {
public $distributors;
function __construct($distributors = array()) {
$this->distributors = $distributors;
}
public function distributors() {
return $this->distributors;
}
}
So the difference is:
$store = new Store(array('some guy', 'some other guy'));
$guys = $store->distributors; # accesing the $distributors property
$more = $store->distributors(); # calling the distributors() method.
The main difference is:
$distributors = $store->distributors() return instance of the relationship object like Illuminate\Database\Eloquent\Relations\BelongsToMany. You can use other conditions such as where after call this.
$store->distributors return instance of the collection Illuminate/Database/Eloquent/Collection. Laravel call the magic method __get under the hood. It will return a result of query relationship.
Maybe this will be usefull.
Access to method:
$object->method();
Access to property:
$object->property;

Call a controller with a parameter?

Let's imagine I have two Models:
A list of users User
A list of marbles Marble which belongs to one User
I would like to fetch all the existing marbles with api/marbles and only my marbles with api/user/marbles. The idea is to avoid a route named like api/marbles?owned=true
In my API routes I have this:
Route::get('marbles', 'MarbleController#index');
Route::get('user/marbles', 'MarbleController#index(true)');
Then in my MarbleController:
class MarbleControllerextends Controller
{
function index($owned = false) {
return $owned ? Marble::where('user_id', Auth::id())->get() : Marble::all();
}
}
Unfortunately the MarbleController#index(true) doesn't really work because (true) will not be accepted by Laravel not populate the optional $owned variable.
Is there a way to avoid defining a new method such as Route::get('user/marbles', 'MarbleController#owned');
function owned() {
return $this->index(true);
}
Route::get('marbles/{me?}', 'MarbleController#index'); will work fine.
Here me is an optional parameter. If you omit it, it will take false otherwise true as it's value.

Dynamic model filter in Laravel's Eloquent

I'm looking for a way to make a dynamic & global model filter in Laravel.
I'm imagining a function like the following in my User.php model:
public function filter() {
return ($someVariable === true);
}
Whenever I do a query using Eloquent's query builder, I only want users to show up in the collection when the filter above returns true. I would have thought a feature like that existed, but a quick look at the documentation suggests otherwise. Or did I miss it?
I believe what you're looking for is Query Scopes.
They are methods that may be defined in a global or local context, that mutate the current query for a given model.
https://laravel.com/docs/5.5/eloquent#query-scopes
For example:
Lets say I have a database table called "Teams" and it has a column on it called "Wins." If I wanted to retrieve all Teams that had a number of Wins between Aand B I could write the following Local scope method on the teams model:
public function scopeWinsBetween($query, int $min, int $max)
{
return $query->whereBetween('wins', $min, $max);
}
And it could be invoked as such:
$teams = Teams::winsBetween(50, 100)->get();
I think you could use Collection macro but you will need to suffix all your eloquent get(); to get()->userDynamicFilter();
Collection::macro('userDynamicFilter', function () {
//$expected = ...
return $this->filter(function ($value) use($expected) {
return $value == $expected;
});
});
Thanks. For now I've simply added a post filter option to the models using the following code:
// Apply a post filter on the model collection
$data = $data->filter(function($modelObject) {
return (method_exists($modelObject, 'postFilter')) ? $modelObject->postFilter($modelObject) : true;
});
in Illuminate/Database/Eloquent/Builder.php's get() function, after creating the collection. This allows me to add a function postFilter($model) into my model which returns either true or false.
Probably not the cleanest solution but a working one for now.

Route binding with composite key

I am trying to bind a model that has composite key. Take a look, at first place I define my route:
Route::get('laptop/{company}/{model}', 'TestController#test');
Now, I define as I want to be resolved:
$router->bind('laptop', function ($company, $model) {
$laptop = ... select laptop where company=$company and ...;
return $laptop;
});
Now, I see how I am injecting the class in order to get the laptop in the controller: function into to test the resolution:
function test(Laptop $laptop){
return 'ok';
}
However, I am receiving the following error:
BindingResolutionException in Container.php line 839:
I assume that the error is caused by $router->bind('laptop' because it should matches a unique placeholder in the url ("company" or "model"). In my case I get lost because I need to matches both at the same time.
Note: I am not using db/eloquent layer. This problem is focused in the way on how to resolve route binding with multiples keys representing an unique object.
I am not sure if is it possible or if am I missing something. Thank you in advance for any suggestion.
Laravel does not support composite key in eloquent query.
You need to use query builder method of laravel to match against both values. ie: DB::select()->where()->where()->get();
Just put select and where conditions in above.
If you bind $router->bind('laptop', ...); then your route parameter should be Route::get('{laptop}', ...);. There is two possibility to query a laptop by model and company as you expected.
The safest way is query laptop on your controller:
Route::get('laptop/{company}/{model}', 'TestController#test');
In you TestController.php
function test(Laptop $laptop, $company, $model){
return $laptop->whereCompany($company)->whereModel($model)->first();
}
Another solution is allow slashes on your route parameter:
Route::get('laptop/{laptop}', 'TestController#test')->where('laptop', , '(.*)?');
and your binding function could be:
$router->bind('laptop', function ($laptop) {
$laptop = explode('/', $laptop);
$company = current($laptop);
$model = end($laptop);
if ((count($laptop) === 2) && ($result = App\Laptop::whereCompany($company)->whereModel($model)->first()) {
return $result;
}
return abort(404);
}

Pass findBy parameter to callback method afterFind

I am trying to catch a certain findBy call (with afterFind) where:
if $results is empty (or the value you are trying to find is nonexistent), but the parameter value is found on another table, then it will modify $results to be valid
Some controller action got this:
$this->User->findByUsername("Bingo"); // yes, username Bingo doesnt exist on users table
User model:
function afterFind($results, $primary){
if(empty($results)) {
if(in_array($findbyparameter, array("Bingo", "Bingo1", "Bingo2"))) {
// modify $results
}
}
}
The problem is, how do I get $findbyparameter?
Thanks! All help will be appreciated!
I am not using these convenience methods, but you can pass the variable as Model property like this:
//where you search
$this->User->searchPhrase = "Bingo";
findByUsername($this->User->searchPhrase);
//Model
function afterFind($results, $primary){
if(empty($results)) {
if(in_array($this->searchPhrase, array("Bingo", "Bingo1", "Bingo2"))) {
// modify $results
}
}
}
It's not the prettiest method, but I guess it would work. Try to print_r($this) in afterFind method and see if you can spot somewhere the phrase which you search. I believe it's passed in the condition's array.
Perhaps a custom find type is what you're looking for. Custom find types have two states: before and after.
In the before you would setup your condition, and in the after you would check your data and modify if necessary. In both states you will have access to the query options.
Setting up custom finds is slightly different in 1.x and 2.x (you haven't mentioned which version you're using), so you can look up the specifics in the book.
In short, you would add add your the find type into the $findMethods property of the model and then add the corresponding method name to your model. Say you call your custom find type 'byUsername'
protected function _findByUsername($state, $query, $results = array()) {
if ($state === 'before') {
// add your condition to the query,
return $query;
} elseif ($state === 'after') {
// modify $results if you need to
return $results;
}
}
And you would call it via $this->User->find('byUsername', array('username' => $username));
In $query you would have the key 'username' which you can add to the conditions key of $query. In both states, you would have access to $query['username'].

Categories