Yii REST API - using multiple joins in query - php

I have a yii app with a rest api. I want to get a post object complete with all comments for that post as well as the user object for the creator of the post.
Also in the comments I want the user object for each user who left a comment.
So one post with post user and many comments each with user.
The post controller serving the api request looks like this:
public function actionIndex(){
$post = Post::find()
->joinWith('user)
->joinWith('comments')
->asArray()
->all();
}
return $post;
Then the models for user and comment:
public function getUser()
{
return $this->hasOne(User::className(), ['id' => 'created_by'])->innerJoinWith('profile p1');
}
public function getComments()
{
return $this->hasMany(Comment::className(), ['object_id' => 'id'])->leftJoin('user u2', 'u2.id = comment.created_by');
}
The user for the post returns fine. The comments are returned. But there is no returned user for each comment. I feel like the left join in the getComments() method should pull in the user. What's missing?
I get back something like this:
{
"id":"1",
"message":"this is a post",
"user":
[{
"id:11",
"name":"bob smith"
}],
"comments":
[{
"id:21",
"remark":"this is a comment"
}]
}
and I want to get back this:
{
"id":"1",
"message":"this is a post",
"user":
[{
"id:11",
"name":"bob smith"
}],
"comments":
[{
"id:21",
"remark":"this is a comment",
"user":
[{
"id:41",
"name":"jane doe"
}]
}]
}
UPDATE: If I change getComments() from leftJoin to innerJoinWith like this:
public function getComments()
{
return $this->hasMany(Comment::className(), ['object_id' => 'id'])->innerJoinWith('user u2', 'u2.id = comment.created_by');
}
...then I get the properly formatted output BUT it only includes posts that contain comments.

I have not checked, but you could try:
$post = Post::find()
->joinWith('user')
->joinWith(['comments' => function($q) {
$q->joinWith(['user']);
}])
->asArray()
->all();
}

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, 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

In Laravel, how can i return a specific *column* from a *table* inside my API Controller ? I'm using Laravel 5

Question
How can i return a specific column from a table [within the API Controller] ? I used pluck but it removed the column name from the output. I need the column name to be included as well. The $product table i refer(Referred in my code below) is just a table with product stuffs like name, price, discount rate etc..
Brief description
Present API response sample -
{
"data" : [
"Graham",
"Marina Philip",
"David Doomer",
],
"message" : "",
"success" : true
}
Expected response -
[
{
"name": "Graham",
},
{
"name": "Marina Philip",
}, {
"name": "David Doomer",
},
]
API Route from the APIController :
Route::resource('searchlist', 'API\SearchlistAPIController');
Index function from my SearchlistAPIController.php [Specific function]
public function index(Request $request)
{
try{
$this->productRepository->pushCriteria(new RequestCriteria($request));
$this->productRepository->pushCriteria(new LimitOffsetCriteria($request));
$this->productRepository->pushCriteria(new ProductsOfFieldsCriteria($request));
if($request->get('trending',null) == 'week'){
$this->productRepository->pushCriteria(new TrendingWeekCriteria($request));
}
else{
$this->productRepository->pushCriteria(new NearCriteria($request));
}
$products = $this->productRepository->all();
} catch (RepositoryException $e) {
return $this->sendError($e->getMessage());
}
//Here i've got the value of the table $Product with a bunch of columns from my database..
$sendinger = $products->pluck('name');
//I'm trying to filter the columns send here. But i lost the column name as well.
return $this->sendResponse($sendinger->toArray(),'');
}
Contents of my sendResponse method:
public function sendResponse($result, $message) { return Response::json(ResponseUtil::makeResponse($message, $result)); }
Also, how can i remove this from my Json response ? :
"message" : "",
"success" : true
Off the top of my head, I believe $products->pluck('name')->toArray() will create return an indexed array. E.g. [ "Graham", "Marina Philip", "David Doomer" ]
What you could do is...
$sendinger = $products->pluck('name')->map(function($name) {
return [ 'name' => $name ];
});
return $this->sendResponse($sendinger->toArray(),'');
More about mapping here: https://laravel.com/docs/7.x/collections#method-map
Not tested but should steer you in the right direction.
For your other question (removing "success" and "message" from the response)...
It appears you are using the Laravel Generator ResponseUtil class which is adding the additional parameters. See source here: https://github.com/InfyOmLabs/laravel-generator/blob/7.0/src/Utils/ResponseUtil.php
Simply replace the last line:
return $this->sendResponse($sendinger->toArray(),'');
with
return response()->json( $sendinger->toArray() );
and that should do the trick!

