Laravel model accessor, 'Call to a member function getAttribute() on null' - php

I'm trying to configure my Laravel app to store/retrieve user emails from a separate, related table. I want Laravel's built-in User model to retrieve its email record from a related Person model, which is linked by a person_id field on User.
I followed the steps in this answer, which is very similar to my scenario.
However, I'm encountering an issue:
I create a new user, and inspecting the records shows that everything has been set up properly: a Person model is created with extra information not included in User, and User properly references the Person model via a relation titled person. I can log in using the new user, which makes me think the service provider is properly linked as well.
When I send a password reset link, however, I get the error:
App\Models\User : 65
getEmailAttribute
'Call to a member function getAttribute() on null'
.
public function person()
{
return $this->belongsTo(Person::class);
}
public function getEmailAttribute()
{
// Error occurs here!
return $this->person->getAttribute('email');
}
public function getFirstNameAttribute()
{
return $this->person->getAttribute('firstName');
}
public function getLastNameAttribute()
{
return $this->person->getAttribute('lastName');
}
It seems like the code in Password::sendResetLink thinks that the person relation is null. I've checked which User id it's trying to reference, and a manual inspection shows that person is defined, I can even use the accessor normally, e.g. User::find({id})->email. I can't think of any reason why person would be null, as it's set up as a foreign key constraint on the database level...
Trying a password reset on another user account in my app - this one created by the database seeder - and it works fine...
Additionally, a nonsense email (not stored in DB) produces the same error... although I've confirmed that my first encounter with this error was using a proper email that is stored in the DB...
EDIT:
public function retrieveByCredentials(array $credentials)
{
if (
empty($credentials) ||
(count($credentials) === 1 &&
str_contains($this->firstCredentialKey($credentials), 'password'))
) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (str_contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->with([$this->foreign_model => function ($q) use ($key, $value) {
$q->whereIn($key, $value);
}]);
} elseif ($value instanceof Closure) {
$value($query);
} else {
//This is not working
$query->with([$this->foreign_model => function ($q) use ($key, $value) {
$q->where($key, $value);
}]);
}
}
return $query->first();
}
.
'users' => [
'driver' => 'person_user_provider',
'model' => App\Models\User::class,
'foreign_model' => 'person'
],

In my experience, somehow you app is calling this logic with faulty data and you are not certain whats causing it. There is ton of exception services, that can give deeper insights to how this happens, context, url called from etc. Since you are here asking the question, i'm guessing you want it fixed and we can help with the first case i described.
Instead why not create a defensive approach that helps this case, which can keep coming up. Laravel has the optional() helper, that makes it possible to call on null objects without the code crashing, the variable will end up null.
return optional($this->person)->getAttribute('email');
PHP 8 has a ?-> nullsafe operator, which does the same. I'm assuming PHP 8 has not been widespread adopted, so optional will works in all cases.
return $this->person?->getAttribute('email');

Related

Laravel's Eloquent table 'inheritence' with parents

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.

Will ActiveRecord:link() method fail or pass on on incorrect data?

