Laravel: How to use scope with mass assignment - php

I have two entities with many to many relationship: Form and Section. A valid section must to be "active" then I have a scope:
/**
*
* #param Builder $query
* #return Builder
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
And when rendering the form, just pass the active sections to the view:
$sections = Section::active()->get();
The problem is that when the POST request is sent, I have to perform a mass assignment in relationship with sync and have not control over the id's passed (actives and not actives):
$form->sections()->sync($sectionsId);
There is a way to attach only active sections without manually checking each one?

You have to take sections in $sectionIds first and then you have to use sync() method.
Try this code,
$sectionIds = Section::active()->whereIn('id', [1, 2, 3])->pluck('id');
$form->sections()->sync($sectionIds);
I believe that this will work.

Related

Return relationships in Laravel show route

When I create a CRUD controller, this is the show route created by default:
/**
* Display the specified resource.
*
* #param \App\Team $team
* #return \Illuminate\Http\Response
*/
public function show(Team $team)
{
//
}
$team is an object here, an instance of Team. If I do this I have the correct object passed to blade:
public function show(Team $team)
{
return view('admin.teams.show', ['team' => $team]);
}
But, Team has a many-to-many relationship with another model called Player, and this relationship is defined as such from the Team side:
public function players() {
return $this->belongsToMany(Player::class);
}
In my show method, I'd like to return the $team with its related players. But since $team is already an object and not a query builder, it's too late to do something like
$team->with('players')
So how do I get the related players here? I know I can do something like:
public function show(Team $team)
{
$team_extended = Team::where('id', $team['id'])->with('players')->first();
return view('admin.teams.show', ['team' => $team_extended]);
}
But it feels like hacking a functionality that should be there by default. Is there a built-in Laravel way to do this or am I just inventing hot water and should take the approach I used in my solution above?
If you've already got your Team model loaded, you can load a relationship without having to completely re-create it using the ->load() method:
public function show(Team $team){
$team->load("players");
return view("admin.teams.show", ["team" => $team]);
}
Note however, this isn't required unless you need to modify the default content of $team->players. When you trying to access $team->players say in your admin.teams.show view, if that property doesn't already exist (as it would using ->with(["players"]) or ->load("players"), Laravel will load it automatically.

Laravel filter data after with closure

I have one quite simple question, Imagine I have Orders model and now I am writing something like that :
Order::where('status', 1)->with('orderer')->get();
Ok. It's simple and returns something like that:
{
id: 1,
price: 200,
status: 1,
income: 21,
orderer_id: 4,
orderer: {
//some orderer fields
}
}
now I don't want to get the whole object, I want to remove income, orderer_id and status properties from data. if I write something like that : get(["id", "price"]) I end up without orderer object (get(["id", "price", "orderer"]) doesn't work too), I couldn't make it work even using select(), so what is the solution? Also I don't want to hide it from everyone, for example admin should know income but user shouldn't, so $hidden field will not work.
You can add select() but make sure select does not take array but comma separated arguments :
$orders = Order::where('status', 1)->with('orderer');
if($user->role == 'admin'){
$orders->select('id','income','status','price');
}
else{
$orders->select('id','status','price');
}
$orders = $orders->get();
Above will first check the current logged in user's role and accordingly will select the columns required.
https://scotch.io/bar-talk/hiding-fields-when-querying-laravel-eloquent-models
In your Order Eloquent model:
protected $hidden = array('hide_this_field', 'and_that_field');
Edit: You want to filter based on role like Admin or User, next time please write that down in your question as well. Well a solution for that is to capture the DB query result, and walk that array, then unset properties of the model if the user is not an admin.
Edit2: I also see a discussion here which might help. Some user suggested using middle ware:
https://laracasts.com/discuss/channels/laravel/hide-eloquent-fields-based-on-user-role-or-any-model
If you are looking for a built in Laravel way to handle this, you could use API Resources: https://laravel.com/docs/5.7/eloquent-resources
php atrisan make:resource OrderResource
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$current_role = $request->user()->role; //or however you determine admin etc
$out = [
'id' => $this->id,
'price' => $this->price,
'orderer'=> $this->orderer,
];
if($current_role == 'admin'){
$out['income'] = $this->income;
$out['status'] = $this->status;
}
return $out;
}
}
In your Controller action
return OrderResource::collection(Order::where('status', 1)->with('orderer')->get());
If you want something a little more robust, consider https://github.com/spatie/laravel-fractal

Am I doing eager loading correctly? (Eloquent)

I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).

Laravel changing sorting of collection when sending mail (between constructor and build function call)

