Exists validations with multiple tables - laravel

Laravel version (7.x).
I have three tables companies, complaint_types & complaints.
The complaints are associated with a company.
When a complaint is assigned to an employee then only he/she can see and enter the complaint status after the visit. Before entering the comments the complaint must be validated, otherwise the comment must not be allowed to enter, which is handled upon submitting the form via a hidden field called complaint_id.
I have added this validation because I don't want anyone opening the inspect tool, playing with the values and causing the application an error.
Tables:
companies: -> complaint_types: -> complaints:
|-- id |-- id |-- id
|-- ... |-- company_id |-- complaint_type_id
|-- ... |-- ...
Complaint.php:
public function complaintType()
{
return $this->belongsTo(ComplaintType::class);
}
ComplaintType.php:
public function company()
{
return $this->belongsTo(Company::class);
}
public function complaints()
{
return $this->hasMany(Complaint::class);
}
ComplaintController.php:
private function validate($data)
{
# variables
$rules = [
'complaint_id' => [
'required',
'exists:complaints,id,complaintType.company_id,' . session()->get('COMPANY')
],
-- OR --
'complaint_id' => [
'required',
'exists:complaints,id,complaint_types.company_id,' . session()->get('COMPANY')
],
...
];
$messages = [
'complaint_id.required' => '`Complaint` - Required<br>',
'complaint_id.exists' => '`Complaint` could not be identifed<br>',
...
];
return Validator::make($data, $rules, $messages);
}
Error
Column not found: 1054 Unknown column 'complaintType.company_id' in 'where clause' (SQL: SELECT COUNT(*) as aggregate FROM complaints WHERE id = 3 AND complaintType.company_id = 1)

