How to update foreign key ide value in Laravel Eloquent? - php

I'm quite new to Laravel and I was not able to find the answer to this problem neither on Laravel docs, nor here.
I guess it's just a matter of how to search for it, cause I'm pretty sure it's a common case.
I have two models in relationship (this is a simplified case), I retrieve the info I need through a Resource file, but I'm not able to understand how to properly store or update info.
Here's a code example:
Models\Company.php
class Company extends Model
{
protected $fillable = [
'name', 'blablabla', 'country_id', 'blablabla2',
];
public function country() {
return $this->belongsTo(Country::class);
}
}
Models\Country.php
class Country extends Model
{
protected $fillable = [
'code', 'name', 'prefix', 'tax_code_id',
];
public function companies() {
return $this->hasMany(Company::class);
}
}
Then I have a CompanyController file to manage API requests:
Controllers\CompanyController.php
class CompanyController extends BaseController
{
public function index()
{
$companies = Company::paginate();
$response = CompanyResource::collection($companies)->response()->getData(true);
return $this->sendResponse($response, 'Companies retrieved successfully');
}
public function store(Request $request)
{
$input = $request->all();
$validator = Validator::make($input, $this->validation_rules);
if($validator->fails()){
return $this->sendError('Validation error.', $validator->errors());
}
$company = Company::create($input);
return $this->sendResponse($company->toArray(), 'Company added successfully.');
}
}
...
public function update(Request $request, Company $company)
{
$input = $request->all();
$validator = Validator::make($input, $this->validation_rules);
if($validator->fails()){
return $this->sendError('Validation error.', $validator->errors());
}
$company->update($input);
return $this->sendResponse($company->toArray(), 'Company updated successfully.');
}
And here the CompanyResource I'm using to display info as I need.
Resources/CompanyResource.php
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'blablabla' => $this->blablabla,
'country' => $this->country,
'blablabla2' => $this->blablabla2,
];
}
}
So when retrieving Companies (or single company) I get a nested JSON:
{
"id": "1",
"name": "something",
"blablabla": "blablabla",
"country": {
"id": "100",
"code": "MA",
"name": "Mars",
"prefix": "123",
"tax_code_id": "#$%"
},
"blablabla2": "blablabla2"
}
If I create or update a new company I send a payload that has the same structure of what I'm getting above, but if I edit country id value my company model doesn't get it.
PUT Api/companies/1
{
"name": "something",
"blablabla": "blablabla3",
"country": {
"id": "200",
"code": "JU",
"name": "Jupiter",
"prefix": "456",
"tax_code_id": "#=%"
},
"blablabla2": "blablabla2"
}
I'm expecting to update country_id field in companies table for record 1 so that it matches payload (so going from 100 to 200), but it's not happening.
I could edit frontend logic in order to send only country_id in payload since I'm not going to update countries table and all that additional info is redundant, but I'd like to know how to manage it in controller with Laravel.
Would you mind helping me? Thanks in advance.

