Adding a new property dynamically to model (Laravel) - laravel

I'm using Laravel 5.4 and can't seem to add a new property, here's my code including a comment section that shows the output which does not return the new attribute I added called Url.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Exercice extends Model
{
protected $table = 'exercices';
protected $connection = 'DB_V2';
protected $appends = ['Url'];
public function getUrlAttribute() {
return 'custom';
}
/*
$exercise = App\Exercice::where("idExercice", 1)->get();
dd($exercise);
#attributes: array:15 [▼
"idExercice" => 1
"image1" => "160_A.jpg"
"image2" => "a.jpg"
"thumb1" => "v.jpg"
"thumb2" => "c.jpg"
"video" => "fliqz|077fzc4f478142cea8a73e586617f8a\r\n"
"draw1" => ""
"draw2" => ""
"drawthumb1" => ""
"drawthumb2" => ""
"licno" => 1000
"idusager" => 0
"code_exercice" => "XAMP160"
"shared" => "Y"
"rank" => 99999999
]
*/
}

The value is not going to show up in a dd() of the object itself, because the attribute doesn't actually exist in the $attributes property.
If you dd() the array or json output, you should see your value:
$exercise = App\Exercice::where("idExercice", 1)->get();
dd($exercise->toArray());

Related

laravel endpoint hide field

How can i hide some fields ?
i want to hide the file field
Eloquent :
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return ReportResource::collection($reports);
Model :
...
public function getFileSizeAttribute()
{
return Storage::disk('files')->size($this->attributes['file']);
}
....
ReportResource:
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file), <-- i want to hide the file field
'file_size' => $this->fileSize, <-- but always show file_size
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
to get file_size field i must select the file field , because it's depends on it to calculate the file size.
but i want to hide the file field before send the response.
i know i can use the protected $hidden = [] method in the model , but i don't want that, because file field it's required on others place. i just want to hide it on this endpoint only.
Since you are using API resources the best and clean way to do this is by using a Resource class for your collection.
Said that, you will have 3 Resources:
The first one, as it is, just for retrieving a single Model with file and file_size attributes. The one you already have ReportResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
A new second resource to be used in your endpoint, without the file attribute. IE: ReportIndexResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
Now you need to create a Resource collection which explicitly defines the Model Resource to use. IE: ReportCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ReportCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* #var string
*/
public $collects = ReportIndexResource::class;
}
Finally, use this new resource collection in your endpoint
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return new ReportCollection($reports);
Of course, you can make use of makeHidden() method, but IMO is better to write a little more code and avoid a non desired attribute in your response because you forgot to make it hidden.
Also, in case you make use of makeHidden() method and you want to show the attribute in a future, you will have to update all your queries instead of a silgle resource file.
If you want to make it Hide From All Returns , you can Do this in model
protected $hidden = ['file'];
and if you want to do it temporirly with this query , you can Use MakeHidden method
$users = $reports->makeHidden(['file']);
It's clear in laravel docs , take a look
https://laravel.com/docs/9.x/eloquent-collections#method-makeHidden

Laravel 8 Class Based Model Factories

I'm trying to figure out why when I try and create a factory of my Player class and dd($this) it comes back as that its not overriding the state of the model. What also doesn't make sense is that it is a collection of two items for the states.
Can anyone give further clarification for any of this?
$player = Player::factory()->injured()->create();
<?php
namespace Database\Factories;
use App\Enums\PlayerStatus;
use App\Models\Player;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class PlayerFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected string $modelClass = Player::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition(): array
{
return [
'name' => $this->faker->name,
'height' => $this->faker->numberBetween(60, 95),
'weight' => $this->faker->numberBetween(180, 500),
'hometown' => $this->faker->city.', '.$this->faker->state,
'status' => PlayerStatus::__default,
];
}
public function injured(): self
{
$this->state([
'status' => PlayerStatus::INJURED,
]);
dd($this);
$now = now();
$start = $now->copy()->subDays(2);
$this->hasEmployments(1, ['started_at' => $start]);
$this->hasInjuries(1, ['started_at' => $now]);
return $this;
}
}
^ Database\Factories\PlayerFactory^ {#2650
#modelClass: "App\Models\Player"
#model: null
#count: null
#states: Illuminate\Support\Collection^ {#2647
#items: array:2 [
0 => Closure()^ {#2631
class: "Illuminate\Database\Eloquent\Factories\Factory"
this: Database\Factories\PlayerFactory {#2626 …}
use: {
$state: []
}
}
1 => Closure()^ {#2646
class: "Illuminate\Database\Eloquent\Factories\Factory"
this: Database\Factories\PlayerFactory {#2648 …}
use: {
$state: []
}
}
]
}
#has: Illuminate\Support\Collection^ {#2610
#items: []
}
#for: Illuminate\Support\Collection^ {#2640
#items: []
}
#afterMaking: Illuminate\Support\Collection^ {#2455
#items: []
}
#afterCreating: Illuminate\Support\Collection^ {#2453
#items: []
}
#connection: null
#faker: null
}
The method call:
$this->state([
'status' => PlayerStatus::INJURED,
]);
does not modify the current object - it creates a new Factory Instance with the updated 'status'. And this new Factory should be returned by the injured method.
In this case one solution would be:
public function injured(): self
{
$injuredPlayer = $this->state([
'status' => PlayerStatus::INJURED,
]);
$now = now();
$start = $now->copy()->subDays(2);
$injuredPlayer->hasEmployments(1, ['started_at' => $start]);
$injuredPlayer->hasInjuries(1, ['started_at' => $now]);
return $injuredPlayer;
}