Problem solved, Here is my code:
$rules = [
'complaint_id' => [
'required',
function($attribute, $value, $fail) use ($data) {
$Complaint = Complaint::where('id', $value)
->whereHas('complaintType', function($query) use($data) {
return $query->where('company_id', session()->get('COMPANY'));
})->count();
if($Complaint <= 0)
$fail('`Complaint` could not be identified');
}
],
I found the reference here How can I add a join to a custom exists rule for a laravel validator?

You should specify relationship with primary key like:
public function complaints(){
return $this->hasMany(Complaint::class, 'complaint_type_id','id');
}

Related

Model appends including entire relationship in query

Edit: I was able to see where the relations are being included in my response, but I still don't know why.
On my Customer model, I have:
protected $appends = [
'nps',
'left_feedback',
'full_name',
'url'
];
The accessors are as follows:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews->count() > 0) {
return $this->reviews->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
if ($this->reviews && $this->reviews->count() > 0 && $this->reviews->first()->feedback != null) {
return "Yes";
} else {
return "No";
}
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location;
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
So if I comment out the $appends property, I get the response I originally wanted with customer not returning all the relations in my response.
But I do want those appended fields on my Customer object. I don't understand why it would include all relations it's using in the response. I'm returning specific strings.
So is there a way to keep my $appends and not have all the relations it's using in the accessors from being included?
Original Question:
I am querying reviews which belongsTo a customer. I want to include the customer relation as part of the review, but I do not want to include the customer relations.
$reviews = $reviews->with(['customer' => function($query) {
$query->setEagerLoads([]);
$query->select('id', 'location_id', 'first_name', 'last_name');
}]);
$query->setEagerLoads([]); doesn't work in this case.
I've tried $query->without('location'); too, but it still gets included
And I should note I don't have the $with property on the model populated with anything.
Here is the Review model relation:
public function customer() {
return $this->belongsTo('App\Customer');
}
Here is the Customer model relation:
public function reviews() {
return $this->hasMany('App\Review');
}
// I dont want these to be included
public function location() {
return $this->belongsTo('App\Location');
}
public function reviewRequests() {
return $this->hasMany('App\ReviewRequest');
}
In the response, it will look something like:
'review' => [
'id'=> '1'
'customer => [
'somecol' => 'test',
'somecolagain' => 'test',
'relation' => [
'relation' => [
]
],
'relation' => [
'somecol' => 'sdffdssdf'
]
]
]
So a chain of relations ends up being loaded and I don't want them.
As you said in one comment on the main question, you are getting the relations due to the appended accessors.
Let me show you how it should be done (I am going to copy paste your code and simply edit some parts, but you can still copy paste my code and place it in yours and will work the same way but prevent adding the relations) and then let me explain why is this happening:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews()->count() > 0) {
return $this->reviews()->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
return $this->reviews()->count() > 0 &&
$this->reviews()->first()->feedback != null
? "Yes"
: "No";
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location()->first();
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
As you can see, I have changed any $this->relation to $this->relation()->first() or $this->relation->get().
If you access any Model's relation as $this->relation it will add it to the eager load (loaded) so it will really get the relation data and store it in the Model's data so next time you do $this->relation again it does not have to go to the DB and query again.
So, to prevent that, you have to access the relation as $this->relation(), that will return a query builder, then you can do ->count() or ->exists() or ->get() or ->first() or any other valid query builder method, but accessing the relation as query builder will prevent on getting the data and store it the model (I know doing ->get() or ->first() will get the data, but you are not directly getting it through the model, you are getting it through the query builder relation, that is different).
This way you will prevent on storing the data on the model, hence giving you problems.
You can also use API Resources, it is used to map a Model or Collection to a desired output.
One last thing, if you can use $this->relation()->exists() instead of $this->relation()->count() > 0 it will help on doing it faster, mostly any DB is faster on looking if data exists (count >= 1) than really counting all the entries it has, so it is faster + more performant on using exists.
Try :
$review->with(‘customer:id,location_id,first_name,last_name’)->get();
Or :
$review->withOnly(‘customer:id,location_id,first_name,last_name’)->get();

Laravel Excel 3.1 throws a "property doesnt have a default value" error despite importing successfully

I am using the Laravel Excel package to handle bulk uploads. Whilst Im able to get the data to upload successfully, my web console indicates and error that 'staff_id' doesn't have a default value. I have tried to catch this as an exception but this does not get triggered. I am using the ToModel import as indicated below
class EmployeesImport implements ToModel, WithHeadingRow
{
public function model(array $row)
{
try {
return new Employee([
'staff_id' => $row['staff_id'],
'first_name' => $row['first_name'],
'middle_name' => $row['middle_name'],
'last_name' => $row['last_name'],
'national_id' => (string) $row['national_id'],
'department_id' => 1,
]);
} catch (\Exception $e) {
dd($e->getMessage(), $row);
}
}
}
The CSV Im importing has the following structure
Within my controller, I have this to exceute the upload/import
Excel::import(new EmployeesImport(), request()->file('bulk'));
And finally, this is my Employees Model, showing the fillable fields
class Employee extends Model
{
use SoftDeletes;
protected $table = "employees";
protected $fillable = [
"staff_id", "first_name", "middle_name", "last_name", "national_id", "department_id", "avatar"
];
}
(One last thing) In case it may hold relevance - my migration file's up method
public function up()
{
Schema::create('employees', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('staff_id')->unique();
$table->string('first_name');
$table->string('middle_name')->nullable();
$table->string('last_name');
$table->string('national_id')->unique();
$table->unsignedBigInteger('department_id');
$table->longText('avatar')->nullable();
$table->timestamps();
$table->softDeletes();
//Foreign keys
$table->foreign('department_id')->references('id')->on('departments')->onDelete('cascade');
});
}
According to the documentation you can catch the errors at the end
https://docs.laravel-excel.com/3.1/imports/validation.html#gathering-all-failures-at-the-end
Gathering all failures at the end
You can gather all validation
failures at the end of the import, when used in conjunction with Batch
Inserts. You can try-catch the ValidationException. On this exception
you can get all failures.
Each failure is an instance of Maatwebsite\Excel\Validators\Failure.
The Failure holds information about which row, which column and what
the validation errors are for that cell.
try {
// import code
} catch (\Maatwebsite\Excel\Validators\ValidationException $e) {
$failures = $e->failures();
foreach ($failures as $failure) {
$failure->row(); // row that went wrong
$failure->attribute(); // either heading key (if using heading row concern) or column index
$failure->errors(); // Actual error messages from Laravel validator
$failure->values(); // The values of the row that has failed.
}
}
you make your models like that:
protected $fillable = [
"staff_id", "first_name", "middle_name", "last_name", "national_id", "department_id", "avatar"
];
and your row like this:
return new Employee([
'staff_id' => $row['staff_id'],
'first_name' => $row['first_name'],
'middle_name' => $row['middle_name'],
'last_name' => $row['last_name'],
'national_id' => (string) $row['national_id'],
'department_id' => 1,
just matching the $row and $fillable, i mean in your $row the "avatar" must have a value to fill the $fillable,
or you can erase the "avatar" from you fillable

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();
}

Laravel Map DB Column Names Using Proper Convention to Actual DB Column Names in Model

We're building a portal to replace part of an existing application as step one, but the DB schema holds to absolutely no conventions. Aside from the lack of any constraints, indexes, etc the names of columns are not descriptive and not snake-cased.
Is it possible to map DB table column names so that the portal uses proper descriptive and snake-cased column names like first_name but writes to the actual database column first to at least have the portal be a first step towards cleaning up the tech debt?
For example, similar to how the table name (Model::table) can be set if the table name doesn't follow convention:
Example
private $columns = [
// convention => actual
'first_name' => 'first',
'last_name' => 'last',
'mobile_phone' => 'phone',
'home_phone' => 'otherPhone', // seriously!?
];
I've looked through Model and the HasAttributes trait, but I'm still hoping that this might exist, or someone has found a way to do this as a temporary solution.
You can create a parent class for all your models:
abstract class Model extends \Illuminate\Database\Eloquent\Model {
protected $columns = [];
public function attributesToArray()
{
$attributes = parent::attributesToArray();
foreach ($this->columns as $convention => $actual) {
if (array_key_exists($actual, $attributes)) {
$attributes[$convention] = $attributes[$actual];
unset($attributes[$actual]);
}
}
return $attributes;
}
public function getAttribute($key)
{
if (array_key_exists($key, $this->columns)) {
$key = $this->columns[$key];
}
return parent::getAttributeValue($key);
}
public function setAttribute($key, $value)
{
if (array_key_exists($key, $this->columns)) {
$key = $this->columns[$key];
}
return parent::setAttribute($key, $value);
}
}
Then override $columns in your models:
protected $columns = [
'first_name' => 'first',
'last_name' => 'last',
'mobile_phone' => 'phone',
'home_phone' => 'otherPhone',
];
The proper way is to use accessors and mutators.
Defining An Accessor
public function getFirstNameAttribute() {
return $this->first;
}
Then, you can access the value by $model->first_name.
Defining A Mutator
public function setFirstNameAttribute($value) {
$this->attributes['first'] = $value;
}
Then, you can mutate the value for example:
$model->first_name = 'first_name';
$model->save();

Validate single rows only when they are present Laravel

So i have a model named Customer.
The db the Customer looks like this:
id, name, lastName, personal, address, zip, location, phones, emails updated_at, created_at
Emails and Phones is special rows because they are store as an json object example
['john#doe.com', 'some#othermail.com', 'more#mails.com']
I use the Customer Model to store the validation rules and custom messages like this
<?php
class Customer extends BaseModel
{
public function validationRules()
{
return array(
'name' => 'required|max:255',
'lastName' =>'max:255',
'personal'=> 'integer',
'location' => 'max:255',
'address' => 'max:255',
'zip' => 'required|integer',
'phones' => 'betweenOrArray:8,10|required_without:emails',
'emails' => 'emailOrArray'
);
}
public function validationMessages()
{
// returns Validation Messages (its too much to write down)
}
}
The OrArray Rule is found here https://stackoverflow.com/a/18163546/1430587
I call them through my controller like this
public function store()
{
$customer = new Customer;
$messages = $customer->validationMessages();
$rules = $customer->validationRules();
$input['name'] = Input::get('name');
$input['lastName'] = Input::get('lastName');
$input['personal'] = preg_replace("/[^0-9]/", "", Input::get('personal'));
$input['location'] = Input::get('location');
$input['address'] = Input::get('address');
$input['zip'] = Input::get('zip');
$input['emails'] = Input::get('emails');
$input['phones'] = Input::get('phones');
foreach($input['phones'] as $i => $value)
{
$input['phones'][$i] = preg_replace("/[^0-9]/", "", $value);
}
$validator = Validator::make($input, $rules, $messages);
}
This all works just fine, but I want to be able to PUT/PATCH request to update a single row.
But the validationRules has Required on certain fields so when its not present i cant update that single row. Without getting an error that the other fields (witch I'm not posting) is required.
How would I best approach this ?
You should get that instance of the model that represent the row you want to edit, that's why the resource controller's update method has a parameter that is the resource you want to edit.
public function update($resourceId) {
$customer = Customer::where('id', '=', $resourceId);
}
Now this customer has all the attributes you set before, so you can access them like:
$customer->name;
$customer->lastName;
So when you valide the values you can use the existing values in your validator where the input is empty:
$input['name'] = (Input::get('name')) ? (Input::get('name')) : $customer->name;
Or a prettier solution with the elvis operator:
$input['name'] = (Input::get('name')) ?: $customer->name;
I came up with another solution to this problem that works very well and its much cleaner.
$customer = Customer::find($id);
$input = Input::except('_method', '_token');
$customer->fill($input);

Resources