[11.x]: Factories: Ensure afterMaking is called prior to create() attributes being set #53294
+111
−12
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The Problem
Today, the
create
attributes in the Factory are ignored/overwritten by those "defaulted" inafterMaking
, because of the following sequence of events:create($attributes)
state($attributes)
(Sets the$attributes
passed tocreate
as the last-to-runstate
transformer)make($attributes)
makeInstance()
(Actually unpacks/resolves the to-be-made attributes)callAfterMaking()
save
callAfterCreating()
Instead, the attributes passed to
create
should be filled aftercallAfterMaking
has finished, just prior tosave
being called on the model.This however isn't as simple as it sounds, because it should be possible to reference things like factory methods in
create()
that would override those inmake()
. For example:This effectively means we've got a chicken-egg problem, where the model is already instantiated (but not yet created), while the create method itself still has state to set/resolve.
This Solution
Since the
state($attributes)
called in step 2 is appended to a (potential) list of other state transformers, as well as because they're ran in sequence, we know that it's the last transformer to run prior to Model instantiation / attribute expansion.By wrapping this very last
$attributes
state call with an interceptor method, we're able to tap in to the raw attributes (e.g. related Factory instances, per-attribute resolver callbacks) right before they're resolved/unpacked byexpandAttributes()
.This is useful, because while we don't necessarily have access to useful values yet at this point, it does mean that we're able to see which attributes keys the user passed in to
create($attributes)
, and track them in a property on the Factory instance.Next, during step 4 in the
makeInstance
method, the Eloquent-ready attributes are expanded (usinggetExpandedAttributes
) just prior to callingnew Model($attributes)
. This again provides us with an opportunity, because at this point we have access to the actual attributes that would otherwise be used to instantiate the model.Now, because we only track the custom attribute keys when
create()
has already been called (which again, is the last step in aFactory
chain), we can simply check if there are any keys on our "tracked" property, and if there are, we can just grab that subset of values and have what is effectively a complete "create()"-method dataset. The only downside at this point however is that we're still in the middle of themake()
call, meaning that the only thing we can do for now is to track them in the same way as we tracked the keys themselves.Finally, once the
make()
call returns in thecreate()
method, with the Model effectively instantiated and theafterMaking
calls having ran as part of the make method, we useforceFill
to set the collected/tracked subset of "create" attributes on the model, leading to the intended result.