Laravel 4.2: Can't Use Transformer with DB::Raw Query - laravel

Throughout my app, I have been taking the results of an Eloquent query and channeling it through a transformer and it is working well. This is what I am doing:
public function index()
{
$users = User::with('profile', 'location', 'role')->get();
return $this->respond([
'data' => $this->userTransformer->transformCollection($users->all())
]);
}
However, I ran into a situation where to produce a nested resource I needed to use a DB::RAW query
public function byLoan($id)
{
$users = DB::select( DB::raw("SELECT * FROM users WHERE id IN (SELECT DISTINCT(user_id) FROM farms WHERE user_id = {$id})") );
return $this->respond([
'data' => $this->userTransformer->transformCollection($users->all())
]);
}
The code above gave me this error "Symfony \ Component \ Debug \ Exception \ FatalErrorException (E_ERROR) Call to a member function all() on array".
I changed transformCollection($users->all()) to transformCollection($users) and now the error is this "Symfony \ Component \ Debug \ Exception \ FatalErrorException (E_ERROR) Cannot use object of type stdClass as array".
I don't know what else to try.
My Transformer class looks like this:
<?php namespace Acme\Transformers;
abstract class Transformer {
public function transformCollection(array $items)
{
return array_map([$this, 'transform'], $items);
}
public abstract function transform($item);
}
And the UserTransformer implentation:
<?php namespace Acme\Transformers;
class UserTransformer extends Transformer
{
public function transform($arr)
{
//return $arr;
return [
'id' => $arr['id'],
'username' => $arr['username'],
'abr' => $arr['abr'],
'email' => $arr['email'],
'phone' => $arr['phone'],
'role_id' => $arr['role_id'],
'role_abr' => $arr['role']['abr'],
'role' => $arr['role']['role'],
'loc_id' => $arr['location']['id'],
'loc_abr' => $arr['location']['loc_abr'],
'region_id' => $arr['location']['region_id'],
'is_admin' => (boolean) $arr['is_admin'],
'is_manager' => (boolean) $arr['is_manager'],
'show_agency' => (boolean)$arr['profile']['show_agency'],
'show_balance_due' => (boolean)$arr['profile']['show_balance_due'],
'show_close_date' => (boolean)$arr['profile']['show_close_date'],
'show_commit_arm' => (boolean)$arr['profile']['show_commit_arm'],
'show_region' => (boolean)$arr['profile']['show_region'],
'show_season' => (boolean)$arr['profile']['show_season']
];
}
}

First off, don't use all() on a DB::select. The select method does execute the query as soon as it's called. Also all() is for Eloquent models only.
However the bigger problem here, is how a Query Builder result is structured. Its an array (of all rows) filled with objects (the individual row, with properties for each column)
So that means, passing it to your transform method and then accessing the columns like an associative array doesn't work. Example:
$arr['id']
What you would have to do instead is:
$arr->id // obviously the name $arr doesn't make any sense now because its an object..
If this messes up the other usages of transform() you should also be fine by just adding
$arr = (array) $arr;
at the beginning of the function. This converts the object into an array and makes sure you can access it like $arr['id']

Related

laravel endpoint hide field