How to update foreign key ide value in Laravel Eloquent?

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

Laravel 5.1 and Fractal: including pivot table data on the transformer

Tables: contact, company and a relationship table with a custom pivot attribute company_contact (company_id, contact_id, is_main)
Company and Contact have a many to many relationship (belongsTo on both models).
Expected output when I retrieve the contacts of a company:
{
"data": [
{
"id": 1,
"name": "JohnDoe",
"is_main": false
},
{
"id": 2,
"name": "JaneDoe",
"is_main": true
}
]
}
Expected output when I retrieve the contact list with ?include=companies:
{
"data": [
{
"id": 1,
"name": "John Doe",
"companies": {
"data": [
{
"id": 501,
"name": "My Company",
"is_main": true
},
{
"id": 745,
"name": "Another Company",
"is_main": false
}
]
}
},
{
"id": 2,
"name": "Jane Doe",
"companies": {
"data": [
{
"id": 999,
"name": "Some Company",
"is_main": true
}
]
}
}
]
}
What's the best way of adding the pivot table attribute? It doesn't seem very clean to add is_main on the company transformer if the attribute is set.
For the first example I was thinking about using parameters ?include=company_relationship:company_id(1) with something like:
public function includeCompanyRelationship(Contact $contact, ParamBag $params) {
// .. retrieve the pivot table data here
$is_main = $company->is_main;
// but now I would need another transformer, when what I actually want is to push the value on the main array (same level)
return $this->item(??, ??);
}
I understand how to retrieve pivot data (related: Laravel 5.1 - pivot table between three tables, better option?) but not the best way of adding it in the https://github.com/thephpleague/fractal Transformer logic.
I already have a ContactTransformer and CompanyTransformer but if I add is_main to the CompanyTransformer all the calls I make (related or not to contacts) will also expect that attribute.
If I'm reading you correctly, you can utilize a single CompanyTransformer to handle whether you wish to have the is_main property set, but only if a $contact parameter is passed in to it's constructor, something along these lines:
class CompanyTransformer extends TransformerAbstract
{
public function __construct(Contact $contact = null)
{
$this->contact = $contact;
}
public function transform(Company $company)
{
$output = [
'id' => $company->id,
'name' => $company->name,
];
if($this->contact) {
// This step may not be necessary, but I don't think the pivot data
// will be available on the $company object passed in
$company = $this->contacts->find($company->id);
// You may have to cast this to boolean if that is important
$output['is_main'] = $company->pivot->is_main;
}
return $output;
}
}
Then in your includeCompanyRelationship just pass in the new CompanyTransformer with the parameter:
public function includeCompanyRelationship(Contact $contact)
{
$companies = $contact->companies;
return $this->collection($companies, new CompanyTransformer($contact));
}
This should work whether you're calling your companies endpoint directly, or calling the contact's endpoint while embedding the company relationship data.
I know this is older, but I just ran into this issue. Here is how I resolved it.
Added the withPivot to the relationships, in my case category and users.
In the CategoryTransformer, I defined my includeUsers method:
/**
* Include Users
* #param Category $category
* #return \League\Fractal\Resource\Collection
*/
public function includeUsers(Category $category)
{
# Just Add withPivot Here.
$users = $category->users()->withPivot('role')->get();
return $this->collection($users, new UserTransformer);
}
Then in your UserTransformer class, on the transform() method:
public function transform($user)
{
return [
'username' => $user['username'],
'lastname' => $user['lastname'],
// ... truncated
'role' => isset($user['pivot']) ? $user['pivot']['role'] : null,
]
}
Then when I call my api for categories and include users I get:
{
"data": [
{
"name": "Network",
"description": "some description of category here.",
"users": {
"data": [
{
"id": 1,
"username": "jborne",
"firstname": "Jason",
"lastname": "Borne",
"title": "Department Head",
"company": "Borne Inc",
"email": "jason#somedomain.com",
"display_name": "Jason Borne",
"mobile": "555-5555",
"active": true,
"role": "IM",
}
]
}
}
]
}
As you see, I get the role relationship I wanted from the pivot. Otherwise you just get null for that field. Still not ideal, but much less messy in my opinion.

Categories