Is $post->comments()->create() more expensive than Comment::create() in Laravel? - php

In Laravel PHP Framework, when you have, let's say, a relationship between two tables e.g. one post can have one or multiple comments, you can create the comments of the post these ways:
// Option 1
$post->comments()->create(['text' => 'Greate article...']);
or
// Option 2
Comment::create([
'post_id' => 1,
'text' => 'Greate article...',
]);
Of course, it depends on the cases. Below are my cases.
For both options, the Post ID 1 has already been validated in the form request whether the post with the ID 1 does exist or not.
For some reasons, I already need to retrieve the post from the database first, thus I already have the post model.
In these above cases, is the Option 1 more expensive than the Option 2?

You can test the queries your application makes by listening to the queries using DB::listen().
I have setup the following as a test:
Migrations:
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('content');
$table->timestamps();
});
Schema::create('comments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('post_id');
$table->string('text');
$table->timestamps();
});
Models:
class Post extends Model
{
protected $guarded = [];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
class Comment extends Model
{
protected $guarded = [];
public function post()
{
return $this->belongsTo(Post::class);
}
}
Test:
$post = Post::create([
'title' => 'Hello World',
'content' => 'Here I am!',
]);
$queries = collect();
DB::listen(function ($query) use ($queries) {
$queries->push($query->time);
});
for ($i = 1; $i <= 3000; $i += 1) {
Comment::create([
'post_id' => $post->id,
'text' => 'Facade '.$i,
]);
$post->comments()->create([
'text' => 'Relation '.$i,
]);
}
$totalTime = [0, 0];
foreach ($queries as $idx => $time) {
$totalTime[$idx%2] += $time;
}
return [
'facade' => $totalTime[0],
'relation' => $totalTime[1],
];
This outputs:
array:2 [▼
"facade" => 1861.3
"relation" => 1919.9
]
So you can see the relation way of create is actually approximately 3% slower in my test scenario.
I have prepared this implode if you want to experiment further.

If it is "cheaper", it won't be by a noticeable amount. You're creating an instance of a Query builder, but you're not actually querying anything. So in this case it's only adding the post_id on the new model you're creating.
I don't think it's something you worry about too much.

Related

Store parent_id in laravel

I'm new to Laravel so I struggle. I have a comment system that worked perfectly fine but now I want to also add a reply system to it. So the way I decided to do it, is by adding a parent_id column to the comments table and then check if a comment has a parent. But I don't know how exactly the store method, in this case, should work. Here's my database set up for comments:
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body');
$table->timestamps();
});
}
And now a set up for the reply column:
public function up()
{
Schema::table('comments', function (Blueprint $table) {
$table->unsignedBigInteger('parent_id')->nullable();
$table->foreign('parent_id')->references('id')->on('comments');
});
}
Model:
class Comment extends Model{
use HasFactory;
protected $guarded = [];
public function post()
{
return $this->belongsTo(Post::class);
}
public function author()
{
return $this->belongsTo(User::class, 'user_id');
}
public function replies() {
return $this->hasMany('App\Comment', 'parent_id');
}
}
Controller:
public function store(Post $post){
request()->validate([
'body' => 'required'
]);
$post->comments()->create([
'user_id' => request()->user()->id,
'parent_id' => request()->get('id'),
'body' => request('body')
]);
return back();
}
I just don't know how exactly I can get parent_id in the store function so I would appreciate some suggetstions
it should be the comment id that got the reply
something like this
$post->comments()->create([
'user_id' => request()->user()->id,
'parent_id' => request()->get('comment_id'),
'body' => request('body')
]);
I hope it's helpful
use code :
public function store(Post $post) {
request()->validate([
'body' => 'required'
]);
$post->comments()->create([
'user_id' => request()->user()->id,
'parent_id' => request()->get('comment_id'),
'body' => request('body')
]);
return back();
}

Laravel 8: Relationships Between User and Posts. Outputting Data and Storing into Database

This is a continuation of my last question.
I like to create a relationship between a user (with an account type that’s equal to a “profile”) and my job posts. What I did was create a relationship like this in my models (not sure if correct tho)
User.php
public function jobposts()
{
$this->hasMany(JobPost::class)->where('account_type', 'profile');
}
JobPost.php
public function userprofile()
{
$this->belongsTo(User::class)->where('account_type', 'profile');
}
JobPostController.php
public function store(Request $request)
{
$this->validate($request, [
'job_name' => 'required|max:100',
'describe_work' => 'required|max:800',
'job_category' => 'required|not_in:0',
'city' => 'required|not_in:0',
'state' => 'required|not_in:0',
'zip' => 'required|regex:/\b\d{5}\b/',
]);
dd(auth()->user()->jobpost()->job_name);
}
2021_11_20_211922_create_job_posts_table.php
Schema::create('job_posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->contrained()->onDelete('cascade');
$table->string('job_name');
$table->text('describe_work');
$table->string('job_category');
$table->timestamps();
});
Got 2 questions about what I can do in the JobPostController.php.
How do I dd() to test the output?
This seems wrong
dd(auth()->user()->jobpost()->job_name);
How do I add it correctly into the DB like this?
public function store(Request $request)
{
$request->user()
->jobpost()
->create([
'job_name' => $request->job_name
]);
}