How can i hide some fields ?
i want to hide the file field
Eloquent :
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return ReportResource::collection($reports);
Model :
...
public function getFileSizeAttribute()
{
return Storage::disk('files')->size($this->attributes['file']);
}
....
ReportResource:
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file), <-- i want to hide the file field
'file_size' => $this->fileSize, <-- but always show file_size
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
to get file_size field i must select the file field , because it's depends on it to calculate the file size.
but i want to hide the file field before send the response.
i know i can use the protected $hidden = [] method in the model , but i don't want that, because file field it's required on others place. i just want to hide it on this endpoint only.
Since you are using API resources the best and clean way to do this is by using a Resource class for your collection.
Said that, you will have 3 Resources:
The first one, as it is, just for retrieving a single Model with file and file_size attributes. The one you already have ReportResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
A new second resource to be used in your endpoint, without the file attribute. IE: ReportIndexResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
Now you need to create a Resource collection which explicitly defines the Model Resource to use. IE: ReportCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ReportCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* #var string
*/
public $collects = ReportIndexResource::class;
}
Finally, use this new resource collection in your endpoint
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return new ReportCollection($reports);
Of course, you can make use of makeHidden() method, but IMO is better to write a little more code and avoid a non desired attribute in your response because you forgot to make it hidden.
Also, in case you make use of makeHidden() method and you want to show the attribute in a future, you will have to update all your queries instead of a silgle resource file.
If you want to make it Hide From All Returns , you can Do this in model
protected $hidden = ['file'];
and if you want to do it temporirly with this query , you can Use MakeHidden method
$users = $reports->makeHidden(['file']);
It's clear in laravel docs , take a look
https://laravel.com/docs/9.x/eloquent-collections#method-makeHidden

Eloquent eager loading specific columns

I have two models :Product and category
which are linked by a one-to-many relationship. A category has several products. I would like to select specific columns from each model.
Here is the query I have, but I have all the columns with category_id, but I want the category name instead of id. How can I do that. Thank you in advance.
here is the method in controller
$products = Product::with('categories:id,name')->get();
if ($products) {
$response = ['api_status' => 1, 'api_message' => 'success', 'data' => $products];
return response()->json($response);
} else {
$response = ['api_status' => 0, 'api_message' => 'Error'];
return response()->json($response);
}
Here is category model
class Categorie extends Model
{
use HasFactory, SoftDeletes;
protected $fillable =['name','redirect'];
public function products()
{
return $this->hasMany(product::class);
}
}
and the product model is:
class Product extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'description',
'detail', 'img',
'categorie_id', 'onSale',
'costPrice', 'inStock', 'salePrice'
];
public function categories()
{
return $this->belongsTo(Categorie::class);
}
}
here is the response:
To modify the output of your model I'd suggest using an API resource. This will give you more granular control about how a resource is returned by the API. A resource is also the best point to modify certain values.
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'detail' => $this->detail,
'img' => $this->img,
'category_id' => $this->categorie->name,
'category_name' => $this->categorie->name,
'onSale' => $this->onSale,
'costPrice' => $this->costPrice,
'inStock' => $this->inStock,
'salePrice' => $this->salePrice,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
'categories' => $this->categories ?? null,
];
}
}
This way you can manually specify which values your response should have.
In your controller you can include the populated array in your response by manually filling the toArray method with the current request object or just by using the resolve method which basically does the previous task for you:
$response = [
'api_status' => 1,
'api_message' => 'success',
'data' => ProductResource::collection($products)->resolve()
];
You can select particular fields from the relationship but you always need to select any keys involved in the relationship:
$products = Product::with('categories:id,name')->get();
Now each Product has its 'categories' loaded and those Category models only have the id and name fields.
Importantly:
The relationship categories is named incorrectly, it should be categorie in this case as the foreign key on Product is categorie_id and it is a singular relationship, it does not return multiple results.
Product::with('categorie:id,name')->get()
If you want to keep the name categories you would have to define the foreign key used when defining the belongsTorelationship, the second argument.
If you need to transform the structure of any of this that is a different thing and you will be walking into transformers or an API Resource.
Not sure how you want your data to look but this is the structure you will have by eager loading records, so if you need a different structure then what you get you will have to show an example.

Using whenLoaded in Resource Collection I got error

