Laravel: Cannot reindex collection's array in eager loading after using unset() - laravel

I have following code:
// User.php
public function groups() {
return $this->belongsToMany(
Group::class,
'group_user',
'user_id',
'group_id',
'id'
);
}
// Group.php
public function users() {
return $this->belongsToMany(
User::class,
'group_class',
'group_id',
'user_id',
'id'
);
}
And in routes/web.php
Route::get('/test', function () {
$me = App\User::first();
$group = App\Group::with('users')->first();
foreach ($group->users as $user_index => $user) {
// Show all users (a.k.a members) of this group, except myself
if ($user->id == $me->id) {
unset($group->users[$user_index]);
}
}
return $group;
}):
Result:
{
"id": 1,
"name": "ABC Group",
"users": { // This should be array right?
"1": { // This should be start with 0
"id": 2,
"name": "...",
"email": "...",
},
"2": { // This should be 1
"id": 3,
"name": "...",
"email": "...",
}
}
}
What I have tried:
#1 Put values() in the end of foreach loop, like:
foreach ($group->users as $user_index => $user) {
// Show all users (a.k.a members) of this group, except myself
if ($user->id == $me->id) {
unset($group->users[$user_index]);
}
$group->users->values(); // Not working
}
#2 Put values() after the foreach loop, like:
Route::get('/test', function () {
$me = App\User::first();
$group = App\Group::with('users')->first();
foreach ($group->users as $user_index => $user) {
// Show all users (a.k.a members) of this group, except myself
if ($user->id == $me->id) {
unset($group->users[$user_index]);
}
}
$group->users->values(); // Still not working
return $group;
}):
Expected result:
{
"id": 1,
"name": "ABC Group",
"users": [ // Array
{ // index 0
"id": 2,
"name": "...",
"email": "...",
},
{ // index 1
"id": 3,
"name": "...",
"email": "...",
}
]
}
Q: How to reindex collection array in eager loading after using unset()?
Thanks in advance

You've got a few things to unpack here that might help you.
First, your query returns a Laravel collection of users attached to the single model Group. Laravel has a bit of magic in the background that allows for array notation as well, but probably easiest to think about this as a collection for your purposes. In some cases, you can translate this to an array using Laravel's toArray() method, something like:
$userArray = $group->users->toArray();
For dropping an index, or in this case a user from the Group's users, take a look at the forget() method, which works on the collection object.
However, I think you may wish to come at this from the reverse... Pull the unwanted index(es) in a single query, rather than having to loop through the collection after the fact. Something like this may be of value to you:
$me = App\User::first();
$group = App\Group::with(['users' => function($query) use($me){
$query->where('users.id', '!=', $me->id);
}])->first();
This query will remove the unwanted user from the collection right out of the database, eliminating the need for additional code, which is what I think you were after.
HTH.

Related

spatie/laravel-medialibrary, filters on getMedia() function not working

I am trying to get media for certain modal as follows:
public function show(Gallery $gallery)
{
$images = $gallery->getMedia('default'); //returns images
// $images = $gallery->getMedia('default', ['name', 'original_url']); //returns empty
return response()->json(['data' => $images]);
}
when not adding filters I get the correct data as follows:
{
"data": {
"686cc1b1-cfdc-425d-9b93-c99290f0d35e": {
"name": "Screenshot from 2022-06-27 23-29-29",
"file_name": "Screenshot-from-2022-06-27-23-29-29.png",
"uuid": "686cc1b1-cfdc-425d-9b93-c99290f0d35e",
"preview_url": "",
"original_url": "http://app.test/storage/media/25/Screenshot-from-2022-06-27-23-29-29.png",
"order": 1,
"custom_properties": [],
"extension": "png",
"size": 10546
}
}
}
but when I use filters I get empty array.
You have to retrieve single media, then you can chain property name
$gallery->getMedia('default')[0]->name;
or
$gallery->getFirstMedia('default')->name;
If you want to return json with name and original_url field for every media, that can be achieved by using API resource, something like this:
$images = $gallery->getMedia('default');
return MediaResource::collection($images);
and in MediaResource class you have:
public function toArray($request)
{
return [
'name' => $this->name,
'original_url' => $this->original_url
];
}

Join pivot has one array

