I'm having an issue when using the model states library from Spatie. I don't think it's a bug but it's not behaving as expected, although only in one of my controllers. We are using the older version of the code and, for now, cannot update to the latest version.
The problem is the "state" field is not being cast to a Spatie\ModelStates\State derived object and is returned as a string. So when I try to transition to a new state I get the exception: "Call to a member function transitionTo() on string".
However, there are other parts of the code where the same model is used and transitions work correctly, with the state being converted to the correct class. I just can't work out why this one controller is causing problems.
States derived from my own abstract class
<?php
namespace App\States\ShiftPattern;
use Spatie\ModelStates\State;
abstract class ShiftPatternBaseState extends State
{
public static array $states = [
Approved::class,
Draft::class,
PendingApproval::class,
Rejected::class,
];
}
Even though I register the states in the base class they are also in the same folder. A migration added the status field to the database table
public function up()
{
Schema::table('shift_patterns', function (Blueprint $table) {
$table->string('status')->default('draft')->after('booking_pay_rate_id');
});
}
and my model implements HasStates
class ShiftPattern extends Model
{
use HasStates, LogsActivity, UserPermissions;
...
public function registerStates(): void
{
$this->addState('status', ShiftPatternBaseState::class)
->default(Draft::class)
->allowTransition([Draft::class, Rejected::class], PendingApproval::class, ToPendingApproval::class)
->allowTransition(PendingApproval::class, Approved::class, PendingApprovalToApproved::class)
->allowTransition(PendingApproval::class, Rejected::class, ToRejected::class);
}
...
}
The problem is occurring in a controller which I am updating. It currently handles an API call to create a new ShiftPattern and attach it to a Booking model, which works. There is a one-to-many relationship defined between Booking and ShiftPattern.
// CreateShiftPatternRequest has the Booking object ($request->record) and attributes ($request->attributes) to create the ShiftPattern
public function createShiftPattern(CreateShiftPatternRequest $request)
{
$this->authorize('editShiftPatterns', $request->record);
$shiftPattern = $request->record->shiftPatterns()->create($request->attributes());
return $this->reply()->content($shiftPattern, [], $this->getMeta('bookings.shift-pattern.create'));
}
The new ShiftPattern is being created in the "Pending Approvel" state, but some bookings do not require them to be "approved", so I want to move them straight to the "Approved" state.
public function createShiftPattern(CreateShiftPatternRequest $request)
{
...
$shiftPattern = $request->record->shiftPatterns()->create($request->attributes());
if (!$request->record->booking_must_be_approved) {
$shiftPattern->transitionTo(Approved::class);
}
return $this->reply()->content($shiftPattern, [], $this->getMeta('bookings.shift-pattern.create'));
}
But I keep getting the error "Call to a member function transitionTo() on string" which happens inside the transitionTo call once the state field has been resolved by the library. As I said, in other cases this works fine, but in this one controller the state field is not automatically cast to an object.
I thought the model might not be "booted" correctly to set up the class casts, so I exposed a function to allow the controller to call bootIfNotBooted but inside that it skips the initialisation as it has been booted.
Then I tried refreshing and reloading the ShiftPattern from the database, to see if that would solve it:
...
$shiftPattern = $request->record->shiftPatterns()->create($request->attributes());
// Attempt 1 - refresh model
$shiftPattern->refresh();
// Attempt 2 - reload model
$newShiftPattern = ShiftPattern::find($shiftPattern->id);
...
neither worked, the state field was still being returned as a string.
I also tried creating the model separately from associating it with the Booking, but that did not fix the issue either
$shiftPattern = ShiftPattern::create($request->attributes());
if (!$request->record->booking_must_be_approved) {
$shiftPattern->transitionTo(Approved::class);
}
$request->record->shiftPatterns()->save($shiftPattern);
...
Does anyone have any idea why this could be happening? I really don't think it's a bug in the ModelStates library since it works in tinker and other sections of my code, it seems to be a Laravel attribute cast issue.
FYI I have also posted this question in the package's github discussion.