If you want it to work with the code now, you need to have country_id in the root JSON object you are sending. As this is the way you would fill the id. This is not the best approach in my opinion, but this is why your update is not working at the moment.
{
"name": "something",
"blablabla": "blablabla3",
"country_id": 200,
...
I actually like the approach of sending complete objects. Commonly to fill id's is not good, as it can interfere with the way relations work. Laravel will set your relationships when you associate, if not you are not guaranteed to have the correct relationship after the fill.
Therefor i would fetch out the id and associate the country object with the company. In a logic similar to this.
// only fill non relation fields, fill used as save is needed after associate()
$company->fill($request->only(['name', 'blabla']));
$company->country()->associate(Country::find($request->get('country')['id']));
//associate does not save
$company->save();

I wrote a gist for this years ago that can relate any two models regardless of their relationship type. You just need to supply it with the name of the relationship method: https://gist.github.com/kmuenkel/055f107139d904e30810bf53750d9c6e

Related

Eloquent: Get specific data from pivot table when retrieving all

I have a many to many relationship between Users & Courses with a pivot table, Users_Courses, containing an isComplete value, but i can't seem to retrieve the isComplete value without looping through every user, getting their courses and then looping over every course getting the pivot data.
All the examples i have found is to map the isComplete value to the course with loops, but that seems like it's awfully taxing on the program and i don't really find it appealing which is why I'm making my own question here. If there's already an answer to this that i haven't seen please link it below as i can't seem to find it.
Also, I'm using Laravel-9 and MySQL.
The data structure I'm retrieving right now looks like this:
"data": [
{
"id": 2,
"fname": "name",
"lname": "last name",
"email": "mail#mail.com",
"courses": [
{
"id": 1,
"name": "test_course_1",
"description": "this is a test course for testing"
},
{
"id": 2,
"name": "test_course_2",
"description": "this is also a test course"
},
{
"id": 3,
"name": "test_course_3",
"description": "this course is a test course"
}
]
}
]
I'm searching for a way to retrieve the pivot value isComplete with Eloquent and getting the data with the course itself like this or something like it.
In other words, I want to check if the user has completed the course or not through the pivot table value isComplete as shown in the example below.
"data": [
{
"id": 2,
"fname": "name",
"lname": "last name",
"email": "mail#mail.com",
"courses": [
{
"id": 1,
"name": "test_course_1",
"description": "this is a test course for testing",
"isComplete": 1
},
{
"id": 2,
"name": "test_course_2",
"description": "this is also a test course",
"isComplete": 0
},
{
"id": 3,
"name": "test_course_3",
"description": "this course is a test course",
"isComplete": 0
}
]
}
]
The code i have right now looks like this:
class User extends Authenticatable
{
public function courses()
{
return $this->belongsToMany(Course::class, 'user_courses')
->withPivot('isCompleted');
}
}
class Course extends Model
{
public function users()
{
return $this->belongsToMany(User::class, 'user_courses')
->withPivot('isCompleted');
}
}
class UserController extends Controller
{
public function getUsersById(int $user_id)
{
try {
$users = User::where('id', $user_id)
->with('courses')
->get();
return response()->json([
'success' => true,
'data' => $users
]);
} catch (Throwable $th) {
return response()->json([
'success' => false,
'data' => null,
'message' => $th,
]);
}
}
}
I am aware that it's called isCompleted in the code, but it's also called that in the database. It's a typing error which haven't yet been fixed :D
In other words, I want to check if the user has completed the course or not through the pivot table value isComplete as shown in the example below.
Did you read about filtering using Pivot table columns in the docs: https://laravel.com/docs/9.x/eloquent-relationships#filtering-queries-via-intermediate-table-columns
If you need only completed courses you can call relation as
$users = User::where('id', $user_id)
->with(['courses' => function($query) {
$query->wherePivot('isCompleted', 1); // use quotes if its datatype is enum in database.
}])
->get();
Or you can make customized relations for completed, Incompleted in your Model.
class User extends Authenticatable
{
public function courses()
{
return $this->belongsToMany(Course::class, 'user_courses')
->withPivot('isCompleted');
}
public function completedCourses()
{
$this->courses()->wherePivot('isCompleted', 1);
}
public function InCompleteCourses()
{
$this->courses()->wherePivot('isCompleted', 0);
}
}
And in user controller you can call them as
$users = User::where('id', $user_id)
->with('completedCourses')
->get();
if you want the output to be like the JSON:
$user = User::with("courses")->find(1);
$user = $user->courses->each(
function($course) {
$course->isComplete = $course->pivot->isComplete;
unset($course->pivot);
}
);
this line will retrieve Courses with an object pivot, which includes the columns of your pivot table.
(Example)

Laravel - Update another attribute when updating model

In one of my models I need to update another attribute when two other attributes from the same model are updated.
In my case, using an Accessor is not an option, because in some cases I will need to do a database query looking for full_phone_number.
What is the best to achive the desired result?
This is my model:
class Address extends Model {
protected $fillable = [
'country_code',
'phone_number',
'full_phone_number',
];
}
When I create a new Address, I need the full_phone_number column to be automatically populated:
$address = Address::create([
'country_code' => 55,
'phone_number' => 1199999999
]);
The expected result is:
# Address model on DB
{
"country_code": 55,
"phone_number": 1199999999,
"full_phone_number": 551199999999
}
When I update country_code or phone_number from an Address, I need the full_phone_number column to be automatically updated:
$address->update([
'phone_number' => 1188888888
]);
The expected result is:
# Address model on DB
{
"country_code": 55,
"phone_number": 1188888888,
"full_phone_number": 551188888888
}
I got the desired result using events, but I don't know if this is the best way to handle this case. The same could be converted to an observer.
class Address extends Model {
protected $fillable = [
'country_code',
'phone_number',
'full_phone_number',
];
protected static function boot()
{
parent::boot();
static::updating(function ($address) {
$address->full_phone_number = $address->country_code . $address->number;
});
}
}

Laravel, Follow-Unfollow System Issue

I'm developing an api for a social media like. This app has a follow-unfollow system. I did my follower list and follower list query and response . But, I wanna add follow info(boolean) in response. How to solve clear this problem? (Sorry for my bad English)
I want this response:
"data": [
{
"user_id": 3,
"username": "example",
"name": "Example",
"photo": "default.jpg",
"is_following": true
}
]
Current response:
"data": [
{
"user_id": 3,
"username": "example",
"name": "Example",
"photo": "default.jpg",
}
]
User model in follow relationship:
function follows(){
return $this->belongsToMany(self::class,"follows","followed_id","following_id")->select("users.id","username","name","photo");
}
function followers(){
return $this->belongsToMany(self::class,"follows","following_id","followed_id")->select("users.id","username","name","photo");
}
My Resource:
public function toArray($request)
{
return [
"user_id" => $this->id,
"username" => $this->username,
"name" => $this->name,
"photo" => $this->photo(),
"is_following" =>
];
}
In your User model define an accessor that return true/false with your "following" logic.
public function getIsFollowingAttribute()
{
if (condition)
return true;
return false;
}
After this add this to User model to include it in array and JSON representations
protected $appends = ['is_following'];
Read this
I asked this kind of question the other day and I was trying to do it with two tables, but a friend said that I could solve it with one table. If you wish, you can look at the location and look at the problem and its solution.
Laravel API JSON customization and table relationship

How to disable or remove updated_At and Created_at when returning data

I'm trying to remove created_at and updated_at before returning response to api . I want to removie these field from both Place_Type and Places
how can I do that? :
I tried : unset(placetypes) but it didn't work
This is my Code:
public function places()
{
$placeType = PlaceType::with('places')->where('id', 1)->get();
return response()->json(['placeType' => $placeType]);
}
The request result:
"placeType": [
{
"id": 1,
"name": "Moriah O'Conner",
"icon": "https://picsum.photos/200/300",
"status": 1,
"created_at": "2019-12-14 18:23:19",
"updated_at": "2019-12-14 18:23:19",
"places": [
{
"id": 1,
"name": "Linda Leffler",
"description": "Alice's shoulder, and it set to work, and very soon came to ME, and told me he was in the air. She did it so VERY remarkable in that; nor did Alice think it would feel very queer to ME.' 'You!' said.",
"icon": "https://picsum.photos/200/300",
"image_name": "https://picsum.photos/200/300",
"rating": 2,
"longitude": -53.389979,
"latitude": 19.633458,
"availability": 1,
"status": 1,
"place_type_id": 1,
"created_at": "2019-12-14 18:23:19",
"updated_at": "2019-12-14 18:23:19"
},
{
"id": 2,
"name": "Lauren Cartwright",
"description": "I should say \"With what porpoise?\"' 'Don't you mean by that?' said the King. 'I can't remember half of anger, and tried to look at it!' This speech caused a remarkable sensation among the leaves.",
"icon": "https://picsum.photos/200/300",
"image_name": "https://picsum.photos/200/300",
"rating": 1,
"longitude": -38.117034,
"latitude": -32.248637,
"availability": 1,
"status": 1,
"place_type_id": 1,
"created_at": "2019-12-14 18:23:19",
"updated_at": "2019-12-14 18:23:19"
}...,
}
Add the fields to the $hidden array:
// Model
protected $hidden = ['created_at', 'updated_at'];
There are different methods you can use.
Method 1 : Fetch only required fields from the database
You can use select() method for retrieving only required fields from db. Hence you can omit the unnecessary fields.
$placeType = PlaceType::with(['places' => function ($query) {
$query->select('id', 'name', 'description', 'icon',
'image_name', 'rating', 'longitude', 'latitude',
'availability', 'status', 'place_type_id'); //timestamps excluded
}])
->select('id', 'name', 'icon', 'status') //timestamps excluded
->where('id', 1)
->get();
return response()->json(['placeType' => $placeType]);
This code will output only specified fields both in the parent model (placetype) and child model (places).
If you use these customized select query more than once and writing all field names multiple time is difficult, then you could use model scope like the following.
PlaceType Model
// add all columns from your table
protected $columns = ['id', 'name', 'icon', 'status', 'created_at', 'updated_at'];
public function scopeExclude($query,$value=[])
{
return $query->select( array_diff( $this->columns,(array) $value) );
}
Place Model
// add all columns from your table
protected $columns = ['id', 'name', 'description', 'icon', 'image_name',
'rating', 'longitude', 'latitude', 'availability',
'status', 'place_type_id', 'created_at', 'updated_at'
];
public function scopeExclude($query,$value=[])
{
return $query->select( array_diff( $this->columns,(array) $value) );
}
Then you could remove unwanted fields like the following
$placeType = PlaceType::with(['places' => function ($query) {
$query->exclude(['created_at', 'updated_at']); //exclude fields from Place model
}])
->exclude(['created_at', 'updated_at']) //exclude fields from PlaceType model
->where('id', 1)
->get();
Courtesy : This SO answer by #Razor
Method 2 : Hide your column from serialization where you need
You can hide your column from serialization using laravel's makeHidden() method. In this method after fetching rows with all fields, you are making the specified fields as hidden. [Please note that the excluded variables won't appear on json but may visible on dump].
//get rows with all fileds (except hidden)
$placeType = PlaceType::with('places')->where('id', 1)->get();
//making timestamps hidden in child model's rows
$placeType->places->makeHidden(['created_at','updated_at']);
//making timestamps hidden in parent model's rows
$placeType->makeHidden(['created_at','updated_at']);
return response()->json($placeType);
Courtesy : This SO answer by #sajed
Method 3 : Using Hidden property
If the timestamps are unnecessary in most of the time in the app, you could use the model's hidden property.
PlaceType Model & Place Model
protected $hidden = ['created_at', 'updated_at'];
Hope this will be helpful. 🤗
1) You just need to declare public $timestamps = false; in every model you want to hide it.
2) You can also disable timestamps by removing $table->timestamps() from your migration.
3) Declare protected $hidden = ['created_at', 'updated_at']; in your model.
In case you want to remove timestamps from model, as mentioned before, place this in your Model:
public $timestamps = false;
Also create a migration with following code in the up() method and run it:
Schema::table('your_table', function (Blueprint $table) {
$table->dropTimestamps();
});
You can use $table->timestamps() in your down() method to allow rolling back.
or in model
const UPDATED_AT = null;
const CREATED_AT = null;
Assuming that $placeType is array, you can use this recursive function:
function removeTimeStampValues($array)
{
if(array_key_exists('created_at', $array) && array_key_exists('updated_at', $array)) {
unset($array['created_at']);
unset($array['updated_at']);
}
foreach ($array as $key => $value) {
if(is_array($value)) {
$array[$key] = recursiveRemoveTimeStampValue($value);
}
}
return $array;
}
If you don't want these columns, you can do as others said based on the documentation (under "Timestamps" title).
But if you need these column and just don't want them in the json response, you can use resource. See the documentation.