this is laravel 5.3
when I preview the email using this:
$wantsheet_products = WantsheetProduct::orderByRaw(EmailService::WANTSHEET_PRODUCT_ORDER_SQL)->get();
View::make('email.wantsheet.email_wantsheet_to_supplier', ['wantsheet_products' => $wantsheet_products]);
the sorting is correct. that is, sorting is ['a','b','c'] the way i want it.
EDIT see note at the bottom
now when actually sending out the mails (i queue them), the sorting changed and is unsorted again, magic?! the change happens between the constructor and the build function
class WantsheetToSuppliersMail extends Mailable
{
public $wantsheet_products;
public $to_email;
/** #var WantsheetContact $wantsheetcontact*/
public $wantsheetcontact;
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($wantsheet_products)
{
//$wantsheet_products is a standard eloquent model collection, e.g. i get it like this: WantsheetProduct::orderByRaw(self::WANTSHEET_PRODUCT_ORDER_SQL)->get()
$this->wantsheet_products = $wantsheet_products; //is ['a','b','c']
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
// $this->wantsheet_products is ['b','a','c'];
$subject = 'abc';
return $this->from('me#myapp.com')->view('email.wantsheet.email_wantsheet_to_supplier', [])->subject($subject);
}
}
EDIT contd.
Now when i do
WantsheetProduct::orderByRaw(EmailService::WANTSHEET_PRODUCT_ORDER_SQL)->get()->toArray();
it doesn't break the sorting any longer (so it works). But that is stupid, isn't it?
When your mail object is queued for delivery, it takes your Collection of Model instances, gets their ids, and stores the list of ids on the queued job. When the queued job is then processed, it takes those Model ids, and retrieves the data from the database.
The problem, however, is that the query being run to rebuild the collection doesn't care about the order of the ids. It just runs a whereIn() statement with the list of ids.
Everything worked when you converted your Collection toArray() because it also converted all your Models to arrays. So, it was no longer a Collection of Models, it was an array of arrays. There is no special serialization that takes place there, so the data went across exactly as you sent it.
The easiest way to get your order back is probably to override the restoreCollection method, so you can add in your order by clause to the restoration query. Add this method to your WantsheetToSuppliersMail class:
protected function restoreCollection($value)
{
if (! $value->class || count($value->id) === 0) {
return new EloquentCollection;
}
$model = new $value->class;
return $model->newQuery()->useWritePdo()
->whereIn($model->getKeyName(), $value->id)
->orderByRaw(EmailService::WANTSHEET_PRODUCT_ORDER_SQL)
->get();
}
This is the same as the current function, just that your custom order by has been applied to the query.
it is a known bug of laravel 5.3
basically reretrieve the objects in the build function e.g.
public function build()
{
$this->wantsheet_products = WantsheetProduct::orderByRaw(EmailService::WANTSHEET_PRODUCT_ORDER_SQL)->get();
$subject = 'abc';
return $this->from('me#myapp.com')->view('email.wantsheet.email_wantsheet_to_supplier', [])->subject($subject);
}

Doctrine2 one-to-many relation overwrite whole collection

Suppose I have two entities Page and Block. It's bi-directional mapping. Each page can have more than one block. Each block could belong to single page.
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
...
/**
* #ORM\OneToMany(targetEntity="Block", mappedBy="page", cascade={"all"}, orphanRemoval=true)
**/
private $blocks;
...
}
/**
* #ORM\Entity
* #ORM\Table(name="block")
*/
class Block
{
...
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="blocks")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*
**/
private $page;
...
}
From time to time blocks that belong to page will change. Some blocks will be added, some of them will be removed. The next case doesn't work for me:
$page->setBlocks([1, 2, 3]);
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3, 4
Expected result after second flush() call is: //Page will have 1, 4
So I need to overwrite completely collection of blocks with merge method.
Constraints:
I can't implement deleteBlock in Page class
I can only call merge() and flush() on $em
Is it possible to implement desired result via annotations or some other trick?
Addressing the Constraints
It cannot be done with your constraints.
Suggested Solution
I'd create a custom function in the BlockRepository Class that deletes all blocks of the given Page, then call that function before calling the setBlock function.
Add this to your Block Repository class:
public function deleteBlocksByPageId($page_id)
{
$qb = $this->createQueryBuilder()
->delete('Block', 'b');
->where('b.page', ':page'));
->setParameter(':page', $page_id);
$numDeleted = $qb->execute();
return $numDeleted;
}
In your Controller:
//Delete all the existing blocks before adding them back in.
$repo = $this->getDoctrine()->getRepository('BundleName:BlockRepository');
$repo->deleteBlocksByPageId($page->getId());
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush()
Conclusion
This is a common pattern for managing items that change often. It required no loops.
If the constraints are self-imposed then you need to loosen them and find another solution, such as the one I offer above. If they are constraints imposed by some higher-up then it's time to have a conversation with him/her about it being counter productive in this instance.

Categories