-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[10.x] Real-Time Model Factories #47849
Conversation
Co-authored-by: James Brooks <[email protected]>
Co-authored-by: James Brooks <[email protected]>
*/ | ||
protected static function newFactory() | ||
{ | ||
return (new RealTimeFactory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you build RealTimeFactory from the container? I can already see wanting to add options to the guessable values. Another option is to create a static method to allow us to set those in user-land, but I think that's much less robust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point, consider using an actual factory, or passing the individual column values in the create
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is easy enough to just create a new application-level trait for HasRealTimeFactory and also extend the RealTimeFactory class to add to the list.
I would at least suggest providing the ability to add to this list in userland, lest GitHub be inundated with weekly PRs to add "family_name", "telephone_number", etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll leave this for Taylor to look at.
🔥 Love this |
What a thinking brother...😘 |
This is super cool. I'm going to pitch something as your fellow bourbon drinker… I feel the DX for this would be even awesomer if you didn't need to specify the trait. Having to jump into the model and add a trait doesn't feel "real-time" to me. At least not in the same sense as the "real-time" facades (only adding a namespace prefix). With the trait, this feels more like a "guessable factory". I don't know the implications of this, but I wonder if the default for all models is a |
Totally agree with this. Some great work in this PR but it just needs that extra Laravel magic 👍🏻 |
I support this, but can we have it in |
Yes yes yes! I really like this, defo agree with the above that it would be nice if it automatically did it, aside from that well done folks |
a404db8
to
0808926
Compare
@joedixon and I have addressed the "real-time" aspect of this feature. You can now simply use Of course, if you need any customization or additional states, you should generate a factory (e.g. @taylorotwell if you'd prefer to keep the feature opt-in, you can revert the last commit. |
@taylorotwell, if this were to be included in the framework, there is the potential to remove the We would have to add |
@joedixon, nice work. Not sure how deep down the rabbit hole you want to go, but I've had a "guesser" built into Blueprint for a while. Might want to pull a few of the name/type mappings. |
Isn't there any better solution other than using |
At the moment, this is the only way to reliably fetch this information. |
Couple of suggestions for this feature: Add a check if the method exists in faker, if so just call it. This removes having to map an There is the side effect that is now uses Guessing this way also allows the factory to use external providers added to ones project, e.g. /**
* Guess the value of a column based on its name.
*/
protected function guessValue(string $column): mixed
{
try {
return fake()->{str($column)->camel()}; // convert to camel case for columns like phone_number
} catch (InvalidArgumentException $e) {
// faker method doesn't exist
}
$guessable = $this->guessableValues();
return isset($guessable[$column]) ? $guessable[$column]() : null;
} Use a match statement to return the faker method result. This will reduce duplication of callback functions and remove the need to check for a key in the guessable array. Resulting factory would look something like this: /**
* Guess the value of a column based on its name.
*/
protected function guessValue(string $column): mixed
{
try {
return fake()->{str($column)->camel()}; // convert to camel case for columns like phone_number
} catch (InvalidArgumentException $e) {
}
return $this->matchColumnToFakerMethod($column);
}
/**
* Match column names with faker method
*/
protected function matchColumnToFakerMethod(string $column): mixed
{
return match ($column) {
'email', 'e_mail', 'email_address' => fake()->safeEmail(),
'login', 'username' => fake()->userName(),
// ...rest of columns
default => null,
};
} |
$columns = $this->schema->listTableColumns($this->table); | ||
|
||
return collect($columns)->keyBy(fn ($column) => $column->getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems possible to do without the $columns
variable like in isForeignKey()
or isPrimaryKey()
in this class below:
return collect($this->schema->listTableColumns($this->table))
->keyBy(fn ($column) => $column->getName());
You may use new native Schema methods on this instead of
Hopefully going to remove |
@jbrooksuk @joedixon I can send a PR on top of this one to use schema methods instead of doctrine dbal if you want, but it would be much easier if you target master instead of 10.x here. |
@joedixon I think it will be helpful if you can kindly give a hint on why you closed this. I mean it seems like a very useful feature and already has very good contributions from @jasonmccreary and @jbrooksuk, among others. Thank you 🙏 |
@damms005 cool idea for sure, but not something for the framework at this time. We are exploring ideas to package this up. |
Nice! Thanks for all you do 🙏 |
Often times when I’m cranking on tests I get frustrated by the need to stop what I’m doing to make a factory for my model. This is especially true when I don’t really care what the model is populated with and I just want to test a relation or prove the correct models are being returned from an endpoint.
I was talking with @jbrooksuk about this en route to Laracon and so we started to explore the idea on the flight where we managed to prove the concept of what we're calling “Real-Time Model Factories” which we believe improves the DX of model factories.
With the addition of this PR, when calling the
factory
method of a model, Laravel will first check to see whether a model exists and, if not, the table definition will be generated automatically using a combination of the following techniques.First, it will attempt to guess the required data using the column name. For example, given a column called
email
oremail_address
, the real-time factory will automatically populate an email address.If the column can’t be guessed, it will attempt to see whether the model is using a cast and, if so, will attempt to use it. This works for all supported built-in data types. For example, if the column is using the
collection
type, the column in the factory definition will return a collection. It doesn't support all custom casts as it’s not possible to know the required data format, but it does support enums and enum collections where the value is selected at random from the defined options.Finally, if the value is still not populated, the column type provided by DBAL is used to infer a value. At this point, if the column is a primary or foreign key or if the column is nullable, the column in the factory definition will resolve
null
. In all other instances, a sensible value derived from the column type is used.Under the hood, the values are generated using faker.
Given the following table and model definitions:
The properties would be assigned similar to the output below when using the real-time factory:
Of course, it’s possible to override fields as with any other model.
It’s also possible to utilize factory relationships:
In the above example, none of the models used a physical factory - they were all generate in real-time, so you can see it’s very easy to build up models without needing to define values until it’s truly necessary.
Of course, when that time comes, simply define a factory for the model and you are back in control of how the model should be generated.
A note on enum columns
It’s not possible to obtain the allowed values of an enum column with the
doctrine/dbal
package. When using an enum cast or when the column is nullable, this is not an issue. However, in all other cases, a random string is used which will error as an invalid value when the query is executed.