I guys, i have 2 relations tables, and when listing all my messages, is shown the pivot columns relation, but i need to show the data has one array, is there a method in eloquent cant make this happen?
I searched and i no that is possible manipulate with collection methods but i wonder if there is another way.
My Model query is:
public function messages()
{
return $this->belongsToMany(Message::class,'message_users')->withPivot('is_read','sent_at');
}
This is how it is:
{
"data": [
{
"id": 4,
"title": "test",
"body": "<p>test</p>",
"pivot": {
"user_id": 1,
"message_id": 4,
"is_read": 0,
"sent_at": "2019-06-05 12:59:11"
}
}
]
}
This is how i want:
{
"data": [
{
"id": 4,
"title": "test",
"body": "<p>test</p>",
"user_id": 1,
"message_id": 4,
"is_read": 0,
"sent_at": "2019-06-05 12:59:11"
}
]
}
You can do next: in User model write toArray() method as
/**
* Convert the model instance to an array.
*
* #return array
*/
public function toArray(): array
{
$attributes = $this->attributesToArray();
$attributes = array_merge($attributes, $this->relationsToArray());
// Detect if there is a pivot value and return that as the default value
if (isset($attributes['pivot'] && isset($attributes['pivot']['user_id']))) {
$attributes['user_id'] = $attributes['pivot']['user_id'];
$attributes['message_id'] = $attributes['pivot']['message_id'];
$attributes['is_read'] = $attributes['pivot']['is_read'];
$attributes['sent_at'] = $attributes['pivot']['sent_at'];
unset($attributes['pivot']);
}
return $attributes;
}

Lumen 5.6.4 API resource collection not returning all data