Laravel parse request into Eloquent model with relations

Im developing a Laravel API that will host and manage the data for a mobile application. The application that I am writing will make AJAX requests and send over JSON data to the Laravel controller.
This works fine for a basic model however I am unable to get this working for nested models. Ill explain:
So I have the following model structure:
Shelf ---- Has Many ----> Boxes ---- Has Many ----> Products
Shelf:
class Shelf extends Model
{
protected $fillable = ['name', 'location'];
public function boxes()
{
return $this->hasMany('App\Box');
}
}
Box:
class Box extends Model
{
protected $fillable = ['name', 'size', 'label'];
public function shelf()
{
return $this->belongsTo('App\Shelf');
}
public function products()
{
return $this->hasMany('App\Product');
}
}
Product:
class Product extends Model
{
protected $fillable = ['name', 'price', 'quantity'];
public function box()
{
return $this->belongsTo('App\Box');
}
}
All my models have validation checking before anything is saved.
I send the following request to my Laravel controller:
{
"name":"Shelf 1",
"location":"LOC1"
"boxes":[{
"name":"box 1",
"size": 130,
"label": "B1",
"products":[
{"name":"Prod1","price":23.00,"quantity":5},
{"name":"Prod2","price":13.00,"quantity":2}
]
}, {
"name":"box 2",
"size": 130,
"label": "B2",
"products":[
{"name":"Prod3","price":3.00,"quantity":15},
{"name":"Prod4","price":7.00,"quantity":8}
]
}, {
"name":"box 3",
"size": 160,
"label": "B3",
"products":[
{"name":"Prod5","price":103.00,"quantity":9},
{"name":"Prod6","price":83.00,"quantity":1}
]
}]
}
When I receive the above data in my Laravel controller i use:
$shelf = new Shelf;
$shelf->fill($request->all());
$shelf->save();
To get all of the data however this will only save the Shelf and not any of the relationships. Is there a common way (or library) I can use to parse the JSON within the Laravel controller?
As far as I'm aware Eloquent doesn't offer anything like that. You would need to save each one individually. May I suggest something like this:
$shelf = Shelf::create($request->only(['name', 'location']));
foreach ( $request->input('boxes') as $box )
{
$box = new Box($box);
$shelf->boxes()->save($box);
$pTemp = [];
foreach ( $box['products'] as $product )
{
$pTemp[] = new Product($product);
}
$box->products()->saveMany($pTemp);
}
Update
To minimise the number of queries run we can loop through all the boxes once, create them then loop through them again to create all the products. You're still having to run one query per box to create the products, but as far as I can see there is no way around that.
$shelf = Shelf::create($request->only(['name', 'location']));
$bTemp = [];
foreach ( $request->input('boxes') as $i => $box )
{
$bTemp[$i] = new Box($box);
}
$shelf->boxes()->saveMany($bTemp);
foreach ( $request->input('boxes') as $i => $box )
{
$pTemp = [];
foreach ( $box['products'] as $product )
{
$pTemp[] = new Product($product);
}
$bTemp[$i]->products()->saveMany($pTemp);
}

Categories