Fetch Multiple Records with Joins in Laravel Eloquent - laravel

I'm very new to using Laravel, thus to Eloquent. I am getting kind of confused about Eloquent's table relationships.
Now I understand how to implement simple joins, like the example in Laravel 4.2's documentation which for one-to-many relationships, where a comment belongs to one post, but a post can have many comments. They use this syntax to get the comments from one post:
Post::find(1)->comments;
Which, in MySQL, is probably like:
SELECT * FROM comments
JOIN posts ON posts.id=comments.post_id
WHERE posts.id=1
What if the results I'm trying to get is something like this that gets more than just one row:
SELECT * FROM comments
JOIN posts ON posts.id=comments.post_id
I know it doesn't make much sense based on the example I gave above. But how do I do it in Eloquent?
To give further details, what I'm actually trying to do is to display joined results from my two tables, assets and asset_classifications. An asset belongs to one asset_classification, and an asset_classification has many assets.
I'm trying to display a tabular data of assets which include their asset_classifications. In MySQL, it's something like this:
SELECT * FROM assets
JOIN asset_classifications ON asset_classifications.id=assets.classification_id
How can I perform it in Eloquent?

I guess you're a little too attached to SQL :) Try thinking outside of joins and queries an more in models and melattionships, because Laravel handles all the abstraction for you.
So you have an Asset model:
class Asset extends Eloquent
{
public function classification()
{
return $this->belongsTo('AssetClassification');
}
}
...and an AssetClassification mdoel:
class AssetClassification extends Eloquent
{
public function assets()
{
return $this->hasMany('Asset');
}
}
And now they are linked and you can do whatever you want. If you want to output all assets and their classification, no problem:
$assets = Asset::all();
foreach($assets as $asset)
{
echo "{$asset->name}" is classified as {$asset->classification}";
}
Or the other way around:
$classifications = AssetClassification::all();
foreach($classifications as $classification)
{
echo "{$classification->name} has the following assets:";
foreach($classification->assets as $asset)
{ ... }
}
As an array, this would look something like
[0] => [
'id' => 1
'name' => 'asset_name_1',
],
[1] => [
'id' => 2
'name' => 'asset_name_2',
],
You get the idea. The problem is, that you do a separate query on each iteration. That's why you should use eager loading to not also load all Assets but also their dependencies:
$assets = Asset::with('classification')->get();
Now you would have an array like this:
[0] => [
'id' => 1
'name' => 'asset_name_1',
'classification' => AssetClassiciation-Object
],
[1] => [
'id' => 2
'name' => 'asset_name_2',
'classification' => AssetClassiciation-Object
],
So now you can loop through assets and their classification without making any further SQL-queries.

Related

firstOrNew with "or" statement

I have a table with columns like "name", "surname", "user_id", and I need to check if entry exists first by id, and then by name and surname together, if there is none, create it. How do I do it neatly, instead of making two update statements, and if both return 0 just create a new one (which seems too bulky)
I thought of using firstOrNew, but it seems that it only can work while matching all of the parameters.
Is there any method I've missed that would apply well to my situation?
You could try something like this (assuming you want to create a model [saved to the database]):
$attributes = [
'name' => ...,
'surname' => ...,
];
$model = Model::where('id', $id)
->orWhere(fn ($q) => $q->where($attributes))
->firstOr(fn () => Model::create($attributes));
This would search for a record by id OR name and surname. If it doesn't find one it will create a new record with the name and surname (assuming those attributes are fillable on the Model).
Laravel 8.x Docs - Eloquent - Retrieving Single Models / Aggregates firstOr

How do I return the ID field in a related table in Laravel request

I have two related tables and I want to return all fields including the ID (key) field. My query below returns all fields except the ID. how do I return the ID field from one of the tables?
'programmes' => ProgrammeInstances::with('programmes')->get(),
the query below returns Unknown column 'programmes.programme_title' as it is looking for it in the table 'programme_instances'
'programmes' => ProgrammeInstances::with('programmes')->select('programmes.programme_title', 'programmeInstances.id', 'programmeInstances.name', 'programmeInstances.year')->get(),
Laravel provides multiple relationships, one of these is the hasMany() relationship which should return a collection where a User hasMany rows inside of your database
For example, inside your User model :
public function programmes() {
return $this->hasMany(Program::class);
}
Now in your controller, you can do :
public function edit($id) {
$programmes = User::find($id)->with('programmes')->get();
return view('user.edit')->with('programmes', $programmes);
}
And then you can loop over it inside your view
#forelse($programmes->programmes as $program)
// provide the data
#empty
// the user doesn’t have any programmes
#endforelse
a solution i found below - still not sure why ID isnt automatically returned when i get all fields, but works when i specify individual fields:
'programmes' => ProgrammeInstances::with('programmes')
->get()
->transform(fn ($prog) => [
'programme_title' => $prog->programmes->programme_title,
'id' => $prog->id,
'name' => $prog->name,
'year' => $prog->year,
]),

Best way to handle a model relationship status/state when unit testing in Laravel 6?