I have a query which return following json which i pass to a resource collection. the problem is it return all the fields correctly but when it come to companies_closed field it shows null. whereas companies_closed is either an array or empty array. I cannot understand why is it happening.
I used this same method (passing data through bunch of functions) in Laravel 5.6.39 it works just fine in Laravel. But when i do it here in lumen 5.6.4 it doesn't work.
I thought this could be cache issue, so i followed almost all the method in this url but no success. since it didn't work i reverted all the changes.
https://github.com/laravel/framework/issues/2501
I couldn't find any more material on lumen api resource collection. can anyone please take a look and tell me what is it i'm doing wrong. any help would be highly appreciate.
Eloquent Query:
$companiesQuery = CpdAdmin::where('admin_id', $admin->admin_id)
->with(['companies' => function($query) use($request) {
$query->where('air_id', $request->airport_id)
->where('status', '1')
->with(['companiesClosed' => function($query) use($request) {
$query->where('close_status', 'active')
->where(function ($queryWhere) use($request) {
$queryWhere->where('start_date', '>=', $request->start_date)
->where('start_date', '<=', $request->end_date)
->where(function ($queryType) {
$queryType->where('journey_type', 'Departure')
->orWhere('journey_type', 'Both');
});
})
->orWhere(function ($queryWhere) use($request) {
$queryWhere->where('end_date', '>=', $request->start_date)
->where('end_date', '<=', $request->end_date)
->where(function($queryType) {
$queryType->where('journey_type', 'Arrival')
->orWhere('journey_type', 'Both');
});
});
}]);
}])
->get();
JSON (before passing it to resource collection):
The json return before passing it to collection works just fine, i shows everything the way i expect.
"data": [
{
"admin_id": 32,
"admin_name": "Eden Parking",
"email": "edenparking#hotmail.com",
"admin_login": "edenparking#hotmail.com",
"admin_information": "what information",
"contact_number": 303633,
"address": "paka dubai da",
"invoice_email": "invoice#gmail.com",
"contact_person": "uzair khan",
"admin_rights": "1114",
"admin_isactive": "1",
"edit_status": 0,
"created_at": null,
"updated_at": null,
"companies": [
{
"comp_id": 19,
"comp_title": "S Gatwick Meet & Greet",
"comp_email": "mcpgatwick#gmail.com",
"status": 1,
"operation_data": "{\"operating_type\":\"24_7\",\"opening_status\":\"open\"}",
"daily_bookings": 100,
"monthly_bookings": 1000,
"comp_admin": 32,
"air_id": 14,
"companies_closed": []
},
{
"comp_id": 57,
"comp_title": "Simply Parking Gatwick Meet & Greet",
"comp_email": "mcpgatwick#gmail.com",
"status": 1,
"operation_data": "{\"operating_type\":\"24_7\",\"opening_status\":\"open\"}",
"daily_bookings": 100,
"monthly_bookings": 1000,
"comp_admin": 32,
"air_id": 14,
"companies_closed": [
{
"id": 3101,
"start_date": "2019-03-04",
"end_date": "2019-03-15",
"journey_type": "Departure",
"close_status": "active",
"comp_id": 57,
"created_at": null,
"updated_at": null
},
{
"id": 3102,
"start_date": "2019-03-04",
"end_date": "2019-03-15",
"journey_type": "Both",
"close_status": "active",
"comp_id": 57,
"created_at": null,
"updated_at": null
}
]
},
{
"comp_id": 149,
"comp_title": "Park with us Gatwick",
"comp_email": "mcpgatwick#gmail.com",
"status": 1,
"operation_data": null,
"daily_bookings": 100,
"monthly_bookings": 1000,
"comp_admin": 32,
"air_id": 14,
"companies_closed": []
}
]
}
]
Here i passed the query to a resource collection
$companiesQueryResourceCollection = new CompanyResourceCollection($companiesQuery);
Resource Collection:
In the very last function i've
'companies_closed' => $eachCompany->companies_closed
I can loop companies_closed but if i can get something, if i'm getting nothing foreach would simply throw an error.
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CompanyResourceCollection extends ResourceCollection
{
public function toArray($request)
{
$response = $this->transformMultiple();
return $response;
}
public function transformMultiple()
{
$transform = [];
foreach ($this->collection as $item)
{
$transform[] = $this->transformSingle($item);
}
return $transform;
}
public function transformSingle($item)
{
return [
'admin_id' => $item->admin_id,
'admin_name' => $item->admin_name,
'email' => $item->email,
'contact_number' => $item->contact_number,
'contact_person' => $item->contact_person,
'companies' => $this->transformCompanies($item->companies),
];
}
public function transformCompanies($companies)
{
$transform = [];
foreach ($companies as $singleCompany)
{
if( ($singleCompany->dailyBookingsCount < $singleCompany->daily_bookings) && ($singleCompany->monthlyBookingsCount < $singleCompany->monthly_bookings) )
{
// initially i was passing each company to its single resource but it was showing the same behaviour, so i moved the code to this file.
// $transform[] = new CompanyResource($singleCompany);
$transform[] = $this->transformEachCompany($singleCompany);
}
}
return $transform;
}
public function transformEachCompany($eachCompany)
{
return [
'comp_id' => $eachCompany->comp_id,
'comp_title' => $eachCompany->comp_title,
'comp_email' => $eachCompany->comp_email,
'status' => $eachCompany->status,
'operation_data' => json_decode($eachCompany->operation_data),
'daily_bookings' => $eachCompany->daily_bookings,
'monthly_bookings' => $eachCompany->monthly_bookings,
'comp_admin' => $eachCompany->comp_admin,
'air_id' => $eachCompany->air_id,
'dailyBookingsCount' => $eachCompany->dailyBookingsCount,
'monthlyBookingsCount' => $eachCompany->monthlyBookingsCount,
// this returns 0 for all
// 'companies_closed' => count($eachCompany->companies_closed)
// this returns null for all
// 'companies_closed' => $eachCompany->companies_closed
// I can pass this array to another function but that would throw an error because the companies_closed is empty
// 'companies_closed' => $eachCompany->companies_closed
];
}
}
Okay, I fixed it myself, here is the problem.
Relations:
public function companiesClosed()
{
return $this->hasMany(CpdCompanyClosed::class, 'comp_id', 'comp_id');
}
public function company()
{
return $this->belongsTo(CpdCompany::class, 'comp_id');
}
Please note that relationship naming convention is companiesClosed
where as in JSON reponse I was seeing companies_closed
I was using companies_closed in API resource. when i changed it companyClosed it worked just fine.
The Reason:
Relationship naming convention is wrong. I changed it all to this naming convention company_closed.

Laravel API : how to get only some fields and sort the response

I would like to know if I do the things correctly.
Let's say I have a table "countries". To get only some fields of this table, in a certain order, I have this url :
/countries?fields=id,country_name&desc=country_name
And the result is clear:
[
{
"id": "SP",
"country_name": "Spain"
},
{
"id": "IT",
"country_name": "Italy"
},
{
"id": "FR",
"country_name": "France"
},
{
"id": "CN",
"country_name": "China"
} ]
To do that I have this route :
Route::get('/countries', 'CountryController#index');
And the method index is :
public function index(Request $request)
{
$query = Country::query();
if ($request->has('fields')){
$fields = explode(',', $request->input('fields') );
foreach ($fields as $field) {
$query->addSelect($field);
}
}
if ($request->has('sort')){
$query->orderBy($request->input('sort'));
}
if ($request->has('desc')){
$query->orderBy($request->input('desc'), 'desc');
}
$countries = $query->get();
return response()->json($countries, 200);
}
It works fine.
My question is am I doing the things correctly ? Is there any other methods ?
To prevent unknown column exception try this:
import Schema class use Illuminate\Support\Facades\Schema;
add this:
$table = "countries";
if ($request->has('fields')){
$fields = explode(',', $request->input('fields') );
foreach ($fields as $field) {
if(!Schema::hasColumn($table,$field)){
return response("Unknown field", 422);
}
$query->addSelect($field);
}
}
What you are doing to me has no vulnerability in it and is very optimized too.
You can try using this:
$countries = $query->pluck('id', 'country_name');
Please check and let me know.

