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 ?
Related
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');
As you probably know, we can use refresh method on a model instance in order to refresh it from the database.
Refresh completely reloads all columns from database. refresh method does not seem to take any input, so I was wondering if there is a way to refresh some fields from the database.
To demonstrate, let's have a pseudo-model as below:
Foo
---
bar: char
baz: char
And I have a single instance of Foo called $foo. $foo->refresh() fetches both $foo->bar and $foo->baz. How do I only fetch/refresh bar field?
Thanks in advance.
Environment
PHP 7.4.5
Laravel 7.x
I think there's no way to do such. The best option you have is to get the PK of your $foo object and query for the fields you want:
$id = $foo->id;
$refreshed = Foo::select('bar')->where('id', '=', $id)->first();
$foo->bar = $refreshed->bar;
However, this will unsync your model with the database. Another option is to create a custom method in your model that does it in a more abstract way and retrieves the new unsynced object:
// In your model:
public function refreshField(string $field)
{
$this->$field = (new static())
->select($field)
->where($this->key(), '=', $this->{$this->key()})
->first()->$field;
return $this;
}
// Then outside:
$foo = Foo::first();
// perform actions. . .
$foo->refreshField('bar');
Again, keep in mind that this will keep your model unsynced with the database. Also, this is a POC, I didn't test it, but the general idea should work. If you create a parent model and extend all your other models from this (which extends from Eloquent), this should work with any of your models.
For a more complete version, you can pass an array instead of a string and retrieve all the fields that you want to refresh, then assign them in a loop:
public function refresFields(string ...$fields)
{
$fields = $this->$field = (new static())
->select($fields)
->where($this->key(), '=', $this->{$this->key()})
->first();
foreach ($fields as $field) {
$this->$field = $field;
}
return $this;
}
I wouldn't recommend this, but if you really need it, give it a try. Good luck
Is there a way to invoke eloquent relationship methods without changing the original eloquent collection that the method runs on? Currently I have to employ a temporary collection to run the method immutable and to prevent adding entire related record to the response return:
$result = Item::find($id);
$array = array_values($result->toArray());
$temp = Item::find($id);
$title = $temp->article->title;
dd($temp); //This prints entire article record added to the temp collection data.
array_push($array, $title);
return response()->json($array);
You are not dealing with collections here but with models. Item::find($id) will get you an object of class Item (or null if not found).
As far as I know, there is no way to load a relation without storing it in the relation accessor. But you can always unset the accessor again to delete the loaded relation (from memory).
For your example, this process yields:
$result = Item::find($id);
$title = $result->article->title;
unset($result->article);
return response()->json(array_merge($result->toArray(), [$title]));
The above works but is no very nice code. Instead, you could do one of the following three things:
Use attributesToArray() instead of toArray() (which merges attributes and relations):
$result = Item::find($id);
return response()->json(array_merge($result->attributesToArray(), [$result->article->title]));
Add your own getter method on the Item class that will return all the data you want. Then use it in the controller:
class Item
{
public function getMyData(): array
{
return array_merge($this->attributesToArray(), [$this->article->title]);
}
}
Controller:
$result = Item::find($id);
return response()->json($result->getMyData());
Create your own response resource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'title' => $this->article->title,
'author' => $this->article->author,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Which can then be used like this:
return new ItemResource(Item::find($id));
The cleanest approach is option 3. Of course you could also use $this->attributesToArray() instead of enumerating the fields, but enumerating them will yield you security in future considering you might extend the model and do not want to expose the new fields.
I see two ways you can achieve that.
First, you can use an eloquent Resource. Basically it'll allow you to return exactly what you want from the model, so in your case, you'll be able to exclude the article. You can find the documentation here.
The second way is pretty new and is still undocumented (as fas i know), but it actually works well. You can use the unsetRelation method. So in your case, you just have to do:
$article = $result->article; // The article is loaded
$result->unsetRelation('article'); // It is unloaded and will not appear in the response
You can find the unsetRelation documentation here
There is not as far as I know. When dealing with Model outputs, I usually construct them manually like this:
$item = Item::find($id);
$result = $item->only('id', 'name', 'description', ...);
$result['title'] = $item->article->title;
return $result;
Should you need more power or a reusable solution, Resources are your best bet.
https://laravel.com/docs/5.6/eloquent-resources#concept-overview
I have a class called Vara, where i have a table field called searchname. I want to do a simple setup of cviebrock eloquent sluggable but can't figure out what the issue is.
When i save my model, nothing happens, it rewrite the old value stored.
If i change in build_from to, whatthefuckisgoingon i get the same output. I have a field called handle, also tried changing the field namne to slug but same result. If i leave build_from empty i also get the same output.
If i however change save_to to something that doesn't exist i get an error. The searchname field does have a value of "Hjordnära test 33 liter", so the output is really wierd.
My guess is that build_from is being ignored, and seen as null. How do i fix this?
My Vara.php looks like this
use Cviebrock\EloquentSluggable\SluggableInterface;
use Cviebrock\EloquentSluggable\SluggableTrait;
class Vara extends \Eloquent implements SluggableInterface {
use SluggableTrait;
protected $sluggable = array(
'build_from' => 'searchname',
'save_to' => 'handle'
);
In my VarorController.php
public function saveVara()
{
$id = Input::get('id');
$vara = Vara::find(Input::get('id'));
$vara->edited_by = Auth::user()->id;
$vara->searchname = Input::get('searchname');
$vara->save();
return $vara->getSlug();
Ok a litle update, found this function in SluggableTrait.php
public function sluggify($force=false)
{
$config = \App::make('config')->get('eloquent-sluggable::config');
$this->sluggable = array_merge( $config, $this->sluggable );
if ($force || $this->needsSlugging())
{
$source = $this->getSlugSource();
$slug = $this->generateSlug($source);
$slug = $this->validateSlug($slug);
$slug = $this->makeSlugUnique($slug);
$this->setSlug($slug);
}
return $this;
}
so if i add $vara->sluggify(true); to my controller the slug is being saved, so now the questions is why it does not sluggify automaticly on $vara->save();
Most probably, it's an issue of validation because you're using Ardent:
Ardent is a package that "provides self-validating smart models for Laravel Framework 4's Eloquent ORM"
Check your rules and use if statement:
if(! $vara->save()) // if model is invalid
dd($vara->errors());
If you don't need to check validation , you may use
$vara->forceSave();
To integrate Eloquent sluggable with Ardent, take a look at this link
Is it possible to update a user without touching the timestamps?
I don't want to disable the timestamps completly..
grtz
Disable it temporarily:
$user = User::find(1);
$user->timestamps = false;
$user->age = 72;
$user->save();
You can optionally re-enable them after saving.
This is a Laravel 4 and 5 only feature and does not apply to Laravel 3.
In Laravel 5.2, you can set the public field $timestamps to false like this:
$user->timestamps = false;
$user->name = 'new name';
$user->save();
Or you can pass the options as a parameter of the save() function :
$user->name = 'new name';
$user->save(['timestamps' => false]);
For a deeper understanding of how it works, you can have a look at the class \Illuminate\Database\Eloquent\Model, in the method performUpdate(Builder $query, array $options = []) :
protected function performUpdate(Builder $query, array $options = [])
// [...]
// First we need to create a fresh query instance and touch the creation and
// update timestamp on the model which are maintained by us for developer
// convenience. Then we will just continue saving the model instances.
if ($this->timestamps && Arr::get($options, 'timestamps', true)) {
$this->updateTimestamps();
}
// [...]
The timestamps fields are updated only if the public property timestamps equals true or Arr::get($options, 'timestamps', true) returns true (which it does by default if the $options array does not contain the key timestamps).
As soon as one of these two returns false, the timestamps fields are not updated.
Above samples works cool, but only for single object (only one row per time).
This is easy way how to temporarily disable timestamps if you want to update whole collection.
class Order extends Model
{
....
public function scopeWithoutTimestamps()
{
$this->timestamps = false;
return $this;
}
}
Now you can simply call something like this:
Order::withoutTimestamps()->leftJoin('customer_products','customer_products.order_id','=','orders.order_id')->update(array('orders.customer_product_id' => \DB::raw('customer_products.id')));
To add to Antonio Carlos Ribeiro's answer
If your code requires timestamps de-activation more than 50% of the time - maybe you should disable the auto update and manually access it.
In eloquent when you extend the eloquent model you can disable timestamp by putting
UPDATE
public $timestamps = false;
inside your model.
If you need to update single model queries:
$product->timestamps = false;
$product->save();
or
$product->save(['timestamps' => false]);
If you need to update multiple model queries use
DB::table('products')->...->update(...)
instead of
Product::...->update(...)
For Laravel 5.x users who are trying to perform a Model::update() call, to make it work you can use
Model::where('example', $data)
->update([
'firstValue' => $newValue,
'updatedAt' => \DB::raw('updatedAt')
]);
As the Model::update function does not take a second argument anymore.
ref: laravel 5.0 api
Tested and working on version 5.2.
I ran into the situation of needing to do a mass update that involves a join, so updated_at was causing duplicate column conflicts. I fixed it with this code without needing a scope:
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) {
$query->getModel()->timestamps = false;
})
For Larvel 5.1, you can also use this syntax:
Model::where('Y', 'X')
->update(['Y' => 'Z'], ['timestamps' => false]);
Laravel 9 and above
Taken directly from the documentation.
If you would like to perform model operations without the model having its updated_at timestamp modified, you may operate on the model within a closure given to the withoutTimestamps method:
Model::withoutTimestamps(fn () => $post->increment(['reads']));
So in OP's case, the code will be something like this:
User::withoutTimestamps(function () {
$user = User::find(1);
$user->name = 'John';
$user->save();
});
Laravel 8
Doing some overrides using seeders and on one test I have:
$item = Equipment::where('name', item_name))->first();
$item->description = 'some description';
$item->save(['timestamps' => false]);
Which works fine, but if I use firstOrNew then the $item->save(['timestamps' => false]); does not work.
// This does not work on Seeder file
$item = Model::firstOrNew(['name' => 'item_name']);
$item->description = 'some description';
$item->save(['timestamps' => false]);
// Doing the following works tho
$item = Model::firstOrNew(['name' => 'item_name']);
$item->description = 'some description';
$item->timestamps = false;
$item->save();
So in some cases you would use one over the other... Just check with die and dump to see whether +timestamps: false
$item->timestamps = false;
$item->save();
or
$item->save(['timestamps' => false]);
Edit:
In my project I opted using $item->timestamps = false; so I would recommend using this as well. Here is a working snippet from laravelplayground:
https://laravelplayground.com/#/snippets/4ae950f2-d057-4fdc-a982-34aa7c9fee15
Check the HasTimestamps on Laravel api:
https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Concerns/HasTimestamps.html
and the save method on Model:
https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html
The save method still accepts options but passing timestamps will not work.