Figure what fields `save()` acted on if any (detecting changes) - laravel

Doing ->save() and ->update() only updates when changes made, I think, is this true?
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
I typically update like this:
public function update(Requests\UpdatePetRequest $request, Pet $pet)
{
$pet->update($request->all());
return $pet;
}
I want to figure from ->save and from ->update if what field names were updated? Along with what was the old value and the new value.
I currently manually do this like this:
public function update(Requests\UpdatePetRequest $request, Pet $pet)
{
$changes = [];
if ($request->exists('name') && $request->name != $pet->name) {
$changes['name'] = array([
'old' => $pet->name,
'new' => $request->name
]);
}
if ($request->exists('avatar') && $request->avatar != $pet->avatar) {
$changes['avatar'] = array([
'old' => $pet->avatar,
'new' => $request->avatar
]);
}
if (!count($changes)) {
return response()->json(['error'=>'No properties changed'], 422);
}
$pet->update($request->all());
$body = json_encode($changes);
$message = new Message(['body' => $body, 'kind' => 'PET_UPDATE']);
}
Is there an automated way to do this?

You can't do that with update(), but you can use getDirty() before save():
$model = Model::find($id);
$model->fill($request->all());
$cahnges = $model->getDirty();
$model->save();
getDirty() will return you an array with changed columns only, for example:
['name' => 'New Name', 'address' => 'New Street, 12']

Related

Observer not running when creating in database

Hello I have this method that mass create student sections
Enrollment controller method this code works fine but it doesn't get in my studentSectionObserver. Although it's getting saved one by one with for loop.
public function setStudentsSection(Request $request)
{
$enrollments = Enrollment::whereIn('student_id', $request->students)->where('session_id', $request->session_id)->get();
$program_section = ProgramSection::withCount('students')->find($request->program_section_id);
if(($program_section->students_count + count($enrollments)) <= $program_section->max_students) {
$new_student_sections = array();
foreach($enrollments as $enrollment) {
$data = [
'student_id' => $enrollment->student_id,
'enrollment_id' => $enrollment->id,
'section_id' => $request->program_section_id,
'created_at' => Carbon::now()
];
array_push($new_student_sections, $data);
}
return StudentSection::insert($new_student_sections);
}
return response()->json(['errors' => ['message' => 'Selected Section is full.']], 405);
}
Then i output this activity with studentSectionObserver and added log::info but it doesn't log anything
public function created(StudentSection $student_section)
{
Log::info('test');
$student = $student_section->student()->get()->first();
$section = $student_section->section()->get()->first();
Logger::createLog("Assigned " . $student->first_name . " " . $student->last_name . " '" . $section->section->name . "'");
}
I know this observer gets triggered cause i tested it with this method whenever i add section the studentSectionObserver triggers Logger.
public function enrollStudent(EnrollmentRequest $request)
{
$check_if_exist = Enrollment::where('student_id', $request->student_id)->where('session_id', $request->session_id)->first();
if (!$check_if_exist) {
$program_section = ProgramSection::withCount('students')->find($request->section_id);
if($program_section) {
if($program_section->students_count < $program_section->max_students) {
$enrollment = Enrollment::create($request->all());
$section_data = ['student_id' => $request->student_id, 'section_id' => $request->section_id, 'enrollment_id' => $enrollment->id];
$section = StudentSection::create($section_data);
return response()->json($enrollment, 200);
}
return response()->json(['errors' => ['message' => 'Selected Section is full.']], 405);
}
$enrollment = Enrollment::create($request->all());
return response()->json($enrollment, 200);
}
return response()->json(['errors' => ['message' => 'Student is already enrolled in this session.']], 405);
}
Any help would be greatly appreciated.
As you have figured out the answer on your own.
The reason for using create method is because it triggers the event on the model. Same goes for update method.
Following is the update method under the hood:
/**
* Update the model in the database.
*
* #param array $attributes
* #param array $options
* #return bool
*/
public function update(array $attributes = [], array $options = [])
{
if (! $this->exists) {
return false;
}
return $this->fill($attributes)->save($options);
}
And the save method has these lines of code:
if ($this->fireModelEvent('saving') === false) {
return false;
}
Similarly, create method works. That's why insert doesn't trigger the event on model and you had to use create method.
Turns out i just need to use create method.
public function setStudentsSection(Request $request)
{
$enrollments = Enrollment::whereIn('student_id', $request->students)->where('session_id', $request->session_id)->get();
$program_section = ProgramSection::withCount('students')->find($request->program_section_id);
if(($program_section->students_count + count($enrollments)) <= $program_section->max_students) {
foreach($enrollments as $enrollment) {
$response = StudentSection::create([
'student_id' => $enrollment->student_id,
'enrollment_id' => $enrollment->id,
'section_id' => $request->program_section_id,
'created_at' => Carbon::now()
]);
return $response;
}
}
return response()->json(['errors' => ['message' => 'Selected Section is full.']], 405);
}
Laravel observer doesn't work on bulk objects, it only works with single object.
So when you use create() function it will trigger the observer.

