Laravel getting builder instead of relation - php

I am trying to validate a model before saving it. Obviously if the model isn't valid, it shouldn't be saved to the database. When validation fails, it throws an exception and doesn't continue to save. This below works:
$question = new Question([...]);
$question->validate();
$question->save();
I have a problem with the answers() hasMany relationship. According to this answer I should be able to call add() on the relation object:
$question = new Question([...]);
$question->answers()->add(new Answer([...]));
$question->validate();
$question->save();
The above fails:
Call to undefined method Illuminate\Database\Query\Builder::add()
I thought the answers() function would return a HasMany relationship object, but it looks like I'm getting a builder instead. Why?

answers() does return a HasMany object. However, because there is no add method on a HasMany object, Laravel resorts to PHP's __call magic method.
public function __call($method, $parameters)
{
$result = call_user_func_array([$this->query, $method], $parameters);
if ($result === $this->query) {
return $this;
}
return $result;
}
The __call method gets the query instance and tries to call the add method on it. There is no add method on the query builder though, which is why you are getting that message.
Finally, the add method is part of Laravel's Eloquent Collection, not a part of HasMany. In order to get the Collection class, you need to drop the parenthesis (as shown in the answer provided in your link) and do this instead:
$question->answers->add(new Answer([...]));

There's no add method. Use the save method:
$question->answers()->save(new Answer([]));

Related

Laravel difference between Model, Builder and Collection

I came across this code snippet on Laravel doc
// Retrieve a model by its primary key...
$flight = App\Flight::find(1);
// Retrieve the first model matching the query constraints...
$flight = App\Flight::where('active', 1)->first();
where and find are builder functions, why App\Flight as a Model can call these function. And what are the differences between Model, Builder and Collection in Laravel?
You're able to call Builder functions on an Eloquent model, because the Model class uses the magic __call method.
As you can see in the method definition below, if the method doesn't exist on the class, or it's not increment or decrement, a new Builder query is created, on which the method is called.
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
try {
return $this->newQuery()->$method(...$parameters);
} catch (BadMethodCallException $e) {
throw new BadMethodCallException(
sprintf('Call to undefined method %s::%s()', get_class($this), $method)
);
}
}
https://github.com/illuminate/database/blob/master/Eloquent/Model.php#L1439
As for the difference between a Model, Builder and Collection:
Model: This follows the pattern where a model is essentially an instance of a database row, allowing you to create, update and delete a single row.
Builder: This is a layer of abstraction between your application and the database. Typically it's used to provide a common API for you to build platform agnostic database queries. E.g. they'll work on MySQL, Postgres, SQL Server, etc.
Collection: This is basically an array on steroids. It provides a chainable API for standard array_* type PHP functions, together with other useful functions for manipulating a collection of data.

Laravel 5 update returns the old relation

I have a "belongsTo" relation.
public function relation()
{
return $this->belongsTo('App\Relation', 'relation_id');
}
When I create an instance everything works as expected. It is returned the new instance including the relation.
public function store(Request $request)
{
$instance = $this->model->create($request->all());
return $instance;
}
When I update the "relation_id" It returns the old relation. It is not returned immediatly.
public function findOrFail($id)
{
$link = $this->model
->with('relation')
->findOrFail($id);
return $link;
}
public function update(Request $request, $id)
{
$instance = $this->findOrFail($id);
$instance->update($request->all());
//$instance = $instance ->fresh();
return $instance ;
}
Seems that using $instance->fresh() or removing ->with('relation') the new relation is returned immediatly in the current instance.
I am wondering why it does not return the new relation immediatly as it does for create.
create() function returns instance see API DOC on create()
update() returns bool see API DOC on update()
After update you need to reload relation using load() or fresh() as you stated.
You only update current model / table not any relationships bound to it.
Most likely you are missing (or have incorrect) $fillable array in your model. Which protects you against "mass-assignment" vulnerability.
Read more on topic in documentation here.
A mass-assignment vulnerability occurs when a user passes an unexpected HTTP parameter through a request, and that parameter changes a column in your database you did not expect. For example, a malicious user might send an is_admin parameter through an HTTP request, which is then passed into your model's create method, allowing the user to escalate themselves to an administrator.
Tip:
Do not use $request->all(), instead use $request->only(['name', 'age']) now its explicitly using only name and age parameters. I know its not DRY, because you have to keep $fillable in sync with ->only array, but anyone new to the code will know right from controller what is going on.

Using a query scope in a collection laravel

My Association model looks like this (irrelevant code redacted):
class Association extends Model
{
public function members() {
return $this->hasMany('App\Member');
}
}
My Member model looks like this:
class Member extends Model
{
public function scopeActive($query) {
return $query->where('membership_ended_at', Null);
}
public function scopeInactive($query) {
return $query->whereNotNull('membership_ended_at');
}
}
This is what I want to be able to do:
$association = Association::find(49);
$association->members->active()->count();
Now, I'm aware there's a difference between a Query and a Collection. But what I'm basically asking is if there's some kind of similar scope for collections. Of course, the optimal solution would be to not have to write TWO active methods, but use one for both purposes.
(question already answered in the comments, but might as well write a proper answer)
It is not possible to use a query scope in a Colletion, since query scope is a concept used in Eloquent to add constraints to a database query while Collections are just a collection of things (data, objects, etc).
In your case, what you need to do is to change this line:
$association->members->active()->count();
to:
$association->members()->active()->count();
This works because when we call members as a method, we are getting a QueryBuilder instance, and with that we can start chaining scopes to the query before calling the count method.