Laravel Quiz Storing The correct answers

I am very new to the Laravel, so I have a project for making a quiz. Currently I achieved to store my questions with radio-answers in the database, but I do not know how to:
1) Display all the questions with answers on web page.
2) Store the points for the user on each correct answer.
UPDATE: Thanks to ettdro I have solved my 1st problem. Only my 2nd left.
I would appreciate any help.
My Answer.php Model is empty for now. My Question.php Model:
class Question extends Model
{
// connect the models by adding a relationship to the Question model
public function answers()
{
return $this->hasMany(Answer::class);
}
}
My up function in Migration for Questions is:
public function up()
{
Schema::create('questions', function (Blueprint $table) {
$table->id();
$table->string('text');
$table->integer('points')->unsigned();
$table->timestamps();
});
}
My up function in Migration for Answers is:
public function up()
{
Schema::create('answers', function (Blueprint $table) {
$table->id();
// since answer is connected to the question
$table->integer('question_id');
$table->string('text');
$table->boolean('correct_one');
$table->timestamps();
});
}
My QuestionAnswerSeeder.php is:
// for filling the tables
class QuestionAnswerSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
// truncating the tables and then store each question and its answers.
public function run()
{
Question::truncate();
Answer::truncate();
$questionAndAnswers = $this->getData();
$questionAndAnswers->each(function ($question) {
$createdQuestion = Question::create([
'text' => $question['question'],
'points' => $question['points'],
]);
collect($question['answers'])->each(function ($answer) use ($createdQuestion) {
Answer::create([
'question_id' => $createdQuestion->id,
'text' => $answer['text'],
'correct_one' => $answer['correct_one'],
]);
});
});
}
// for the actual data, I use a separate getData method to keep it cleaner
// in this method, I return a big collection with all the questions and answers
private function getData()
{
return collect([
[
'question' => 'When did the World War 2 end?',
'points' => '1',
'answers' => [
['text' => '1939', 'correct_one' => false],
['text' => '1941', 'correct_one' => false],
['text' => '1945', 'correct_one' => true],
],
],
[
'question' => 'Who discovered America?',
'points' => '1',
'answers' => [
['text' => 'Adolf Hitler', 'correct_one' => false],
['text' => 'Napoleon Bonaparte', 'correct_one' => false],
['text' => 'Christopher Columbus', 'correct_one' => true],
],
],
]);
}
}
You should have a QuestionController.php that has this content:
/**
* In this function, you need to get all the data you want to pass to your view
* and send it to the compact function in the return statement.
*/
public function index() {
// This will return a collection of questions including their answers.
$questionsCollection = Question::all();
return view('myquestionsview', compact('questionsCollection'));
}
Next, because you returned myquestionsview in the index function, you will need a file named: myquestionsview.blade.php in the views folder.
To display the informations of your questions, in your myquestionsview.blade.php you should have something like
#foreach ($questions as $question)
{{ $question->text }}
// Now, we want to display each answers of the question.
#foreach ($question->answers as $answer)
{{ $answer->text }}
#endforeach
#endforeach
This is basically what you want to do for your first question.

Saving multiple IDs in a HasMany Eloquent relationship

