Laravel Eloquent Model Unit testing - laravel

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.

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.

Laravel Mutator to add predefined values into database

I'm new into Laravel and I'm trying to store the user's company id on a column of the products table each time a user creates a new product. The company's id it's retrieved from the user's session. I'm trying it with Laravel's Mutator:
public function setFirstNameAttribute($value) {
$this->attributes['company_id'] = session()->get('company.id');
}
But each time I create a new Product the company id stored it's null. Seems like the function it's never executing. Is there any other resource to perform actions like this?
You must use model events - this will be executed on model creation before saving. Or you can use another events depends on you logic - see docs.
class YourModel extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::creating(function (YourModel $model) {
$model->company_id = session()->get('company.id');
});
}
}
Mutators only works when you change mutating field directly:
$model->first_name = 'new_name'
And with your code - you will lost "new_name".
I noticed that the function name is incorrect, since the accessors use "studly" cased name of the column you wish to access, it may be as simple as to change
public function setFirstNameAttribute($value)
to
public function setCompanyIdAttribute($value)

Laravel - Eloquent Relationship hasMany but also hasOne?

I've been all over the web and struggling with this for around 2 hours.
I have a USER model, a RUN model and a TIME model.
In the real world the user is in a race and gets their time entered in the database along with a USER_id and a RUN_id.
A user should only be able to have one row in the TIMES table for each RUN_id - if that makes sense!
Is this something I need to manage at the controller level? Or is there a relationship I can setup to ensure that a duplicate entry of this style can not enter the database?
Database structure at present:
USERS:
name
RUNS:
name
TIMES:
time
user_id
run_id
The Models:
USER:
public function times()
{
return $this->hasMany(Time::class);
}
RUN:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Run extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function times()
{
return $this->hasMany(Time::class);
}
}
TIME:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Time extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function run()
{
return $this->belongsTo(Run::class);
}
}
You can add a unique key constraint on the times table to enforce unique combinations of user_id and run_id
$table->unique(['user_id, 'run_id']);
To validate uniqueness at the application level, we can also add a constraint to the form validation. Assuming that you are passing both user_id and run_id in the request to create a new time, you can add the following to a form request
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'user_id' => Rule::unique('times')->where('run_id', $this->input('run_id'))
];
}
public function messages()
{
return [
'user_id.unique' => 'A user may only have 1 time entry per Run'
];
}
This will enforce the user_id is unique in the times table, filtered by that run id. The messages function also returns a more useful error message, since "user_id must be unique" is unhelpful in this situation.
This answer should supplement the accepted answer. You should still define the pair of user_id,run_id as a unique key.
However, in your case user and run have an N-N relationship with times as a pivot table. You should code it as such.
User
public function runs()
{
return $this->belongsToMany(Run::class, 'times')->withPivot('time');;
}
Run:
public function users()
{
return $this->belongsToMany(User::class, 'times')->withPivot('time');
}
Then you can retrieve them as:
$runs = User::find($userId)->runs; // Collection of all runs a user participated in
// $runs[X]->pivot->time has the time
You can check the documentation for more info

Using whereHasMorph inside a local scope for MophByMany relationships

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');
});
}

How to properly implement this polymorphic relationship in Laravel?

I am trying to build an inventory for users in Laravel 5.8, however the items have their own properties, therefore I needed to set up a polymorphic relationship. When attaching items to users, it tries to add the model User to the table on itemable_type and the user's ID to itemable_id aswell as add the User's ID to user_id, something I could workaround by passing the models I need, but when I try to retrieve them it tries to find item with itemable_type = 'App\Models\User', which makes me think something's completely wrong here. Can I have some orientation on how to solve it?
class User extends Model
{
public function inventory()
{
return $this->morhpToMany(InventoryItem::class, 'itemable', 'user_inventories', null, 'itemable_id')
->withPivot('amount', 'notes');
}
}
class InventoryItem extends Model
{
public $timestamps = false;
protected $table = 'character_inventories';
protected $fillable = [
'character_id', 'itemable_type', 'amount', 'parent_id', 'notes'
];
public function cloth()
{
return $this->mophedByMany(Cloth::class, 'itemable');
}
public function food()
{
return $this->morphedByMany(Food::class, 'itemable');
}
// Other similar relations
}
// The Inventory migration:
Schema::create('user_inventories', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->unsignedInteger('itemable_id');
$table->string('itemable_type');
$table->unsignedInteger('amount')->default(0);
$table->text('notes', 65535)->nullable();
$table->foreign('character_id')->references('id')->on('characters');
});
The expected result is the User model to have different items in his inventory, but the relation is trying to query by joinning to itself and filtering by user type instead of actual items.
The error:
Syntax error or access violation: 1066 Not unique table/alias: 'user_inventories' (SQL:
select `user_inventories`.*,
`user_inventories`.`itemable_id` as `pivot_itemable_id`,
`user_inventories`.`itemable_type` as `pivot_itemable_type`,
`user_inventories`.`amount` as `pivot_amount`,
`user_inventories`.`parent_id` as `pivot_parent_id`,
`user_inventories`.`notes` as `pivot_notes`
from `user_inventories`
inner join `user_inventories` on `user_inventories`.`id` = `user_inventories`.`itemable_id`
where `user_inventories`.`itemable_id` in (4)
and `user_inventories`.`itemable_type` = App\Models\User)
I highly suspect that you have to references the user table in the inventory relation. In general it is a million times easier just following the Laravel convention for naming.
public function inventory()
{
return $this->morhpToMany(InventoryItem::class, 'itemable', 'users', null, 'itemable_id')
->withPivot('amount', 'notes');
}

Resources