laravel DB update get changes column

i want to save log of changes when i update something on the database.
there is elegant way to get the column that will be updated (just if there is change).
i want to save the old column value in log..
for example:
$updateUser = DB::table('users')->where('id','1')->update(array('email' => 'new#email.com', 'name' => 'my new name'));
from this i want to get back the old email was in database (if changed) and the old name (again, only if changed)
thanks!
As others have mentioned, Eloquent is a great way to go if using Laravel. Then you can tap directly into Laravel's events using Observers. I have used a method very similar to what is below. Of course, you would need to set up Models for User and AuditLog.
See more info regarding Observers.
https://laravel.com/docs/5.8/eloquent#observers
In Controller Method
$user = User::find(1);
$user->update([
'email' => 'new#email.com',
'name' => 'my new name'
]);
App/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider
{
// ...
public function boot()
{
User::observe(UserObserver::class);
}
}
App/Observers/UserObserver.php
class UserObserver
{
/**
* The attributes to exclude from logging.
*
* #var array
*/
protected $except = [
'created_at',
'updated_at'
];
/**
* The attributes to mask.
*
* #var array
*/
protected $masked = [
'password',
];
/**
* Listen for model saved event.
*
* #var array
*/
public function saved($model)
{
// search for changes
foreach ($model->getChanges() as $key => $new_value) {
// get original value
$old_value = $model->getOriginal($key);
// skip type NULL with empty fields
if ($old_value === '' && $new_value === null) {
continue;
}
// attribute not excluded and values are different
if (!in_array($key, $this->except) && $new_value !== $old_value) {
// mask designated fields
if (in_array($key, $this->masked)) {
$old_value = '********';
$new_value = '********';
}
// create audit log
AuditLog::create([
'user_id' => auth()->user()->id,
'model_id' => $model->id,
'model' => (new \ReflectionClass($model))->getShortName(),
'action' => 'update',
'environment' => config('app.env'),
'attribute' => $key,
'old_value' => $old_value,
'new_value' => $new_value,
]);
}
}
}
}
I hope this helps!
EDIT: See comment regarding update.
I will suggest 2 options:
1) to use the Eloquent model on every changes,
and then to use the existing methods like :
model->isDirty()
model->getChanges()
you can implement it on the model life cycle of updating / updated events listeners
more information and example you can see here:
https://laravel.com/docs/5.8/events
https://medium.com/#JinoAntony/10-hidden-laravel-eloquent-features-you-may-not-know-efc8ccc58d9e
https://laravel.com/api/5.3/Illuminate/Database/Eloquent/Model.html
2) if you want to log changes even if you are running regular queries and not only via model life cycle,
you can use MySql Triggers on every table updates and then to check OLD vs NEW and insert directly to the log changes db
more information you can find here:
https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html
MySQL Trigger after update only if row has changed
Why not just something like this:
$changeArr = ['email' => 'new#email.com', 'name' => 'my new name'];
$id = 1;
$table = 'users';
foreach($changeArr as $key => $value){
DB::table('updateTable')->insert(['table' => $table, 'id' => $id, 'col' => $key, 'oldVal' => $value]);
}
$updateItem = DB::table($table)->where('id', $id)->update($changeArr);
Check for the changed values and update accordingly, saving the old values to log table if changed
$newData = ['email' => 'new#email.com', 'name' => 'my new name'];
$user = App\User::find(1);
$log = [];
if ($user->email != $newData['email']) {
$log['user_id'] = $user->id;
$log['email'] = $user->email;
$user->email = $newData['email'];
} elseif ($user->name != $newData['name']) {
$log['name'] = $user->name;
$user->name = $newData['name'];
$logged = DB::table('log')->insert($log);
}
$updateUser = $user->save();
//try this. hpe it helps out:
function Update(Request $request, $id)
{
$dbrecord = DB::table('users')->where('id',$id)->first();
$oldemail = $dbrecord->email;
$oldname = $dbrecord->name;
if(($oldemail==$request->input('email'))&&($oldname==$request->input('name')))
{
//do nothing
}
elseif(($oldemail!=$request->input('email'))or($oldname!=$request->input('name')))
{
$updateUser = DB::table('users')->where('id',$id)->update(array('email' => $request->input('email'), 'name' => $request->input('name')));
if($updateUser)
{
DB::table('log')->where('id',$id)->insert(array('email' => $oldemail, 'name' => $oldname));
}
}
}

