Deleting nested comments and related data from laravel controller - laravel

ive implemented nested comments in laravel with parent_id and there's another table votes where related data's are stored.
I've hasMany relation defined in comments model. Now when i delete a comment, it should delete all its replies and votes as well.
To delete votes i used
$review->votes()->delete();
which works perfectly. but i'm stuck with deleting votes for nested replies.
If i use foreach loop how to loop inside all levels which is dynamic.
public function deletereview($id=null){
$review = Review::find($id);
foreach($review->replies as $reply){
$reply->votes()->delete();
//how to do this for all levels?
$reply = $reply->votes(); // this doesn't work
}
return back();
}
Kindly advise on the proper way of doing it.
Note : i've read through the cascade options from migrations but that doesn't explain anything for nested comments(reply of replies and its related data's).
Thanks
-Vijay

// Review Model
public function deleteRelatedData() {
// Delete all votes of this review
$this->votes()->delete();
// Calling the same method to all of the child of this review
$this->replies->each->deleteRelatedData();
}
// Controller
public function deletereview($id=null){
$review = Review::find($id);
$review->deleteRelatedData();
return back();
}

I would recommend use observer for this.
https://laravel.com/docs/5.8/eloquent#observers
public function deleted(Review $review)
{
foreach($review->replies as $reply){
$votes = $reply->votes;
Votes::destroy($votes)
}
Destroy method allow you to delete multiple models.
For any next level you have to use another foreach loop in this case.
$reply = $reply->votes(); doesn't work since you should use
$votes = $reply->votes;
//or
$votes = $reply->votes()->get();

Related

Lazy Eager Loading for third level relationship in Laravel

UserModel has many lead and each lead can have one propertyLead and each propertyLead can have many attachments.
Each model is listed below,
UserModel:
public function leads()
{
return $this->hasMany('App\Models\Leads', 'fk_user_id');
}
LeadsModel:
public function propertyLead()
{
return $this->hasOne('App\Models\PropertyLead', 'fk_lead_id');
}
PropertyLeadModel:
public function attachments()
{
return $this->hasMany('App\Models\Attachments', 'fk_property_lead_id');
}
Now, I am using the Lazy Eager Loading of laravel to readData from dataBase, so far I am able to reach to PropertyLeadModel but I am not able to understand how to reach to attachment relation in the PropertyLeadModel,
$leads = User::find(Auth::user()->id)->leads->load('propertyLead');
so user gives me leads and leads gives me propertyLead but not able to understand how to reach more down to Attachments in the propertyLeadModel.
Please, help me to understand.
Thank you.
You can load nested relationships using "dot" notation.
$user = Auth::user();
$user->load('leads.propertyLead.attachments');
// see all relationships loaded
dd($user);
Since leads and attachments are "many" relationships, they will be Collections you have to iterate through to access any particular instance.
Try this, not tested
$leads = Auth::user()->leads->load('propertyLead', 'propertyLead.attachments');
Try this
User::with('leads.propertyLead.attachments')->where('id',Auth::user()->id)->first();

How can I link 2 existing models in my laravel code?

I have two models customer and orders. They are already fecthed separately
$customers = customer::all();
$orders = orders::all();
customerID=1 has orderID : 1, 2,4 customerID=2 has orderID : 3,5,9
They are related (hasMany, belongsTo) but the problem is inside my for a certain reason they are separated but I want to send them as response in API using toJson or ToArray as one data having the orders nested to their correct customers.
How can I achieve that linking to have at the end one variable $customersWithOrders that should be transformed to JSON ?
I am using laravel 5.5
I don't know what the context is. Defining relationships as other answers mentioned is a good solution.
In addition, I recently read a pretty good article about this specific scenario.
So you can also do something like this, if you have already retrieved customers and orders:
$customers = Customer::all();
$orders = Order::all();
return $customers->each(function ($customers) use ($orders) {
$customer->setRelation('orders', $orders->where('customer_id', $customer->id));
});
If you already have a relation you just use it. For example, in model Customer.php:
public function orders()
{
return $this->hasMany(Order::class);
}
Then you'd get customer orders by calling $customer->orders
If you already have defined relations, you can simply fetch data with eager loading
// in customer model
public function orders()
{
return $this->hasMany(orders::class, 'orderID');
}
// in controller
$customersWithOrders = customer::with('orders')->get();
return response()->json(['customersWithOrders' => $customersWithOrders]);
// in js
for (let customer in response.customersWithOrders){
let orders = customer.orders
}

Best Practice - Laravel Controller Eloquent merge

I have a scope on my Supplier model that returns results where active = true.
This works great when creating new entries, as I only want the user to see active suppliers.
Current entries may have an inactive supplier; When I edit it, I want to see all active Suppliers, plus the current supplier (if it is inactive)
I have this code in my controller:
$suppliers = Supplier::active()->get();
if (!$suppliers->contains('id', $record->supplier->id))
{
$suppliers->add(Supplier::find($record->supplier->id));
}
Two questions: Is this the correct way to do this? Should this code be in my controller or should I have it somewhere else? (perhaps a scope but I wouldn't know how to code that).
Edit:
Thanks for the help guys. I have applied advice from each of the answers and refactored my code into a new scope:
public function scopeActiveIncluding($query, Model $model = null)
{
$query->where('active', 1);
if ($model && !$model->supplier->active)
{
$query->orWhere('id', $model->supplier->id);
}
}
What you've written will work, but the Collection::contains function can potentially be pretty slow if the collection is large.
Since you have the id, I would probably make the following change:
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
Of course, the downside to this is that you may be making an unnecessary query on the database.
So you have to consider:
is the record's supplier more likely to be active or inactive?
is the size of the collection of active suppliers large enough to justify another (potentially wasted) call to the database?
Make the choice that makes the most sense, based on what you know of your application's data.
As for the second question, if you will only need this specific set of suppliers in this one part of your application, then the controller is a good place for this code.
If, however, you will need this particular set of suppliers in other parts of your application, you should probably move this code elsewhere. In that case, it might make sense to create a function on the the related model (whatever type $record is...) that returns that model's suppliers set. Something like:
public function getSuppliers()
{
$suppliers = Supplier::active()->get();
$supplier = $this->supplier;
if (!$supplier->active) {
$suppliers->add($supplier);
}
return $suppliers;
}
I saw #Vince's answer about 1st question, and I'm agree with him.
About 2nd question:
Write scope in Supplier model like this:
public function scopeActive($query){
$query->where('active', 1); // for boolean type
}
For good practice, you need to write the logic parts in services like "App\Services\SupplierService.php". And there write the function you want:
public function activeSuppliersWithCurrent($record) {
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
}
In your SupplierController's constructor inject the instance of that service and use the function, for example:
use App\Servives\SupplierService;
protected $supplierService = null;
public function __construct(SupplierService $supplierService) {
$this->supplierService = $supplierService;
}
public function getActiveSuppliersWithCurrent(...) {
$result = $this->supplierService->activeSuppliersWithCurrent($record);
}
As you can see, later you will not need to change anything in controller. If you'll need to change for example the query of suppliers selection, you will just have to change something only in service. This way will make your code blocks separated and shorter.
Also the sense for this pattern: you don't need to access the models from controller. All logic related with models will implemented in services.
For other projects you can grab only services or only controllers, and implement another part differently. But in that case if you had all codes in controller, that will prevent you to grab the portions of necessary codes, cuz may you don't remember what doing each blocks...
You could add a where clause to the query to also find that id.
$suppliers = Supplier::active()->orWhere('id', $record->supplier->id)->get();
You could potentially slide this into the active scope by passing the 'id' as an argument.
public function scopeActive($query, $id = null)
{
$query->where('active', true);
if ($id) {
$query->orWhere('id', $id);
}
}
Supplier::active($record->supplier->id)->get();
Or make another scope that does this.

Using Active Record, how can I save child's detail information through its parent?

I'm using parent->child (master->detail) relation in Yii2 Active Record
When I want to create a child, I have to manually fill its parent info like this:
Relation: Client (1) ---> (n) Comments
class ClientController extends \yii\web\Controller
{
public function actionAddComment() {
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->client = $this->id; // Client id
$comment->save();
}
return $this->render('view', ['comment'=>$comment]);
}
}
I've optimized it, creating a Comment method to do that:
class Comment extends ActiveRecord {
public function newComment($client) {
$comment = new Comment;
$comment->client = $client; // Client id
return $comment;
}
}
And I have gone through beforeSave in the Comment model, but still not sure if there is a better way.
Is there anything like:
$comment = new Comment(Yii::$app->request->post());
$client->save($comment); // Here the parent is writing his information to the child
Or one-liner shortcut:
$client->save(new Comment(Yii::$app->request->post());
Without having to create this logic in beforeSave?
Yes, I recommend to use the built in link() and unlink() methods provided by Active Record which you can use in your controller to relate or unrelate 2 models either they share many-to-many or one-to-many relationship.
It even has an optional $extraColumns attribute for additional column values to be saved into a junction table if using it link( $name, $model, $extraColumns = [] )
So your code may look like this :
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->link('client', $this);
}
check docs for more info.
Now about where to use this code to relate models, it depend on how your app is structured. I'm not sure if doing that through a triggered event would be a good practice, you need to remember that errors may happens and
you may need to evaluate certain scenarios or logic before throwing exceptions. So in my case, I prefer to use that code into my Controllers.
Sometimes you need to build a specific action like you did actionAddComment(), In certain other cases like when your Post request is meant to update the Parent model and also update its related child models at once, the Parent's Update Action ClientController::actionUpdate() may be a good place to do so, maybe something like this will do the job :
$params = Yii::$app->request->post();
$client->load($this->params, '');
if ($client->save() === false && !$client->hasErrors()) {
throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
}
foreach ($params["comments"] as $comment) {
// We may be sure that both models exists before linking them.
// In this case I'm retrieving the child model from db so I don't
// have to validate it while i just need its id from the Post Request
$comment = Comment::findOne($comment['id']);
if (!$comment) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
// according to its documentation, link() method will throw an exception if unable to link the two models.
$comment->link('client', $client);
...

One to many relationship count - difference in accessing relationship

I have one to many relation - Entry can have many Visits.
In my Entry model I have the following methods:
public function visits() {
return $this->hasMany ('Visit', 'entry_id','id');
}
public function visitsCount() {
return $this->hasMany('Visit', 'entry_id','id')
->selectRaw('SUM(number) as count')
->groupBy('entry_id');
}
In Blade I can get number of visits for my entry using:
{{$entry->visits()->count() }}
or
{{ $entry->visitsCount()->first()->count }}
If I want to create accessor for getting number of visits I can define:
public function getNrVisitsAttribute()
{
$related = $this->visitsCount()->first();
return ($related) ? $related->count : 0;
}
and now I can use:
{{ $entry->nr_visits }}
Questions:
In some examples I saw defining such relation this way:
public function getNrVisitsAttribute()
{
if (!array_key_exists('visitsCount', $this->relations)) {
$this->load('visitsCount');
}
$related = $this->getRelation('visitsCount')->first();
return ($related) ? $related->count : 0;
}
Question is: what's the difference between this and the "simple method" I showed at the beginning? Is it quicker/use less resource or ... ?
Why this method doesn't work in this case? $related is null so accessor return 0 whereas using "simple method" it returns correct number of visits
I've tried also changing in visitsCount method relationship from hasMany to hasOne but it doesn't change anything.
1 Your relation won't work because you didn't select the foreign key:
public function visitsCount() {
// also use hasOne here
return $this->hasOne('Visit', 'entry_id','id')
->selectRaw('entry_id, SUM(number) as count')
->groupBy('entry_id');
}
2 Your accessor should have the same name as the relation in order to make sense (that's why I created those accessors in the first place):
public function getVisitsCountAttribute()
{
if ( ! array_key_exists('visitsCount', $this->relations)) $this->load('visitsCount');
$related = $this->getRelation('visitsCount');
return ($related) ? $related->count : 0;
}
This accessor is just a handy way to call the count this way:
$entry->visitsCount;
instead of
$entry->visitsCount->count;
// or in your case with hasMany
$entry->visitsCount->first()->count;
So it has nothing to do with performance.
Also mind that it is not defining the relation differently, it requires the relation to be defined like above.
Assuming your schema reflects one record / model per visit in your visits table, The best method would be to get rid of the visitsCount() relation and only use $entry->visits->count() to retrieve the number of visits to the entry.
The reason for this is that once this relation is loaded, it will simply count the models in the collection instead of re-querying for them (if using a separate relationship)
If your concern is overhead and unnecessary queries: My suggestion would be to eager-load these models in a base controller somewhere as children of the user object and cache it, so the only time you really need to re-query for any of it is when there have been changes.
BaseController:
public function __construct(){
if(!Cache::has('user-'.Auth::user()->id)){
$this->user = User::with('entries.visits')->find(Auth::user()->id);
Cache::put('user-'.Auth::user()->id, $this->user, 60);
} else {
$this->user = Cache::get('user-'.Auth::user()->id);
}
}
Then set up an observer on your Entry model to flush the user cache on save. Another possibility if you are using Memcached or Reddis would be to use cache tags so you don't have to flush the whole user's cache every time an Entry model is added or modified.
Of course, this also assumes that each Entry is related to a user, however, if it isn't and you need to use Entry alone as the parent, the same logic could apply, by moving the Cache class calls in your EntryController

Resources