Laravel, remove null Eloquent object attributes from JSON

Is there an elegant way to remove NULL values from an Eloquent Object? My object is nested with relationships. This particular call can be 1000s of lines long, so my main reason for trying this is to save bandwidth for the user, but server performance is also a consideration.
My code:
$data['locations'] = Location::with('address')->where('user_id', '1')->get();
return Response::json($data);
I experimented with Mutators, but unless I'm mistaken Mutators don't have power over the object key, just the value.
I also tried and failed to use array_filter like these:
Any PHP function that will strip properties of an object that are null?
How to remove empty associative array entries
EDIT As requested,
{
"status": "ok",
"locations": [
{
"id": "1",
"latitude": "12.239107980271",
"longitude": "109.19479025725",
"user_time": "",
"transport": "Bus",
"title1": "",
"title2": "",
"address": {
"town": "Nha Trang",
"country": "Vietnam",
"address": "36-44 Hùng Vương, Lộc Thọ, Nha Trang, Khanh Hoa Province, Vietnam"
},
"altitude": {
"altitude": "10.006237983704"
},
"timezone": {
"offset": "25200"
},
"forecast": {
"icon": "",
"high_temp": "",
"low_temp": ""
}
},
{
"id": "2",
Desired response:
{
"status": "ok",
"locations": [
{
"id": "1",
"latitude": "12.239107980271",
"longitude": "109.19479025725",
"transport": "Bus",
"address": {
"town": "Nha Trang",
"country": "Vietnam",
"address": "36-44 Hùng Vương, Lộc Thọ, Nha Trang, Khanh Hoa Province, Vietnam"
},
"altitude": {
"altitude": "10.006237983704"
},
"timezone": {
"offset": "25200"
}
},
{
"id": "2",
As you can see, I could simply loop through the whole lot and remove any keys - or keys of keys - without values. I was hoping Laravel might provide a neat/fast way of doing the same.
I should add that technically only the latitude and longitude are required fields!
3 possibilities:
Write a response macro which cleans up your json data:
http://laravel.com/docs/responses#response-macros
Extend the Response class and implement your cleanup routine there. See this great tutorial for details how to do this: http://fideloper.com/extend-request-response-laravel
Implement the jsonSerialize method in your model which will be automatically called when your model is converted to json and place your cleanup routines there. You can even go a step further and write your own Collection for your Location model. Depending on your data structure this can make things a little bit easier. A nice tutorial for this purpose can be found here: http://heera.it/extend-laravel-eloquent-collection-object
I personally would prefer option 3.) because the data modifications happens where it should happen - in your model.
But bottom line it really depends which solutions fits best to your project.
First make a trait and add your custom validation then use in your each resource where you need
trait ResourceHelpers
{
/**
* Remove null values from Eloquent api resource
* #param array $data
* #return array
*/
public function removeNullValues(array $data)
{
$filtered_data = [];
foreach ($data as $key => $value) {
// if resource is empty
if ($value instanceof JsonResource and $value->resource === null) {
continue;
}
$filtered_data[$key] = $this->when($value !== null, $value);
}
return $filtered_data;
}
}
Then use it in your resource
class UserResource extends JsonResource
{
use ResourceHelpers;
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return $this->removeNullValues([
"id" => $this->id,
"first_name" => $this->first_name,
"last_name" => $this->last_name,
"phone" => $this->phone,
"email" => $this->email,
"balance" => $this->balance,
'address' => $this->address,
'city' => $this->city,
'state' => $this->state,
'zip_code' => $this->zip_code,
'country' => CountryResource::make($this->whenLoaded('country')),
"joined_at" => $this->created_at,
"updated_at" => $this->updated_at,
]);
}
}
I enhanced the removeNullValues to also ignore empty arrays and be recursive to handle nested arrays. Please check this one.
function removeNullValues(array $data)
{
$filtered_data = [];
foreach ($data as $key => $value) {
if (is_array($value))
{
if (sizeof($value) > 0)
$filtered_data[$key] = $this->removeNullValues($value);
}
else if ($value != null){
$filtered_data[$key] = $value;
}
}
return $filtered_data;
}

Resources