Laravel: How to get model instance after create that has a global scope

I struggle to get the model instance with global scopes.
Normally, if you write:
$offer = Offer::createNew();
with
public static function createNew()
{
return static::create([
'user_id' => auth()->id(),
'reviewed' => false
]);
}
You would get the model instance. But now I've added a global scope and cannot get it to work.
The model is "nearly" empty as you expect because in my case I want only to get Offers that are reviewed. However, if I add:
public static function createNew()
{
return static:::withoutGlobalScopes()->create([
'user_id' => auth()->id(),
'reviewed' => false
]);
}
I get a result from a limited model that only contains these attributes:
#attributes: array:5 [
"user_id" => 1
"reviewed" => false
"updated_at" => "2018-09-24 11:48:27"
"created_at" => "2018-09-24 11:48:27"
"id" => 2
]
But my model has obviously more attributes than that. If I add get(), I'm only getting
Illuminate\Database\Eloquent\Collection {#1635
#items: []
}
So how would you get the model with create when having a global scope?
Edit
My first workaround looks like this:
public static function createNew()
{
$model = static::create([
'user_id' => auth()->id(),
'reviewed' => false
]);
return static::withoutGlobalScopes()->find($model->id);
}
Edit 2
My Globalscope looks like this:
class FoodScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('reviewed', true)
->where('paused', false);
}
}
This behavior is not caused by the global scope.
Use ->fresh() when you want to get the other attributes:
public static function createNew()
{
return static::create([
'user_id' => auth()->id(),
'reviewed' => false
])->fresh();
}

Laravel append attribute not working

My objective is to append average rating of each products so i can display in the front end
I have two tables one is products and another is reviews
My review model
class Review extends Model
{
protected $table = 'reviews';
public $timestamps = true;
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $fillable = array('user_id', 'product_id', 'rating', 'feedback');
public function user()
{
return $this->belongsTo('App\Models\User');
}
public function product()
{
return $this->belongsTo('App\Models\Product');
}
}
My product model
protected $appends = ['average_rating','my_rating'];
// i added these accoceries inside class as per the laravel documentation
public function reviews()
{
return $this->hasMany(Review::class);
}
public function getAverageRatingAttribute(){
return round($this->reviews()->avg('rating'),1);
}
public function getMyRatingAttribute(){
//check if user loged in
if(Auth::check()){
return round($this->reviews()->where('user_id',Auth::user()->id)->avg('rating'),1);
}else{
return round($this->reviews()->where('user_id',NULL)->avg('rating'),1);
}
}
Response
[original:protected] => Array
(
[id] => 1
[user_id] => 1
[sku_code] =>
[name] => Product title
[slug] => product-title
[description] => This is loream ipsum text to check if the application is working correctly or not
[thumbnail] => images/products/thumbnail/a9fa0b28.jpg
[short_description] => This is loream ipsum to check if update working or not
[featured_image_id] =>
[category_id] => 1
[subcategory_id] =>
[price] => 259.00
[img_height] => 20
[img_width] => 10
[discount_id] =>
[featured_product] => 0
[availability] => 1
[created_at] => 2018-02-22 11:33:27
[updated_at] => 2018-02-22 13:36:21
[deleted_at] =>
[weight] => 100
[weight_unit] => kg
)
So basically this should append average rating to my product when ever i call from the controller.
But instead i m getting Only the fields available in product table. I worked with this before and worked fine back then but i do not understand why it is not working this time.
Can anyone please help me?
thank you.
I think there is a misunderstanding about how $appends works. From the official Laravel docs:
Once the attribute has been added to the appends list, it will be included in both the model's array and JSON forms.
So it will not appear in the attributes list of the model if you print the model instance itself. However, the appended attribute will appear in the results of $model->toArray() and $model->toJson(). It will also be accessible using $model->appended_attribute (or $model->average_rating in your case).
The solution to this problem is getting data like this $product->my_rating and $product->average_rating. as suggested by #tykus at laracast
https://www.laracasts.com/discuss/channels/eloquent/laravel-append-attribute-not-working

Resources