Laravel Eloquent: Store model together with related submodels - laravel

i got the models Exam and Question. Simplified code:
class Exam {
public function questions() {
return $this->hasMany('Question');
}
// ...
}
class Question {
public function exam() {
return $this->belongsTo('Exam');
}
// ...
}
Now i wanna upload a file with exam data. The parser class shall create the exam without saving it. Then gradually the questions should be created and added to the exam. At the end everything should be saved in a transaction.
class ExamParser {
$exam = new Exam();
// ...
while ($linesRemaining) {
$question = new Question();
// ...
$exam->questions[] = $question; // something like this?
}
$exam->saveTogetherWithQuestions(); // how do i realize this?
}
I know basically how to save related models but not how I can just relate them and save the whole construct later.

Create an array of Question first. Then create an Exam, and save all questions via Exam's relations. Put all codes in a transaction.
Updated: Moved model creation out of transaction execution.
$questions = [];
while ($linesRemaining) {
$question = new Question();
// ...
$questions[] = $question;
}
$exam = new Exam();
// ...
DB::transaction(function() {
$exam->save();
$exam->questions()->saveMany($questions);
});
Important note that you have to save Exam before attach Question to it. Parent Exam must have its own primary key first (by saving itself) to allow attaching success. Otherwise, you will have Question saved without parent's id.

Related

Deleting nested comments and related data from laravel controller

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();

Get all badges with user progress if any

I have a many to many relationship between users and achievements. Table layout is similar to this.
users
-id
-first_name
-last_name
-email
acheivements
-id
-type-name
-description
achievement_user
-id
-acievement_id
-user_id
-is_complete (boolean)
-percentage_complete (integer)
-completed_at
I can get the bulk of the stuff I want such as all achievements with type = badge for a user that are in the pivot table. I can also get a list of all the achievements that have type = badge. Where I am stuck is trying to get all achievements with type = badge along with is_complete, percentage_complete, completed_at for that given user. So if there are 8 total badges and they have started 3 of them I would want to return 8 records where 3 of them would have the information from the pivot table. I know I could grab all badges and loop through them to add on additional information but I know there is a eloquent way that would be easier.
Thank for any help.
This is in response to you recent comment.
To achieve what you want, I have introduced a new method on the User model called achievementsblock, which uses php array_map method.
I have not tested the code, but you should get the idea.
//in User model
public function achievements()
{
return $this->belongsToMany('App\Achievements')->withPivot('is_complete', 'percentage_complete', 'completed_at');
}
//in User model
public function achievementsblock()
{
$all_achievements = Achievement::all();
return array_map(function($temp_achievement){
$achievement_ids = $this->achievements()->pluck('id');
$new_temp = new \stdClass();
$new_temp->type_name = $temp_achievement->type_name;
$new_temp->description = $temp_achievement->description;
if(in_array($temp_achievement->id, $achievement_ids)){
$user_achievement = $this->achievements()->where("achievements.id","=",$temp_achievement->id)->first();
$new_temp->is_complete = $user_achievement->is_complete;
$new_temp->percentage_complete = $user_achievement->percentage_complete;
$new_temp->completed_at = $user_achievement->completed_at;
}
else {
$new_temp->is_complete = 0;
$new_temp->percentage_complete = 0;
$new_temp->completed_at = null;
}
return $new_temp;
}, $all_achievements);
}
In your controllers, you can get the achievementsblock for a user as follows:
$user_achievement_blocks = User::first()->achievementsblock();
On your belongsToMany relationship, use the withPivot function as follows:
//in User model
public function acheivements()
{
return $this->belongsToMany('App\Acheivement')->withPivot('is_complete', 'percentage_complete', 'completed_at');
}
//in Acheivement model
public function users()
{
return $this->belongsToMany('App\User')->withPivot('is_complete', 'percentage_complete', 'completed_at');
}
You need to learn about whereHas method which lets you query on relationship while getting results
In User model
// create achievements relationship
public function achievements()
{
return $this->belongsToMany(Achievement::class)
->withPivot('is_complete', 'percentage_complete', 'completed_at');
}
//then query it like shown below.
$achievements= $user->achievements;
Read more here

How to check model is update , insert or delete in Yii2

