Using whereHasMorph inside a local scope for MophByMany relationships - laravel

I am needing to find out the current players on a given team. I have created a scope for this. The requirements are that it checks a pivot table to see if the left_at field is null or not. If it is Null that means they are still on the team (current player). The relationship is a polymorphic many to many relationship. The reason why it's polymorphic because a team can also have players come and go as well as coaches for the team.
Team.php
/**
* Scope a query to only include current players.
*
* #param \Illuminate\Database\Eloquent\Builder $query
*/
public function scopeCurrentPlayers($query)
{
return $query->whereHasMorph('players', Player::class, function ($query) {
$query->whereNull('left_at');
});
}
**
* Get all players that have been members of the stable.
*
* #return \Illuminate\Database\Eloquent\Relations\MorphByMany
*/
public function players()
{
return $this->morphedByMany(Player::class, 'member')->using(Member::class)->withPivot(['joined_at', 'left_at']);
}
Controller
$currentTeamPlayers = $team->currentPlayers()->get()->pluck('id');
I am expecting to get a collection of current players on the team however I am receiving the following error.
+exception: Symfony\Component\Debug\Exception\FatalThrowableError^ {#5387
-originalClassName: "TypeError"
#message: "Argument 1 passed to Illuminate\Database\Eloquent\Builder::getBelongsToRelation() must be an instance of Illuminate\Database\Eloquent\Relations\MorphTo, instance of Illuminate\Database\Eloquent\Relations\MorphToMany given, called in /Users/jeffreydavidson/Projects/Ringside/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php on line 215"`

whereHasMorph function only works for morphTo associations. Reference: https://laravel.com/docs/8.x/eloquent-relationships#querying-morph-to-relationships
For morphMany associations, you can simply use whereHas. Reference: https://laravel.com/docs/8.x/eloquent-relationships#querying-relationship-existence
So in your case,
public function scopeCurrentPlayers($query)
{
return $query->whereHas('players', Player::class, function ($query) {
$query->whereNull('left_at');
});
}

Related

Laravel Many To Many (Polymorphic) issue

I've been trying to create a Many To Many (Polymorphic) system that saves the state of every update done by a specific user on specific Models (for instance: Company, Address, Language models) with a note field and the updated_by. The goal is to keep track of who updated the model and when, and the note field is a text field that states where in the system the model was updated.
Below is what I created, but I'm open to getting a different solution that, in the end, allows me to accomplish the goal described above.
I've created the model Update (php artisan make:model Update -m) with the migration:
/**
* Run the migrations.
*
* #access public
* #return void
* #since
*/
public function up()
{
Schema::create('updates', function (Blueprint $table) {
$table->id()->comment('The record ID');
$table->bigInteger('updatable_id')->unsigned()->comment('THe model record id');
$table->string('updatable_type')->comment('THe model name');
$table->string('note')->nullable()->comment('The record note');
$table->integer('updated_by')->unsigned()->nullable()->comment('The user ID which updated the record');
$table->timestamps();
$table->softDeletes();
});
}
On the model Update all the $fillable, $dates properties are standard, and the method of the Update model:
class Update extends Model
{
/**
* Method to morph the records
*
* #access public
*/
public function updatable()
{
return $this->morphTo();
}
}
After trying several ways on the different models, my difficulty is getting the relation because when I save to the updates table, it saves correctly. For instance, in the Company model: Company::where('id', 1)->with('updates')->get(); as in the model Company I have the method:
public function updates()
{
return $this->morphToMany(Update::class, 'updatable', 'updates')->withPivot(['note', 'updated_by'])->withTimestamps();
}
Most certainly, I'm doing something wrong because when I call Company::where('id', 1)->with('updates')->get(); it throws an SQL error "Not unique table/alias".
Thanks in advance for any help.
The problem I see here is using morphToMany instead of morphMany.
return $this->morphToMany(Update::class, 'updateable', 'updates'); will use the intermediate table (and alias) updates (3rd argument) instead of using the default table name updateables. Here it will clash with the table (model) updates, so it will produce the error you are receiving.
return $this->morphMany(Update::class, 'updateable'); will use the table updates and should work with your setup.
Do notice that morphMany does not work with collecting pivot fields (e.g. withPivot([..]), it's not an intermediate table. Only morphToMany does.

how to call model method in the model relationship method in laravel

I have four tables namely: users, games, activities. The users and activities tables have Many to Many relationship. And, games and activities have One to Many relationship respectively. Of course I've that fourth pivot table for the Many to Many relationship above, namely, activity_user.
Inside the Game model, I have this relationship method to call all activities owned by that game, like so:
/**
* Get game activities
*
* #return hasMany
*/
public function activitiesFilter()
{
$user_id = auth('api')->id();
return $this->hasMany(Activity::class)->where('status_id', 2)->multiplayer()->whereDoesntHave('users', function($q) use ($user_id){
$q->where('user_id', $user_id);
});//select only this game approved(status_id===2) activities whose multiplayer count have not been reached and which this user have not joined/created
}
To select activities whose multiplayer count have not been reached, I've a scope function inside the Activity model, like so:
/**
* Scope a query to only include activity whose multiplayer count have not been reached.
*note that the activities table have an int field for multiplayer.
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeMultiplayer($query)
{
return $query->where('multiplayer','>', $this->users()->count());
}
However, this doesn't seem to work as $this->users()->count() seems to always return 0.
Your solution will be highly appreciated.

How to add the condition on inter table not relation table when defining a model relation

I have a relation table named company_team it relation to company and user, and there is a field is_ceo in company_team table to flag weather a company team member is an ceo. following is my model define
class CompanyTeam extends Pivot
{
/**
* return all company team member with out ceo
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function team_member()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* return only ceo's info
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function company_ceo()
{
$this->where('is_ceo', 1); // it dosen't work, but it is effect l want
return $this->belongsTo(User::class, 'user_id');
}
}
l searched an answer with using addGlobalScope, but it doesn't fit me, because when l use both of team_member and company_ceo relation, it will add condition on both of then
In user model, you can define a scope
public function scopeCompanyCeo($query){
return $query->where('is_ceo',1);
}
then you can use like in your controller
$user = User::find(1)->companyCeo()->get();
You can't add conditions when defining the relationship.
You have to add the condition when performing the queries.
CompanyTeam::with(['company_ceo' => function ($q) use() {
$q->where('is_ceo', 1);
}])
->get();
public function company_ceo()
{
return $this->belongsTo(User::class, 'user_id')->where('is_ceo', 1);
}
I hope this helps. Have a great day and happy coding... :)

Using toHasOne macro with MorpMany relationship

I have multiple Model classes that utilize a HasRetirements trait class. Both models use a MorphMany relationship to target the associated retirements table model for each model. Inside the HasRetirements trait class, I also have a isRetired() method as well as a currentRetirement() method. These methods are shown below.
I have come across a macro that can be chained onto an Eloquent relationship so that you can retrieve a single record. The macro toHasOne() utilizes model relationships through a hasMany relationship however my question is could this also be used for a morphMany relationship since it's polymorphic.
https://scotch.io/tutorials/understanding-and-using-laravel-eloquent-macros
public function currentRetirement()
{
return $this->retirements()->whereNull('ended_at')->latest()->toHasOne();
}
public function isRetired()
{
return $this->retirements()->whereNull('ended_at')->exists();
}
With Laravel 5.5, you could register a macro returning a derived class from the BelongsToMany relation. This derived class also could be an anonymous class if you are not planning on using it anywhere else. Within the derived class, you need to override the match method and return the single object as a relation or null otherwise
BelongsToMany::macro('asSingleEntity', function() {
return new class(
$this->related->newQuery(),
$this->parent,
$this->table,
$this->foreignPivotKey,
$this->relatedPivotKey,
$this->parentKey,
$this->relatedKey,
$this->relationName) extends BelongsToMany {
/**
* Match the eagerly loaded results to their parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #return array
*/
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have an array dictionary of child objects we can easily match the
// children back to their parent using the dictionary and the keys on the
// the parent models. Then we will return the hydrated models back out.
foreach ($models as $model) {
if (isset($dictionary[$key = $model->{$this->parentKey}])) {
$model->setRelation(
// $relation, $this->related->newCollection($dictionary[$key]) // original code
$relation, array_first($dictionary[$key])
);
} else {
$model->setRelation($relation, null);
}
}
return $models;
}
};
});
Then, you could simply use it within the model.
return $this
->belongsToMany(\App\Models\Entity::class, 'pivot_table_name')
->asSingleEntity();