I have the following unit test in Laravel 6.x, using SQLite:
<?php
/** #var \Illuminate\Database\Eloquent\Factory $factory */
use App\Entry;
use App\EntryStatus;
use Faker\Generator as Faker;
$factory->define(Entry::class, function (Faker $faker) {
return [
'user_id' => 1,
'caption' => $faker->sentence(10),
'entry_status_id' => EntryStatus::where('name', 'pending')->first()->id,
];
});
$factory->state(Entry::class, 'awaiting_payment', [
'entry_status' => EntryStatus::where('name', 'awaiting_payment')->first()->id,
]);
I have a test error of 'Trying to get property 'id' of non-object', which is for the following line:
'entry_status' => EntryStatus::where('name', 'awaiting_payment')->first()->id,
I have a few ideas on how to fix this error, but I'm wondering what the best practice here would be in terms of unit testing and 'The Laravel way'.
The way I have thought about this, I have an EntryStatus table which has static statues 'pending' => 0, 'awaiting_payment' => 1, 'paid' => 2 etc. And I create a relationship within my App\Entry model > App\EntryStatus. Ideas are as follows:
The original plan; for the unit tests, I need to seed the static EntryStatus table each time for each test. Looking at the docs, I would use something like setUp() > $this->artisan('db:seed'). But this feels like it would really slow the tests. Unless there is a way to seed the DB once before all tests are started.
Try and create a factory which creates static data ('pending' => 0, 'awaiting_payment' => 1) but I'd manually need to update the database seed and the factory each time to match which seems clunky.
Within the Laravel docs testing documents they have this example; $factory->state(App\User::class, 'delinquent', ['account_status' => 'delinquent',]). This makes me think I could just remove the relationship table EntryStatus completely and just use a string column in the Entry to represent the state. I think this would be the best solution, but I'm worried that there is a reason we use ids as statuses as I'd imagine that they are quicker in search queries. But if not, my statues will be fixed, and this seems like the most eloquent solution.
Another option would be to store the statues as integers anyway, keeping a note of what status is what integer, but again this doesn't seem correct.
The record is not created yet and its not a good idea to assign static relationships this way in factories.
You can use a factory create instead then assign the relationship inside the test case.
For example:
Entry factory
$factory->define(Entry::class, function (Faker $faker) {
return [
'user_id' => factory(User)->create()->id,
'caption' => $faker->sentence(10),
'entry_status_id' => factory(EntryStatus)->create()->id,
];
});
EntryStatus factory
$factory->define(EntryStatus::class, function (Faker $faker) {
return [
'name' => $faker->word,
'entry_status' => $faker->numberBetween(0, 2) //Its better start from 1 though
];
});
User factory
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->word
];
});
In your test case start linking things together (if you need to).
/**
* #test
*/
public function exampleTestCase()
{
$enteryStatus = factory(EntryStatus::class)->create(['entry_status' => 1]);
//create 6 entries
$entry = factory(Entry::class, 6)->create(['entry_status_id' => $enteryStatus->id]);
//TODO: assert something
}
You can check afterCreatingState
$factory
->state(EntryStatus::class, 'awaiting_payment', ['name' => 'awaiting_payment'])
->afterCreatingState(EntryStatus::class, 'awaiting_payment', function ($entryStatus, $faker) {
factory(Entry::class)->create([
'entry_status_id' => $entryStatus->id,
]);
});

Eloquent many to many to many record attach

I have two tables.
message - id, title, content
user - id, name
I have a pivot table.
message_user - message_id, user_id_sender, user_id_receiver
A message can be sent by 1-0 (sender can be null) users, defined with user_id_sender
A message can be received by 1-N users, defined with user_id_receiver
I am trying to define this in Eloquent.
The only solution I have is:
class Message
public function users()
{
return $this->belongsToMany(
'User',
'message_user',
'user_id_sender',
'user_id_receiver');
}
// Somewhere in the code
$message = $this->messageRepository->store($message);
$message->users()->attach($message->id, array(
'message_id' => $message->id,
'user_id_sender' => $userSender->id,
'user_id_receiver' => $userReceiver->id
));
This is the only solution I could think of, but it feels like a workaround. Is there a proper way to achieve this?
You could put into your Message model this:
public function betweenUsers($sender, $receiver) {
return $this->users()->attach($this->id, [
'message_id' => $this->id,
'user_id_sender' => $sender->id,
'user_id_receiver' => $receiver->id,
]);
}
Then you could somewhere in the code do $message->betweenUsers($userSender, $userReceiver);
I understand it is not a exactly what you wanted, but since you are really uncommon kind of pivot table, this is probably sufficient too.

yii find models through related models by using having

For example I have two related models with MANY_MANY relation.
Need to find all models which name contains 'test' or in which relation model2.name contains 'test'.
On sql I wrote this query and it's what I want to get from ActiveRecord in result by using standard relations mechanism and CDbCriteria.
SELECT
m1.*
FROM
model1 m1
LEFT JOIN model_model mm
ON mm.from_id = m1.id
LEFT JOIN model2 m2
ON mm.to_id = m2.id
GROUP BY m1.id
HAVING (
m1.name LIKE '%test%'
OR GROUP_CONCAT(m2.name) LIKE '%test%'
);
Simple use Activerecord.findBySql is not good solution because I have many models such as above. So for faster combination any models a relations is prefered.
When I use CDbCriteria.with Yii generate 2 query.
When I use CDbCriteria.with with CDbCriteria.together Yii tried to select all columns from related tables, it's redundantly and may be slowly in future because number of relations can become much more than in this example.
Have any idea?
Thanks.
You should define the relation in "M1" model class:
public function relations()
{
return array(
'M2s' => array(self::MANY_MANY, 'M2',
'model_model(from_id, to_id)'),
);
}
In your M2 model create a scope:
public function nameScope($name)
{
$this->getDbCriteria()->mergeWith(array(
'condition'=>'name LIKE :name',
'params'=>array(':name'=>"%{$name}%"),
));
return $this;
}
If your ready you can do this:
M1::model()->findAll(
array(
'condition' => 'name LIKE %test%',
'with' => array(
'M2s' => array(
'scopes' => array(
'nameScope' => 'test',
)
)
),
)
);

Resources