Laravel - How to update Input Array without deleting Sales Detail

In my Laravel-8 project, I have this controller for Input Field Array Update.
Controller:
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
SaleDetail::where('sale_id', $sale->id)->delete();
foreach ($data['invoiceItems'] as $item) {
$details = [
'sale_id' => $sale->id,
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$saleDetail = new SaleDetail($details );
$saleDetail->save();
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
In the form, the user can add more Sales Detail or remove.
Some of the SaleDetail fields are being used somewhere else.
Is there a way to update the input field array without deleting the SaleDetail as shown in what I did here:
SaleDetail::where('sale_id', $sale->id)->delete();
Thanks
I've tried to restructure your code so that's easier to edit. I've left some comments. I can really recommend refactoring.guru. There you will find many ways to improve your code so that it is more extensible, maintainable and testable. If you have any questions, please feel free to ask.
class Sale extends Model
{
// Use a relationship instead of building your own query
public function details() {
return $this->hasMany(SaleDetail::class);
}
}
class SaleDetail extends Model
{
// Use a computed property instead of manually calculating total price
// You can access it with $saleDetail->totalPrice
public function getTotalPriceAttribute() {
return $this->price * $this->quantity;
}
}
class UpdateSaleRequest extends Request
{
public function authorize() {
return true;
}
protected function prepareForValidation() {
$this->merge([
// Create a Carbon instance by string
'date' => Carbon::make($this->date)
]);
}
public function rules() {
// Your validation rules
// Please also validate your invoice items!
// See https://laravel.com/docs/8.x/validation#validating-arrays
}
}
// We let Laravel solve the sale by dependency injection
// You have to rename the variable name in ihr web.php
public function update(UpdateSaleRequest $request, Sale $sale)
{
// At this point, all inputs are validated!
// See https://laravel.com/docs/8.x/validation#creating-form-requests
$sale->update($request->validated());
// Please ensure, that all properties have the same name
// In your current implementation you have price = cost, be consistent!
foreach($request->input('invoiceItems') as $invoiceItem) {
// How we can consider that a detail is already created?
// I assume that each item_id will only occur once, otherwise you'll
// place the id of each detail in your update form (e.g. in a hidden input)
$candidate = $sale->details()
->where('item_id', $properties['item_id'])
->first();
if($candidate) {
$candidate->update($properties);
} else {
$sale->details()->create($properties);
}
}
// A JWT-Exception should not be necessary, since your authentication
// will be handled by a middleware.
return response()->json($sale);
}
I have not tested the code, few adjustments may be needed.
Laravel has a method called updateOrCreate as follow
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
That means you could do some thing like
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
foreach ($data['invoiceItems'] as $item) {
$details = [
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$sale->saleDetail()->updateOrCreate([
'sale_id' => $sale->id
], $details);
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
I would encourage you to refactor and clean up your code.You can also read more about it here https://laravel.com/docs/8.x/eloquent#upserts

Call to a member function pro_events() on null

Hello I'm trying to fix this issue where the error
Call to a member function pro_events() on null
would not happen all the time. I'm not sure what's causing this.
Any idea how to prevent this error in case it would appear again? We just saw this error in the log but can't duplicate the issue. Thanks!
Organizer.php model
class Organiser extends MyBaseModel
{
protected $rules = [
'org_name' => ['required', 'unique:organisers,name'],
'email' => ['required', 'email', 'unique:organisers'],
'organiser_logo' => ['mimes:jpeg,jpg,png', 'max:10000'],
'terms_agreed' => ['required'],
'org_tags' => ['required'],
];
protected $messages = [
'org_name.required' => 'You must at least give a name for the event organiser.',
'org_name.unique' => 'Your desired organisation name is already taken.',
'organiser_logo.max' => 'Please upload an image smaller than 10Mb',
'organiser_logo.size' => 'Please upload an image smaller than 10Mb',
'organiser_logo.mimes' => 'Please select a valid image type (jpeg, jpg, png)',
];
public function pro_events() {
return $this->hasMany(\App\Models\ProEvent::class)->orderBy('event_date', 'asc');
}
SomeController.php
public function showPackageHome($organiser_id, $event_dummy = null, $event_id = null, $the_country = null, $package_type_id = null, $package_category_id = null)
{
date_default_timezone_set('Europe/London');
$now = Carbon::now();
$cacheKey = md5(vsprintf('%s.%s', [
$organiser_id,
'organiser_cache'
]));
$organiser = Cache::remember($cacheKey, 10, function() use ($organiser_id) {
return Organiser::find($organiser_id);
});
$cacheKey = md5(vsprintf('%s', [
'event_list_cache'
]));
$events = Cache::remember($cacheKey, 1, function() use ($organiser, $now) {
return $organiser->pro_events()
->where("event_status", "live")
->whereDate('event_date', '>=', $now->format("Y-m-d"))
->orderBy('event_date', 'asc')
->get();
});
ProEvent.php model
class ProEvent extends MyBaseModel
{
use SoftDeletes;
protected $table = 'pro_events';
You must go up in the call stack to find where the organizer_id comes from. A possible reason would be that the admin/loggedIn person may change the route manually and try to load an organizer that doesn't exist.
If you have a route like /organizer/{organizer_id}, then you must make sure that the provided ID actually exists in db. One way to do it would be to use findOrFail instead of find and to catch the thrown exception in case the organizer doesn't exist.
public function showPackageHome($organiser_id, $event_dummy = null, $event_id = null, $the_country = null, $package_type_id = null, $package_category_id = null)
{
date_default_timezone_set('Europe/London');
$now = Carbon::now();
$cacheKey = md5(vsprintf('%s.%s', [
$organiser_id,
'organiser_cache'
]));
$organiser = Cache::remember($cacheKey, 10, function() use ($organiser_id) {
return Organiser::findOrFail($organiser_id);
});
$cacheKey = md5(vsprintf('%s', [
'event_list_cache'
]));
$events = Cache::remember($cacheKey, 1, function() use ($organiser, $now) {
return $organiser->pro_events()
->where("event_status", "live")
->whereDate('event_date', '>=', $now->format("Y-m-d"))
->orderBy('event_date', 'asc')
->get();
});
}
In your controller method you can actually catch the exception and display something to the user:
public function yourControllerMethod(Illuminate\Http\Request $request)
{
// something more here
try {
$events = $this->showPackageHome($request->get('organizer_id'), /* the other parameters */);
return $this->view(..., ['events' => $events]);
} catch (Illuminate\Database\Eloquent\ModelNotFoundException $ex) {
// The organizer couldn't be found
return redirect()->back()->withErrors(['organizer-not-found' => 'The organizer could not be found'])
}
}
i think it's because the instance of Organizer where you called pro_events() function was null.

Is there any efficient method on how to get id of object to my create method

I am creating a web module, and want to get ID of table licensing level two parse into my create method. Hence each ID of level will have a task and the ID need to be stored within my licensing table as a foreign key which reflects ID in Level Two table. How could I solve this, anyone can give me a good suggestion or way on doing this
public function add_show($id)
{
$level = PreLicensingLevelTwo::where('id', $id)->first();
$level->prelicensingtask = PreLicensingTask::where('pre_licensing_level_two_id', $level->id)->with('staff', 'statusdesc', 'prelicensingtaskstaff')->get();
return view('staff.regulatory.statutory.approval.display',compact('level'));
}
public function create()
{
$staff = Staff::pluck('staff_name');
$status = PreLicensingStatus::pluck('status_description', 'id');
return view('staff.regulatory.statutory.approval.create', compact('staff','status'));
}
public function show($id)
{
$one = PreLicensingLevelOne::where('pre_licensing_main_id', $id)->get();
foreach ($one as $key => $license)
{
$license->two = PreLicensingLevelTwo::where('pre_licensing_level_one_id', $license->id)->get();
}
$rendered = view('staff.regulatory.statutory.approval.show')->with('one', $one)->render();
return response()->json(array('status' => 1, 'tableData' => $rendered));
}
With help from my working collegue this is how i able to solve the question i asked
public function store(Request $request)
{
$this->validate($request, [
'task_title' => 'required',
'task_description' => 'required',
'task_due_date' => 'required',
]);
$leveltwo = PreLicensingLevelTwo::find($request->input('pre_licensing_level_two_id'));
$prelicensingtask = new PreLicensingTask;
$prelicensingtask->task_title =$request->input('task_title');
$prelicensingtask->task_description =$request->input('task_description');
$prelicensingtask->task_due_date =$request->input('task_due_date');
$prelicensingtask->created_by_staff_id = Auth::user()->ref_user->staff_id;
$prelicensingtask->status = $request->input('status');
$prelicensingtask->pre_licensing_level_two_id = $leveltwo->id;
$prelicensingtask->pre_licensing_level_one_id = $leveltwo->pre_licensing_level_one_id;
$prelicensingtask->pre_licensing_main_id = $leveltwo->pre_licensing_main_id;
$prelicensingtask->centre_id = Auth::user()->ref_user->centre_id;
$prelicensingtask->save();
return redirect()->back();
}

How To Create Conditional for Unique Validation (UPDATE/PATCH) on Form Request

I'm trying to get my controller cleaner by moving 'validation request' into a form request called 'BookRequest'.
The problem is on the update process, I try to create a condition to check, if it PATCH or POST with the following codes
MyRequest.php
public function rules()
{
// Check Create or Update
if ($this->method() == 'PATCH')
{
$a_rules = 'required|string|size:6|unique:books,column2,' .$this->get('id');
$b_rules = 'sometimes|string|size:10|unique:books,column3,' .$this->get('id');
}
else
{
$a_rules = 'required|string|size:6|unique:books,column2';
$b_rules = 'sometimes|string|size:10|unique:books,column3';
}
return [
'column1' => 'required|string|max:100',
'column2' => $a_rules,
'column3' => $b_rules,
'column4' => 'required|date',
'column5' => 'required|in:foo,bar',
'column6' => 'required',
'column7' => 'required',
'column8' => 'required',
];
}
.$this->get('id') it failed, the form still treat the unique on the update.
Controller#update
public function update($id, BookRequest $request)
{
$book = Book::findOrFail($id);
$input = $request->all();
$book->update($request->all());
return view('dashboards.book');
}
Controller#edit
public function edit($id)
{
$book = Book::findOrFail($id);
return view('dashboards.edit', compact('book'));
}
Controller#create
public function create()
{
return view('dashboards.create');
}
Controller#store
public function store(BookRequest $request)
{
$input = $request->all();
$book = Book::create($input);
return redirect('dashboards/book/index');
}
I try the alternative .$book->id, and it throw me an ErrorException Undefined variable: book
Any suggestion? I'm using Laravel 5.2 by the way
You are using book as your route parameter but trying to get with id. try this-
if ($this->method() == 'PATCH')
{
$a_rules = 'required|string|size:6|unique:books,column2,' .$this->route()->parameter('book');
$b_rules = 'sometimes|string|size:10|unique:books,column3,' .$this->route()->parameter('book');
}
Hope it helps :)

Resources