Returning a relationships results through static method

I'm trying to provide an extra static 'find' method on my eloquent model, shown here:
public static function findBySku($sku)
{
// Using new self; provides the same empty collection results
$instance = new static;
// Using $instance->sku()->newQuery()->get() also returns the same empty collection
$results = $instance->sku()->get();
/*
* This returns an empty collection, however there are records inside the
* relationship database table?
*/
dd($results);
}
So I can use: Inventory::findBySku($sku);
Here's the relationship:
public function sku()
{
return $this->hasOne('Stevebauman\Maintenance\Models\InventorySku', 'inventory_id', 'id');
}
I know the relationship itself isn't the issue because this returns the results from the database table fine:
Inventory::find(1)->sku()->get();
Anyone have any ideas why this doesn't work?
I know it could be because I'm calling a non-static method from a static instance, but why would it return a resulting collection without throwing an error?
Thanks!
Hang on, figured it out, apologies!
Eloquent relationships have a method getRelated() to access the related model instance. I can then call the methods I need off of it, for example:
public static function findBySku($sku)
{
$instance = new static;
// Using the getRelated() method allows me to run queries on the related model
$results = $instance->sku()->getRelated()->get();
dd($results);
}
Just sort of an odd workaround as you'd think accessing the relationship itself would give you the proper query.
I hope this helps out someone in the future!

Why I'm getting 'Non-static method should not be called statically' when invoking a method in a Eloquent model?

Im trying to load my model in my controller and tried this:
return Post::getAll();
got the error Non-static method Post::getAll() should not be called statically, assuming $this from incompatible context
The function in the model looks like this:
public function getAll()
{
return $posts = $this->all()->take(2)->get();
}
What's the correct way to load the model in a controller and then return it's contents?
You defined your method as non-static and you are trying to invoke it as static. That said...
1.if you want to invoke a static method, you should use the :: and define your method as static.
// Defining a static method in a Foo class.
public static function getAll() { /* code */ }
// Invoking that static method
Foo::getAll();
2.otherwise, if you want to invoke an instance method you should instance your class, use ->.
// Defining a non-static method in a Foo class.
public function getAll() { /* code */ }
// Invoking that non-static method.
$foo = new Foo();
$foo->getAll();
Note: In Laravel, almost all Eloquent methods return an instance of your model, allowing you to chain methods as shown below:
$foos = Foo::all()->take(10)->get();
In that code we are statically calling the all method via Facade. After that, all other methods are being called as instance methods.
Why not try adding Scope? Scope is a very good feature of Eloquent.
class User extends Eloquent {
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
public function scopeWomen($query)
{
return $query->whereGender('W');
}
}
$users = User::popular()->women()->orderBy('created_at')->get();
Eloquent #scopes in Laravel Docs
TL;DR. You can get around this by expressing your queries as MyModel::query()->find(10); instead of MyModel::find(10);.
To the best of my knowledge, starting PhpStorm 2017.2 code inspection fails for methods such as MyModel::where(), MyModel::find(), etc (check this thread), and this could get quite annoying.
One (elegant) way to get around this is to explicitly call ::query() wherever it makes sense to. This will let you benefit from free auto-completion and a nice formatting/indentating for your queries.
Examples
BAD
Snippet where inspection complains about static method calls
// static call complaint
$myModel = MyModel::find(10);
// another poorly formatted query with code inspection complaints
$myFilteredModels = MyModel::where('is_foo', true)
->where('is_bar', false)
->get();
GOOD
Well formatted code with no complaints
// no complaint
$myModel = MyModel::query()->find(10);
// a nicely formatted and indented query with no complaints
$myFilteredModels = MyModel::query()
->where('is_foo', true)
->where('is_bar', false)
->get();
Just in case this helps someone, I was getting this error because I completely missed the stated fact that the scope prefix must not be used when calling a local scope. So if you defined a local scope in your model like this:
public function scopeRecentFirst($query)
{
return $query->orderBy('updated_at', 'desc');
}
You should call it like:
$CurrentUsers = \App\Models\Users::recentFirst()->get();
Note that the prefix scope is not present in the call.
Solution to the original question
You called a non-static method statically. To make a public function static in the model, would look like this:
public static function {
}
In General:
Post::get()
In this particular instance:
Post::take(2)->get()
One thing to be careful of, when defining relationships and scope, that I had an issue with that caused a 'non-static method should not be called statically' error is when they are named the same, for example:
public function category(){
return $this->belongsTo('App\Category');
}
public function scopeCategory(){
return $query->where('category', 1);
}
When I do the following, I get the non-static error:
Event::category()->get();
The issue, is that Laravel is using my relationship method called category, rather than my category scope (scopeCategory). This can be resolved by renaming the scope or the relationship. I chose to rename the relationship:
public function cat(){
return $this->belongsTo('App\Category', 'category_id');
}
Please observe that I defined the foreign key (category_id) because otherwise Laravel would have looked for cat_id instead, and it wouldn't have found it, as I had defined it as category_id in the database.
You can give like this
public static function getAll()
{
return $posts = $this->all()->take(2)->get();
}
And when you call statically inside your controller function also..
I've literally just arrived at the answer in my case.
I'm creating a system that has implemented a create method, so I was getting this actual error because I was accessing the overridden version not the one from Eloquent.
Hope that help?
Check if you do not have declared the method getAll() in the model. That causes the controller to think that you are calling a non-static method.
For use the syntax like return Post::getAll(); you should have a magic function __callStatic in your class where handle all static calls:
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}

Categories