I have a model Foo, which has many Bars:
class Foo extends Model
{
public function bars()
{
return $this->hasMany('App\Bar');
}
}
class Bar extends Model
{
public function foo()
{
return $this->belongsTo('App\Foo');
}
}
When saving a new Foo, the request payload comes with an array of Bar ids. I want to save these at the same time. This works:
public function store(StoreFoo $request)
{
$foo = Foo::create($request->validated());
foreach ($request->barIds as $barId) {
$foo->bars()->create(['bar_id' => $barId]);
}
}
My question is: is there a way to do this without a loop? I've tried sync and attach but these aren't applicable in this case.
The only way I can think of that you can achieve this without writing a loop yourself is by using the saveMany method on the HasMany relation. You can create instances of your Bar model and pass them all as an array to the saveMany method and that will save all of them and return an array of the created entities in response.
$foo->bars()->saveMany([new Bar(['id' => 1]), new Bar(['id' => 2])]);
That being said, Laravel uses a loop to save these models one by one under the hood so it doesn't really do much different to what you're doing now.
Similarly, there's also a createMany method that you can use in the same way as saveMany but instead of providing newly created models, you can provide arrays of attributes instead.
migration table sample
Schema::create('logs', function(Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id')->default(0)->index();
$table->string('type', 10)->index(); // add, update, delete
$table->string('table', 50)->index();
$table->unsignedBigInteger('row');
$table->dateTime('created_at');
});
Schema::create('log_fields', function(Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('log_id')->index();
$table->string('field', 50)->index();
$table->longText('old');
$table->longText('new');
});
model Log.php file
class Log extends Model
{
const UPDATED_AT = null;
protected $fillable = [
'user_id',
'type',
'table',
'row'
];
public function logFields()
{
return $this->hasMany(LogField::class);
}
}
model LogField.php file
class LogField extends Model
{
public $timestamps = false;
protected $fillable = [
'field',
'old',
'new'
];
public function log()
{
return $this->belongsTo(Log::class);
}
}
boot function for another model for save change in database.
hook created, updating and deleting for answer your question
public static function boot()
{
parent::boot();
static::created(function($resorce) {
$_log = new Log;
$_log->create([
'user_id' => session('uid', 0),
'type' => 'add',
'table' => $resorce->getTable(),
'row' => $resorce->fresh()->toArray()['id']
]);
return true;
});
static::updating(function($resorce) {
$_log = new Log;
$log = $_log->create([
'user_id' => session('uid', 0),
'type' => 'update',
'table' => $resorce->getTable(),
'row' => $resorce->fresh()->toArray()['id']
]);
foreach($resorce->getDirty() as $field => $new) {
$log->logFields()->create([
'field' => $field,
'old' => $resorce->fresh()->toArray()[$field],
'new' => $new
]);
}
return true;
});
static::deleting(function($resorce) {
$_log = new Log;
$log = $_log->create([
'user_id' => session('uid', 0),
'type' => 'delete',
'table' => $resorce->getTable(),
'row' => $resorce->id,
]);
foreach($resorce->fresh()->toArray() as $field => $value) {
$log->logFields()->create([
'field' => $field,
'old' => '',
'new' => $value
]);
}
return true;
});
}
Hope I have helped you to understand this.

Laravel - Unit Testing with SoftDeletes

I have a basic test which verifies that a user can only see its own posts.
In my test_database I whip up a couple posts, attach some of them to the user with the ModelFactory - which I then test that this user can only see the posts that are linked to the user according to the 'user_id' column in the post records. The test basically asserts that the output of a query is as expected.
I'm using the SoftDeletes trait in my Post model.
I have 'deleted_at' in the 'protected $dates' property on Post Model.
The problem is that the 'deleted_at' field is NOT returned in the factory()->create() output - but it IS present in the query where I fetch all the user's posts.
I was able to solve this by adding the 'deleted_at' field to the factory with value null, but it seems silly to me to have to add this field to every factory.
Why do I explicitly need to do this for 'deleted_at' but not for example for 'created_at' and 'updated_at'?
Code
Post schema
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('company_id')->unsigned();
$table->integer('locale_id')->unsigned();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('active')->default(false);
$table->datetime('start_date')->nullable()->default(null);
$table->datetime('end_date')->nullable()->default(null);
$table->softDeletes();
$table->timestamps();
});
Post factory
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'company_id' => function () {
return factory(App\Company::class)->create()->id;
},
'locale_id' => function () {
return factory(App\Locale::class)->create()->id;
},
'title' => $faker->sentence(rand(1,3)),
'body' => $faker->paragraph(1),
'active' => rand(0,1),
'start_date' => $faker->dateTimeBetween('now', '+2 years')->format('Y-m-d H:i:s'),
'end_date' => $faker->dateTimeBetween('+2 years', '+4 years')->format('Y-m-d H:i:s')
];
});
My unit test
public function test_scope_foruser_only_returns_posts_for_authenticated_user()
{
// Given there are 2 companies
$companies = factory(Company::class, 2)->create();
// With 1 post per company
$postX = factory(Post::class)->create(['company_id' => $companies->first()->id]);
$postY = factory(Post::class)->create(['company_id' => $companies->last()->id]);
// Given I am logged in as user of the first company
$user = factory(User::class)->create([
'typeable_type' => Company::class,
'typeable_id' => $companies->first()->id
]);
// Login
$this->be(User::first());
// When I fetch the posts for User (via company)
$posts = Post::forUser()->get()->toArray();
// Then It returns only posts which the user's company
// is associated with in a properly formatted array
$this->assertCount(1, $posts);
$this->assertEquals([$postX->toArray()], $posts);
}
This is the query scope applied to the Post model as seen above
public function scopeForUser($query)
{
if ($company = auth()->user()->company) {
return $query->where(['company_id' => $company->id]);
}
return $query;
}

Categories