For example i have following code:
public function index()
{
return
Model::select(['id', 'some_field', ...// more some fields])
->with('data') // load relation
->paginate(20);
}
How do i format (transform/manipulate on) obtained data from database?
CakePHP ORM have useful method for this -https://book.cakephp.org/3.0/en/orm/query-builder.html#adding-calculated-fields
&& https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#map-reduce
But i can't find any thing, that can help me to do a same things in Laravel.
I can override "toArray" method in a model, but this will affect all application parts (not only an index action in my controller).
You can do same thing in Laravel, for example:
return Model::select(['id', 'some_field', ...// more some fields])
->with('data')
->paginate(20)
->map(function($item, $key) {
// Some pseudo code
$item->uid = uniqid();
// Must return the $item
return $item;
});
There are other ways doing similar things. You can do many more in Laravel. There is a transform method as well, among many.
Here is my solution to only modify items without loosing the pagination data
public function index()
{
$data_with_pagination = Model::select(['id', 'some_field', ...// more some fields])
->with('data') // load relation
->paginate(20);
foreach ($data_with_pagination->items() as $item) {
// modify your item here
$item['uid'] = uniqid();
}
return $data_with_pagination;
}
paginate() and get() will return a Collection giving you access to all the Collection methods.
You would be able to do:
public function index()
{
return
Model::select(['id', 'some_field', ...// more some fields])
->with('data') // load relation
->paginate(20)
->map(function($model) {
$model->total_things = $model->one_thing + $model->other_thing;
return $model;
});
}
Most of the answers already provided will work, but will return a collection instead of a paginated resource.
The trick is to use the tap helper method before map'ping, to return the same object you modified.
public function index()
{
return tap(Model::select(['id', 'some_field', ...// more some fields])
->with('data') // load relation
->paginate(20))
->map(function ($model) {
$model->something_to_format = someFormattingHelper($model->something_to_format);
return $model;
});
}
I'm not sure if anyone still needs this.
But I found another solution. It's not the best solution, but it gets the job done.
Both the transform() and map() functions are supported.
Here's the link: https://jnbrnplbr.medium.com/transform-map-laravel-paginated-collection-b1ab912d7996
Related
I need to eager load relationship within laravel policy
The problem is i use the laravel policy inside yajra-datatable
which will load it (the policies) line by line
Here's the code looks like :
public function create(User $user)
{
$user->load('sections');
$sections = collect($user->sections->toArray())->pluck('name');
return
in_array($user->role_id,[3,4])
&& in_array('Produksi', $sections->toArray())
&& Session::get('productionPlans-branch') !== 'all'
&& Session::get('productionPlans-period') !== 'all';
}
and i use it in my yajra-datatable like so :
public function table(Request $request)
{
$query = ProductionPlan::with(['branch','product.category','period'])->orderBy('created_at');
return \Yajra\DataTables\DataTables::of($query)
->addIndexColumn()
->addColumn('action', function($row) {
if ($request->user->can('create', $row)) {
return 'Add';
}
})
->rawColumns(['action'])
->make(true);
}
so every line will load the relation again and again
I'm expecting more efficient way to load them just once instead of load the relation line by line
How can i achieve this ?
Update :
I tried to use accessor on User model to append the relations with sections table
protected $appends = ['sections'];
public function getSectionsAttribute ()
{
return $this->attributes['sections'] = $this->sections()->first();
}
This was success for only the FIRST relation only, i tried to remove the first() method but got the error PDO serialization instead
Serialization of 'PDO' is not allowed
I think Laravel policies uses the exact object it is called on. So $this->user->can(...) actually is the same object that is being passed as first parameter to the create(User $user) policy method.
In that case, I would try to load it before you call the ->can method inside the closure.
The code could look like this:
public function table(Request $request)
{
$query = ProductionPlan::with(['branch','product.category','period'])->orderBy('created_at');
$user = $request->user;
$user->load('sections');
return \Yajra\DataTables\DataTables::of($query)
->addIndexColumn()
->addColumn('action', function($row) use ($user) {
if ($user->can('create', $row)) {
return 'Add';
}
})
->rawColumns(['action'])
->make(true);
}
And then you also have to remember to remove the $user->load('sections'); from inside the create() method in the policy.
This is very important :-)
Essentially I am just trying to add a where condition to this request where I get a "Phase" with a bunch of its children (i sudo'd it up a bit) :
public function show($projectId, $phaseId)
{
return Phase::with('headers.subheaders.lines.values')->findOrFail($phase);
}
I want to do something like this:
public function show($projectId, $phaseId)
{
return Phase::with('headers.subheaders.lines.projectValues')
->where('headers.subheaders.lines.projectValues.project_id', '=' , $projectId)
->findOrFail($phaseId);
}
I've tried various variations of this :
return Phase::with(['headers.subheaders.lines.projectValues' => function ($query) use ($projectId) {
$query->where('project_id', $projectId);
}])->findOrFail($phaseId);
But I can't find the magical combination of syntax to get this working properly. I normally get the error that project_id is not an attribute of phase for the last example... I've tried giving it the full path twice but it doesn't seem to like it... Maybe I'm just being dumb and theres a simple solution...
Edit :
Some of the relationships:
class Line extends Model
{
// Other stuff
public function projectValues()
{
return $this->hasMany(ProjectValues::class, 'question_id');
}
}
class QuestionValue extends Model
{
// Other stuff
public function project()
{
return $this->belongsTo(Project::class);
}
public function line()
{
return $this->belongsTo(Line::class);
}
}
have you tried using a chain of whereHas:
return Phase::with('headers.subheaders.lines.projectValues')
->whereHas('headers',function ($query)use($project_id){
$query->whereHas('subheaders',function ($query2)use($project_id){
$query2->whereHas('lines',function ($query3)use($project_id){
$query3->whereHas('projectValues',function ($query4)use($project_id){
$query4->where('project_id','=',$project_id);
});
});
});
})
->findOrFail($phaseId);
I want to correctly save both polymorphic relationships at the same time. The code below works, but I feel it could be a lot cleaner as I presume the two update() methods are calling the database twice.
A NewsModule::class can have different module items; VideoModule, TextModule, ImageModule, and a few more. Each containing their own content to be attached to the parent NewsModule.
As mentioned, the code works so the relationships are set up correctly, but I'm convinced there's a cleaner way of saving both at the same time.
I'm also open to suggestions about cleaning up the if statements too. But maybe that's another post.
public function update(Request $request, $id)
{
$module = NewsModule::find($id);
if ($module->type === 'text') {
$content = TextModule::find($module->content_id);
} elseif ($module->type === 'image') {
$content = ImageModule::find($module->content_id);
};
$module->update($request->all());
$content->update($request->all());
return fractal()
->item($module, new NewsModuleTransformer)
->parseIncludes(['content'])
->toArray();
}
Updated (more code by request)...
Structure:
news_modules
- id
- content_id
- content_type
- etc
text_modules
- id
- content
- etc
image_modules
- id
- image_id
- etc
NewsModule:
class NewsModule extends Model
{
public function content()
{
return $this->morphTo();
}
}
All item modules:
class TextModule extends Model
{
public function newsmodules()
{
return $this->morphMany(NewsModule::class, 'content');
}
}
public function update(Request $request, $id)
{
$modele = NewsModule::find($id);
$module->update($request->all());
$module->content->update($request->all());
return fractal()
->item($module, new NewsModuleTransformer)
->parseIncludes(['content'])
->toArray();
}
That will run 4 queries total. 1 for each module to retrieve and another to update. That can be cut down to 3 like:
public function update(Request $request, $id)
{
$modele = NewsModule::find($id);
$module->update($request->all());
$module->content()->update($request->all());
return fractal()
->item($module, new NewsModuleTransformer)
->parseIncludes(['content'])
->toArray();
}
The downside to $module->content()->update($request->all()); is it will throw an error if there is anything in $request->all() that isn't a column in that content model or there is an array as a value. You can avoid that by just calling update() on the $fillable properties (if you have them defined) of the related model like:
$fillable = $module->content()->getRelated()->getFillable();
$module->content()->update($request->only($fillable));
This way will also not fire any model event listeners you have since you are never retrieving the model from the database.
To take everything one step further, look into Route Model Binding. In your app\Providers\RouteServiceProvider's boot() method:
public function boot()
{
parent::boot();
Route::model('news', App\NewsModule::class);
}
This way 'news' will always resolve to an instance of NewsModule when using it as a route parameter. So your route would be something like:
Route::match(['patch', 'put'], '/news/{news}', 'NewsController#update');
So in your update method you could resolve the model by just type hinting it in the method allowing you to do:
public function update(Request $request, NewsModule $news)
{
$news->update($request->all());
$news->content->update($request->all());
return fractal()
->item($news, new NewsModuleTransformer)
->parseIncludes(['content'])
->toArray();
}
In my Menu controller I have a method which should display the specified record and all its children.
I have two models: MenuItem and MenuVariation, My items will have many variations as outlined in my methods:
MenuItem model
public function variant()
{
return $this->hasMany('App\MenuVariation');
}
MenuVariation model
public function item()
{
return $this->belongsTo('App\MenuItem', 'menu_item_id');
}
Now in my controller I have the following method:
public function show($id)
{
$item = MenuItem::findOrFail($id);
return $item;
}
...which currently only shows the item record but not its variations, so I have changed the code like this...
public function show($id)
{
$item = MenuItem::findOrFail($id)->with('variant')->get();
return $item;
}
but this oddly return ALL items and their variations.
Could someone help me get this working as desired? I would like to still utilise FindOrFail on the Item record, but it should also retrieve any variants (if found).
findOrFail will initiate the query, so you want to switch the order and put with in-front of it. Then use findOrFail. See Below:
public function show($id)
{
$item = MenuItem::with('variant')->findOrFail($id);
return $item;
}
There is also no need for get when you do that.
I have three tables - Campaigns, Actions and Activists. A Campaign has many Actions, and an Action belongs to both an Activist and a Campaign.
Each action has a client_id (from the client_id of the campaign it belongs to), so when a client views a list of activists, they should only see those who have taken an action on one of their campaigns.
Likewise, when viewing an individual activist, they should only see those actions related to their campaigns.
Models
Campaign.php
public function actions()
{
return $this->hasMany('Action');
}
Action.php
public function campaign()
{
return $this->belongsTo('Campaign', 'campaign_id');
}
public function activist()
{
return $this->belongsTo('Activist', 'activist_id');
}
Activists.php
public function actions()
{
return $this->hasMany('Action');
}
Controllers
ActivistsController.php
public function index()
{
$activists = Activist::with('actions')->whereHas('actions', function($q) {
$user = Sentry::getUser();
$q->where('client_id', $user->client_id);
}))->get();
foreach ($activists as $activist)
{
$activist->total = $activist->actions()->count();
}
}
public function getActivist($id)
{
$activist = Activist::with('actions')->whereHas('actions', function($q) {
$user = Sentry::getUser();
$q->where('client_id', $user->client_id);
})->find($id);
$activist->total = $activist->actions()->count();
}
I'm seeing the following:
On the /activists page, I'm correctly seeing only those activists who have taken an action related to my client_id, but also every action they've taken. Likewise, count() returns a full count of all the activists' actions.
On the /activists/{id} page, it correctly returns null if the activist hasn't taken any actions related to my client_id, but where they have, I again see all of their actions and a full count.
AFL. There's something blinding obvious I'm missing, right?
Thanks.
[edit] Updated to add:
Using the client_id filter on both with and whereHas rectifies the 'all actions appearing regardless' issue, but the count issue remains (and I'm not sure this is remotely the right way to improve this):
ActivistController.php
public function index()
{
$filter = function($q) {
$user = Sentry::getUser();
$q->where('client_id', $user->client_id);
};
$activists = Activist::with(array('actions' => $filter))
->whereHas('actions', $filter)
->get();
}
public function getActivist($id)
{
$filter = function($q) {
$user = Sentry::getUser();
$q->where('client_id', $user->client_id);
};
$activist = Activist::with(array('actions' => $filter))
->whereHas('actions', $filter)
->find($id);
}
I've solved this now, but for reference:
$activist->actions()->count()
This, obviously in hindsight, ignores any of the prior queries and simply counts data returned from the actions() method as defined in the activist model.
I should have provided an alternate method in the model that includes the appropriate where function, like so:
public function actionsClient($id)
{
return $this->hasMany('Action')->where('client_id', $id);
}
Meaning the count could then be invoked with:
$activist->total = $activist->actionsClient($id)->count();
for a single campaign and
foreach ($activists as $activist)
{
$activist->total = $activist->actionsClient($activist->id)->count();
}
on the index. I'd previously tried this, but as described here - How to access model hasMany Relation with where condition? - relations must be described in camelCase (actions_client > actionsClient).
In my usage this worked for me:
$clients = Profile::select('id', 'name')->orderBy('initial')->whereHas('type', function ($query) {
$query->where('slug', 'client');
})->office()->pluck('name', 'id');
You already have the instance of $activist eager loading their actions, meaning you already know the actions of the activist beacause they are already in the instance, so why call actions() again instead of just doing this:
$activist->actions->count()