Laravel Eloquent Model Unit testing

I am trying to write a testcase which tests the association and detachment of the relationship between two Eloquent models in Laravel 4.2
Here's my test case:
class BookingStatusSchemaTest extends TestCase
{
private $statusText = "Confirmed";
private $bookingStub;
private $statusStub;
public function testMigrateService()
{
$this->createTestData();
$booking = $this->bookingStub;
$status = $this->statusStub;
/**
* Check that the booking has no status. OK
*/
$this->assertNull($booking->status);
/**
* Check that status has no booking. OK
*/
$this->assertEquals(count($status->bookings), 0);
/**
* Add a status to the booking. OK
*/
$booking->status()->associate($this->statusStub);
/**
* Check that status has a booking. NOT OK - This gives error
*/
$this->assertEquals(count($status->bookings), 1);
/**
* Check that the booking has a status. OK
*/
$this->assertNotNull($booking->status);
/**
* Do NOT delete the status, just set the reference
* to it to null.
*/
$booking->status = null;
/**
* And check again. OK
*/
$this->assertNull($booking->status);
}
private function createTestData()
{
$bookingStatus = BookingStatus::create([
'status' => $this->statusText
]);
$booking = Booking::create([ ]);
$this->bookingStub = $booking;
$this->statusStub = $bookingStatus;
}
}
When I execute it I get:
There was 1 failure:
1) BookingStatusSchemaTest::testMigrateService
Failed asserting that 1 matches expected 0.
Booking model:
class Booking extends Eloquent {
/**
* A booking have a status
*/
public function status()
{
return $this->belongsTo('BookingStatus');
}
}
BookingStatus Model:
class BookingStatus extends Eloquent
{
protected $table = 'booking_statuses';
protected $guarded = [ 'id' ];
protected $fillable = ['status'];
/**
* A booking status belongs to a booking
*/
public function bookings()
{
return $this->hasMany('Booking');
}
}
Here's the migration Schema for bookingstatus:
Schema::create('booking_statuses', function(Blueprint $table)
{
$table->increments('id');
$table->string('status');
$table->timestamps();
});
And heres for booking:
Schema::create('bookings', function(Blueprint $table)
{
$table->increments('id');
$table->unsignedInteger('booking_status_id')->nullable();
$table->timestamps();
});
What do I have to add / change to be able to verify the relationship in my test case?
It's been a while and I had totally forgotten about this question.
Since OP still sems interested in it, I'll try to answer the question
in some way.
So I assume the actual task is: How to test the correct relationship between two Eloquent models?
I think it was Adam Wathan who first suggested abandoning terms like "Unit Tests" and "Functional Tests" and "I-have-no-idea-what-this-means Tests" and just separate tests into two concerns/concepts: Features and Units, where Features simply describe features of the app, like "A logged in user can book a flight ticket", and Units describe the lower level Units of it and the functionality they expose, like "A booking has a status".
I like this approach a lot, and with that in mind, I'd like to refactor your test:
class BookingStatusSchemaTest extends TestCase
{
/** #test */
public function a_booking_has_a_status()
{
// Create the world: there is a booking with an associated status
$bookingStatus = BookingStatus::create(['status' => 'confirmed']);
$booking = Booking::create(['booking_status_id' => $bookingStatus->id]);
// Act: get the status of a booking
$actualStatus = $booking->status;
// Assert: Is the status I got the one I expected to get?
$this->assertEquals($actualStatus->id, $bookingStatus->id);
}
/** #test */
public function the_status_of_a_booking_can_be_revoked()
{
// Create the world: there is a booking with an associated status
$bookingStatus = BookingStatus::create(['status' => 'confirmed']);
$booking = Booking::create(['booking_status_id' => $bookingStatus->id]);
// Act: Revoke the status of a booking, e.g. set it to null
$booking->revokeStatus();
// Assert: The Status should be null now
$this->assertNull($booking->status);
}
}
This code is not tested!
Note how the function names read like a description of a Booking and its functionality. You don't really care about the implementation, you don't have to know where or how the Booking gets its BookingStatus - you just want to make sure that if there is Booking with a BookingStatus, you can get that BookingStatus. Or revoke it. Or maybe change it. Or do whatever. Your test shows how you'd like to interact with this Unit. So write the test and then try to make it pass.
The main flaw in your test is probably that you're kind of "afraid" of some magic to happen. Instead, think of your models as Plain Old PHP Objects - because that's what they are! And you wouldn't run a test like this on a POPO:
/**
* Do NOT delete the status, just set the reference
* to it to null.
*/
$booking->status = null;
/**
* And check again. OK
*/
$this->assertNull($booking->status);
It's a really broad topic and every statement about it inevitably opinioted. There are some guidelines that help you get along, like "only test your own code", but it's really hard to put all the peaces together. Luckily, the aforementioned Adam Wathan has a really excellent video course named "Test Driven Laravel" where he test-drives a whole real-world Laravel application. It may be a bit costly, but it's worth every penny and helps you understand testing way more than some random dude on StackOverflow :)
To test you're setting the correct Eloquent relationship, you have to run assertions against the relationship class ($model->relation()).
You can assert
It's the correct relationship type by asserting $model->relation() is an instance of HasMany, BelongsTo, HasManyThrough... etc
It's relating to the correct model by using $model->relation()->getRelated()
It's using the correct foreign key by using $model->relation()->getForeignKey()
The foreign key exists as a column in the table by using Schema::getColumListing($table) (Here, $table is either $model->relation()->getRelated()->getTable() if it's a HasMany relationship or $model->relation()->getParent()->getTable() if it's a BelongsTo relationship)
For example. Let's say you've got a Parent and a Child model where a Parent has many Child through the children() method using parent_id as foreign key. Parent maps the parents table and Child maps the children table.
$parent = new Parent;
# App\Parent
$parent->children()
# Illuminate\Database\Eloquent\Relations\HasMany
$parent->children()->getRelated()
# App\Child
$parent->children()->getForeignKey()
# 'parent_id'
$parent->children()->getRelated()->getTable()
# 'children'
Schema::getColumnListing($parent->children()->getRelated()->getTable())
# ['id', 'parent_id', 'col1', 'col2', ...]
EDIT
Also, this does not touch the database since we're never saving anything. However, the database needs to be migrated or the models will not be associated with any tables.

Resources