multiple graphs/property_paths on same class
MartinP7r opened this issue · 10 comments
Hi, I was wondering if it would be possible to apply more than one statemachine to the class? e.g. when you have two different state variables on the same model?
It seems that right now, getGraph
can only set for one graph that in turn is coupled to one property_path
.
Actually you can create multiple graphs for a class, and set a different property path. E.g. in your state-machine.php
config:
<?php
return [
'orderStatus' => [
'class' => App\Order::class,
'graph' => 'default',
'property_path' => 'status',
'metadata' => [],
'states' => [],
'transitions' => [],
'callbacks' => [],
],
'shipmentStatus' => [
'class' => App\Order::class,
'graph' => 'shipment',
'property_path' => 'shipment_status',
'metadata' => [],
'states' => [],
'transitions' => [],
'callbacks' => [],
],
];
So now you can gate the state machine for each graph like this:
$orderState = StateMachine::get($order); // this is the default graph
$shipmentState = StateMachine::get($order, 'shipment'); // this is the shipment graph
I'm not sure how you can use this feature with iben12/laravel-statable
, as the trait is allowing only one graph per model.
Hello @MartinP7r , did you find a solution for your problem? I was wondering if you need more assistance or if I can close this issue. Thanks!
@sebdesign thanks a lot and sorry for the late response. I'm going to implement it as you suggested, possible just not using the trait...
a kind of related question, if I may:
Is there a way to apply the same graph to more than one model? E.g. 'class' => [App\Order::class, App\Shipment::class]
? Or do I just need to duplicate the config entry for another class?
At the moment you can't specify more than one model in the class
in the config.
But you can use an interface or an abstract class in the class
instead of a concrete class.
For example, if you have a graph for your App\User
model and you want to reuse the same for your App\Admin
model, you can specify class => Illuminate\Foundation\Auth\User::class
which is the base Authenticatable
model that App\User
extends, or class => Illuminate\Contracts\Auth\Authenticatable::class
which is the AuthenticatableContract
interface that the Authenticatable
model implements. Of course the App\Admin
model must extends the Authenticatable
model, or the App\User
model, or just implement the AuthenticatableContract
.
Does it make sense?
Thanks so much for the more detailed explanation and sorry for the late reaction.
Considering your remarks, is there anything speaking against just specifying App\Model
for a state-machine that's being implemented by more than one model, since all models inherit from it?
Or would you advise for implementing an "empty" interface like interface SomeStatable {}
and then just specify class => SomeStatable::class
(if it's even possible to use an interface in that way).
Sorry for the non-related inquiry. I suppose I should post half of this to stackoverflow instead...
No worries, you can post this kind of questions here.
It is possible to specify an interface instead of a concrete class in a graph.
It can be an empty interface so you can apply it to multiple classes without worrying about the implementation.
You could also specify Illuminate\Database\Eloquent\Model
or App\Model
, but I would consider this a code smell: Does it mean that every model in your application has the same states and transitions? For example a user, a post, a comment?
I'm curious about what problem you are trying to solve by using a single graph for many classes. Do you have an example? Maybe I can help you find a solution or an idea for refactoring.
Thanks again.
From what you are saying, I'm tending towards a shared empty interface. Your explanation about the code-smelly aspect makes a lot of sense. It's only a couple of models in my project.
Essentially I have several models that are types of requests (BusinessTripRequest
, ExpenseReport
, etc.) going through and Approval
process with variable number of approvers involved. The relevant state goes from Approval
through one Final Approval
to Approved
and can move to Denied
at any step. What the models have in common is a one-to-many relationship with an Approval
model.
Small sidenote: I'm kind of reluctant to use inheritance (e.g. a Request
model from which the concerning models inherit) since the system requirements have been changing a lot to say the least, there's more than just the aspect of the approval process that's common between some of the models in the project and I would prefer composition anyway. But I am struggling a little with the relevant concepts in PHP&Laravel.
Coming from Swift, where class properties can be part of interfaces( aka Protocols), I've been trying to use PHP traits
only to realize they can't be used to constrain parameter types. So everything has to be enforced through getter/setter (accessor?) methods?
But Laravel provides these convenient ->attribute_name
shortcuts for everything, so it seems like a step back.
edit: thinking about this a little more, maybe I should just decouple the whole Approval-Process into its own model and quasi composition it through a relationship into the other models. Though this creates one more layer of relationships that might be hard to manage...
But on the other hand, guard-related logic might change based on the parent model (BusinessTripReuqest
, ExpenseReport
, etc...), and would be hard to manage via a Policy if callbacks run on some kind of ApprovalProcess
model, separate from them.
I have implemented a similar functionality in the past, regarding multiple approvals and approvers, although not using state machines, but I can give you some insight.
While you cannot define properties on interfaces, you can define them in traits but you can't use them for attributes on Eloquent models, because the model attributes are stored in the attributes
array property. So you could define some custom getters/setters. Eloquent uses the getXAttribute
and setXattribute
convention, that will get called when you use the property access on the model: $object->x
.
In addition, the winzou/state-machine
uses the symfony/property-access
component which has another convention for getters/setters: By default it uses PHP's property access $object->x
, but you can define getX
and setX
methods on the object instead. See the Using Getters and Writing to Objects documentation.
For start, I would create an Approvable
interface, which could be empty, or you could define the method for the Approval
has-many relationship.
In this example I'm using Symfony's getter/setter notation, but you can use Laravel's, or none if you don't want to enforce this.
interface Approvable
{
public function approvals(): HasMany;
public function getApprovalStatus(): string;
public function setApprovalStatus(string $status): void;
}
For the implementation, I would create an ApprovableTrait
trait:
The getter and the setter are delegating to Eloquen't magic getter/setters so they are actually reading/writing the approval_status
attribute on the model's attributes
property.
trait ApprovableTrait
{
public function approvals(): HasMany
{
return $this->hasMany(Approval::class);
}
public function getApprovalStatus(): string
{
return $this->approval_status;
}
public function setApprovalStatus(string $status): void
{
$this->approval_status = $status;
}
}
Your models then would implement the Approvable
interface by using the ApprovableTrait
trait:
class BusinessTripRequest extends Model Implements Approvable
{
use ApprovableTrait;
}
This seems very clean and easy to implement for me. Thanks so much for the detailed example! 😀
Just for confirmation. If I understand this correctly, any logic that I create (state-machine or otherwise), that wants to use the Approvable interface instead of a concrete class, would need to call getApprovalStatus
etc. in order to access properties and would not be able to use $object->x
unless I otherwise implement it like symfony/property-access
does?
Since the Approvable interface is implemented by the eloquent Model
you can directly access properties $object->x
like any other eloquent model. The getters/setters are just meant to use custom logic for getting/setting the attribute, but that's only if you need to. You don't have to specify any getter/setter in the interface or the trait if you $object->x
is working for your case.