When using ActiveRecord:link() method, am I forced to update relations in a "secure" way:
foreach ($postData['User']['lab'] as $labId) {
$lab = Lab::findOne($labId);
if ($lab instanceof \app\models\Lab) {
$model->link('lab', $lab);
}
}
Or can I do this, the "lazy" way:
foreach ($postData['User']['lab'] as $labId) {
$model->link('lab', Lab::findOne($labId));
}
Without caring for extra checkings?
Will link fail or pass on, if its feed with null (because call to Lab::findOne($labId) won't find given record on certain iteration)?
It will give error as far as i see from code.
With via in relation or without it calls methods from $model and if Lab::findOne($labId) is null - you'll get an error.
if ($relation->via !== null) {
if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
......
else {
$p1 = $model->isPrimaryKey(array_keys($relation->link));
......
And as documentation says:
Note that this method requires that the primary key value is not null.

Updating a One-To-Many Relationship in Laravel's Eloquent

Suppose I have a relationship between the following two Models in Laravel's Eloquent:
<?php
// user:
// - user_id
class User extends Model
{
protected $table = 'users';
public function settings()
{
return $this->hasMany('Setting');
}
public function settingSet($key, $value)
{
\Setting::setConfigItem($key, $value, $this->user_id);
}
}
// settting:
// - setting_key
// - setting_value
// - user_id
class Setting extends Model
{
public function setConfigItem($key, $value, $user_id)
{
// Note: I've provided this code here as an example, so it should
// exist here only as pseudo-code - it has not been tested and
// is outside the scope of this issue but has been requested by
// a commenter so I've provided the basis for this method:
$existing = \Setting::where(['key' => $key, 'user_id' => $user_id])->first();
if (!$existing) {
\Setting::insert([ 'setting_key' => $key, 'setting_value' => $value, 'user_id' => $user_id ]);
} else {
$existing->setting_value = $value;
$existing->save();
}
}
}
And I want to retrieve a single user and his settings, I can do the following:
<?php
$user = User::with(['setting'])->find(1);
Now, with this user, I can update or insert a setting using the settingSet method, as listed above.
<?php
$user->settingSet('foo','bar');
However, if I retrieve the settings at this point, I will get stale data.
<?php
print_r($user->settings); // onoes!
What's the best practice to force the data for this relationship to be updated after an INSERT/UPDATE/DELETE in the User::settingSet method or in other similar methods?
You can force the data to be updated by using Lazy Eager Loading, load() function.
print_r($user->load('settings'));
source: http://laravel.com/docs/5.0/eloquent#eager-loading
You have this issue due to using query builder instead of the eloquent,I dont understand why your using both,if you're using eloquent then use eloquent if you're using query builder use query builder,but dont use both,at least not when you have the possibility not to.
I find the setConfigItem method useless as you arent pushing a user into a setting but a setting into a user so basically all implementions should be on a user class and not on the settings class
After clearing that out,you could try doing something like this -
public function settingSet($key, $value)
{
$setting = new Setting([
'setting_key' => $key,
'setting_value' => $value
]);
$this->settings()->save($setting);
}
also you could improve this method by instead of accepting just 1 setting at a time you could accept array of settings
btw is there a reason why you arent using pivot table ? are the settings unique foreach user ?

Trigger validation error in related model

I have a model in Yii that contains an array of another model type. I am then trying to validate that no duplicate emails are filled out in a form, where you can fill out for n number of persons at the same time.
My current approach is to trigger a custom validation of the "outer" model that holds all the entrants, however, that model is not accessible in the view, only the array of entrants is, and if I then trigger the error on the "outer" model, it will not be displayed to the user. Therefore I would like to trigger it for the first entrant that violates the rule, but how do I go about doing that?
My code that attempts this, looks like this so far:
/*
* Custom validation rule to hinder the same e-mail being used twice.
*/
public function noRepeatingEmails($attribute, $params)
{
if (!isset($attribute)) return;
$emails = array();
foreach($this->$attribute as $user)
{
if (isset($user) && strlen(trim($user->email)) != 0)
{
$emailToAdd = strtolower(trim($user->email));
if (in_array($emailToAdd, $emails))
{
$this->addError($user, '<my error message>');
return;
}
else
{
$emails[] = $emailToAdd;
}
}
}
}
This only results in a code 500 error though:
Illegal offset type
I presume that is because it is looking for the property "user" in my model, rather than adding an error to "$user" object.
How do I best accomplish this?
I have a .NET background, so I am probably doing loads wrong here however.
If I understood correctly from your comment, you want to validate your model before saving it. For this purpose, CActiveRecord provides beforeSave() method. You need to put this method inside your model:
protected function beforeSave()
{
if(parent::beforeSave())
{
if(/* Your validation goes here*/)
return true;
else
return false
}
else
return false;
}
When the result of this method is true, save() method will be called. Otherwise save() method won't be called and therefore no record will be saved into your database.

Exceeded maximum time error when overriding the newQuery on Laravel 4.0

So, I was trying to implement this answer for my other question on the same subject... and it keeps givin me the exceeded time error. Any clues?
This is on my product model. It inherits from Eloquent.
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if( $user_permission->master )
return parent::newQuery();
else if( $user_permission->web_service )
{
$allowed_ids = array();
foreach( $user_permission->allowed_products()->get() as $allowed)
$allowed_ids[] = $allowed->id;
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
If the user is master there is no need to query scope on the request. But, if it isn't then I need to filter by the logged user's permissions.
UPDATE:
I tried the following code in a controller and it works alright:
$user_permission = Auth::user()->permissions;
echo "<PRE>"; print_r($user_permission->allowed_products()->get()); exit;
UPDATE 2:
Guys, I just found out that the problem was in this peace of code:
$allowed = Auth::user()->permissions()->first()->allowed_products()->get()->list('id');
It somehow give me an Maximum execution time of 30 seconds exceeded. If I put the exact same code in a controller, works like a charm, though! I also tried to put it in a scope, also worked. This it's really grinding my gears!
Elloquent has a function called newQuery. Controller does not. When you implement this function in a Model you are overriding the one in Elloquent. If you then invoke Elloquent methods that need a new query for your model before they can return, like ->allowed_products()->get(). Then you are calling your own newQuery() method recursively. Since the user permissions have not changed, this results in infinite recursion. The only outcome can be a timeout because it will keep on trying to determine a filtered product list which causes your newQuery() method to be called, which tries to determine the filtered product list before returning the query, and so on.
When you put the method into a Controller, it is not overriding the Elloquent newQuery method so there is no infinite recursion when trying to get the allowed_product list.
It would be more efficient to apply the filter to the product query based on whether the id is in the user's allowed_products() list using ->whereExists() and build up the same query as allowed_products() except now add condition that id from the query you are filtering is equal to the product id in the allowed products query. That way the filtering is done in the database instead of PHP and all is done in the same query so there is no recursion.
I don't see how your update code works. Illuminate\Database\Eloquent\Collection does not have any magic methods to call the relation functions, you should get an undefined method error trying to do that.
Can you try something like
public function newQuery($excludeDeleted = true)
{
// Returns `Illuminate\Database\Eloquent\Collection`
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
// If here you was to call $user_permission->allowed_products()->get() not much is going to happen, besides probably getting an undefined method error.
$allowed_ids = Auth::user()->permissions()->allowed_products()->get()->lists('id');
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
Update: as per comments below I believe the problem is due to newQuery() being called multiple times as the code works just fine when called once in a controller. When this is applied to every query there is no need to collect all the IDs over and over again (assuming they're not going to change each time you call for them). Something such as the below will allow you to store these and only process them once per request rather than every time a query is run.
private $allowed_ids_cache = null;
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
if ($this->allowed_ids_cache === null)
{
$this->allowed_ids_cache = Auth::user()->permissions()->allowed_products()->get()->lists('id');
}
return parent::newQuery()->whereIn('id', $this->allowed_ids_cache);
}
return parent::newQuery();
}

Categories