I have a model in Yii.
How I can identify Yii model is being update, insert or delete after I save it ($model->save() || $model->delete())?
With $model->isNewRecord you can check if the model will be saved or updated. With ($model->save() || $model->delete()) you cannot directly determine what has happend. If $model->save() was not successful, then because of validation errors. $model->delete() will be called. This will be 1 (i.e. true) if deletion was successfull.
I don't know why you have this statement, but if you cannot change it for some reason and want to know what is going on you could use the events:
$model->on(yii\db\BaseActiveRecord::EVENT_AFTER_INSERT, function(yii\db\AfterSaveEvent $e) {
// model was inserted
});
$model->on(yii\db\BaseActiveRecord::EVENT_AFTER_UPDATE, function(yii\db\AfterSaveEvent $e) {
// model was updated
});
$model->on(yii\db\BaseActiveRecord::EVENT_AFTER_DELETE, function(yii\base\Event $e) {
// model was deleted
});
If you can extend the model class you could overwrite the corresponding methods instead of attaching event handlers. Then you could create a intermediate class that deals with what you need to do, and let derive any model classes from that intermediate class:
class ExtendedActiveRecord extends yii\db\ActiveRecord {
public function afterSave($insert, $changedAttributes) {
// Do anything you want
if($insert)
// model was inserted
else
// model was deleted
parent::afterSave($insert, $changedAttributes);
}
public function afterDelete() {
// Do anything you want
parent::afterDelete();
}
}
class ModelA extends ExtendedActiveRecord {
....
}
class ModelB extends ExtendedActiveRecord {
....
}
If you want only log what is happening with your models, you may use one of audit trails extensions: bedezign/yii2-audit or sammaye/yii2-audittrail
If you want to write your own behavior you may be interested by $dirtyAttributes property which holds attributes that has been changed after save/fetch from database.
The easiest way is to check when saving. What I mean is that when you save your model, it will return a true/false value, and in the case of a false the model will contain an array or error messages.
That's the easiest way also here you can solve problems, send alerts, print debug info, or do something regarding an error.
$model = new MyTable();
$model->name = example;
if($model->save()){
//Nice its inserted/updated, go ahead
} else {
//Woops error here, not inserted
}
Go ahead, give it a try.
Read more here: https://www.yiiframework.com/doc/api/2.0/yii-db-baseactiverecord#save()-detail

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 relation - how to implement push

I am trying to do an insert which will create a parent record for one table and then insert records that link back to the parent record into another.
In other words this: User completes Course information form, then completes a series of questions on the same page. On submission, the course information is inserted into its own table then questions are inserted into a separate one.
My Course model is this:
class CourseVerification extends Eloquent
{
public function courseverificationqanda()
{
return $this->hasMany('CourseVerificationQandA', 'id', 'verification_id');
}
My Question model is this:
class CourseVerificationQandA extends InnovedBaseModel
{
public function courseverification()
{
return $this->belongsTo('CourseVerification');
}
On form submission, my controller is doing this:
// create course verification record first
$veri = new CourseVerification;
$veri->verification_date = $input['verification_date'];
// create collection to store questions
$collection = new Illuminate\Database\Eloquent\Collection();
// loop through submitted questions and push them to the collection
for($i = 0; $i < count(Input::get('question_id')); $i++) {
$answer = new CourseVerificationQandA;
$answer->question_id = $input['question_id'][$i];
$answer->answer = $input['answer'][$i];
$answer->additional_notes = $input['additional_notes'][$i];
$collection->push($answer);
}
// add collection to quesetion relation
$veri->courseverificationqanda = $collection;
// insert both course record and questions
$veri->push();
The push method then errors and debugs the SQL command
insert into `CourseVerification`
(`verification_date`, `topic_id`, `course_id`, `verifier_id`,`iv_status`,
`verification_type`, `courseverificationqanda`, `created_by`)
values
(29/10/2014, 1294, 47, 1, 1, I, [{"question_id":"2","answer":"0","additional_notes":""},
{"question_id":"3","answer":"0","additional_notes":""},
{"question_id":"4","answer":"0","additional_notes":""}], 1)
As you can see, the assignment of the collection to $veri->courseverificationqanda is then getting treated as a table column in the SQL query when it is actually a relationship to the question table.
Any ideas?
You have a few mistakes there.
You don't assign collection to the relation. You need to load that relation (as a collection in this case) and push on it, then just save everything:
$veri = new CourseVerification;
$veri->verification_date = $input['verification_date'];
// Save the parent model before you can add its children
if ($veri->save())
{
// loop through submitted questions and push them to the collection
for($i = 0; $i < count(Input::get('question_id')); $i++) {
$answer = new CourseVerificationQandA;
// associate with the parent model
// explicitly
$answer->verification_id = $veri->getKey();
// or the eloquent way
// $answer->courseverification()->associate($veri);
$answer->question_id = $input['question_id'][$i];
$answer->answer = $input['answer'][$i];
$answer->additional_notes = $input['additional_notes'][$i];
$veri->courseverificationqanda->push($answer);
}
}
// save both course record and questions
$veri->push();
Another thing are your relations, which are both wrong:
// this is how it should look like
// CourseVerification
public function courseverificationqanda()
{
return $this->hasMany('CourseVerificationQandA', 'verification_id', 'id');
}
// QandA
public function courseverification()
{
return $this->belongsTo('CourseVerification', 'verification_id');
}
In firsts case you swapped the keys, so it would work but not the way it should.
In second case you didn't specify the foreign key at all, so Eloquent would look for courseverification_id in the table (method_name_id).

Resources