In laravel 6 app I have a Resource Collection, which works ok for me :
class UserSkillCollection extends ResourceCollection
{
public static $wrap = 'user_skills';
public function toArray($request)
{
return $this->collection->transform(function($userSkill){
return [
'id' => $userSkill->id,
'user_id' => $userSkill->user_id,
'user_name' => $userSkill->user_name,
'skill_id' => $userSkill->skill_id,
'skill_name' => $userSkill->skill_name,
'rating' => $userSkill->rating,
'created_at' => $userSkill->created_at,
];
});
}
except when some fields are defined, like user_name, I have keys with null values.
To get rid of them I tried to use whenLoaded, but with line :
'user_id' => $this->whenLoaded('user_id'),
I got error :
"message": "Method Illuminate\\Support\\Collection::relationLoaded does not exist.",
Which way is valid ?
MODIFIED :
I added relations in models and making :
'user' => $userSkill->whenLoaded('user'),
or
'user' => $this->whenLoaded('user'),
I got error :
Call to undefined method App\UserSkill::whenLoaded(
I suppose this error as I call it from Collection.
How correctly ?
Thanks!
relationLoaded() is a method inherited from the HasRelationships trait on Illuminate\Database\Eloquent\Model.
Your code is trying to access it on a instance of an Illuminate\Support\Collection.
Try accessing the relationship user rather than it's key user_id. Like so:
$this->whenLoaded('user')

How to convert object return by laravel model factory create method into array containing model fields?

For example, I have a UserFactory.php
<?php
use App\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'role' => 'USER',
'password' => 'sasdcsdf34', // password
'remember_token' => Str::random(10),
];
});
Now, I can create a user as following
$user = factory(User::class)->create();
Now, How can I convert this $user object into array containing user info like name,email etc without initializing new array and manually assigning every $user object property. ??
I DON'T want to manually assign like following as it is tedious if there are many properties in $user object
$userArray=[
'id' => $user->id,
'name' => $user->name,
'email' => $user->email
]
I have tried this but it creates array containing various other properties and actual values needed are nested inside properties
$userArray=array($user)
You should try using the raw method of factory instead of create.
$user = factory(User::class)->raw();
This should give you an array you can work with.
Try to add something like this to your model class:
public function getArr(){
foreach($this->attributes as $key => $val){
$array[$key] = $val;
}
return $array;
}
If you wish to have this function in every model you could create trait with this function and then just attach it in model class or any class extending it.
You can use json_decode.
// Laravel 7
$userArray = json_decode(factory(User::class)->create(), true);
// Laravel 8
$userArray = json_decode(User::factory()->create(), true);
For Laravel 8, instead of make or create method, use:
User::factory()->raw();
This will return an array

Exporting Excel getting this error Trying to get property 'id' of non-object

I'm using Maatwebsite Laravel Excel package 3.0.
This is my InvoicesExport.php
class InvoicesExport implements FromCollection
{
public function collection()
{
$company_id = Auth::user()->company_id;
$assets = Asset::where('id', $company_id)->get()->toArray();
$assets_array = array();
if(!$assets->isEmpty()){
foreach($assets as $asset){
$assets_array[] = array(
'Asset ID' => $asset->id,
'Asset Name' => $asset->name,
'Description' => $asset->description,
'Serial Number' => $asset->serialno,
'External ID' => $asset->external_id,
'Location' => $asset->location,
'Expiry Date' => $asset->expiry_date,
'Owner' => $asset->owner,
'Status' => $asset->status,
'Updated at' => $asset->updated_at
);
}
}
//dd($assets_array);
return $assets_array;
}
}
And i keep getting this error
Trying to get property 'id' of non-object
And this is my function in controller
public function excel()
{
return Excel::download(new InvoicesExport, 'invoices.xlsx');
}
My code looks like this now and I keep getting this error.
Call to a member function each() on array
When i use dd($assets_array) I get those 2 items that i have in database, so i think maybe it's problem in my RETURN
It seems, the problem is outside of the InvoicesExport Class.
Why don't you change the type of return?
instead of returning
return $assets_array;
Do it like this:
return collect($assets_array);
you convert the collection to array and try to access it as object
just remove the toArray
$assets = Asset::where('id', $company_id)->get();

Resources