Fundición: Accesorios que te encantarán
Construir accesorios es bastante sencillo, pero algo aburrido. Y sería súper aburrido crear manualmente 25 mezclas dentro del método load(). Por eso vamos a instalar una impresionante biblioteca llamada "Foundry". Para ello, ejecuta:
composer require zenstruck/foundry --dev
Ejecuta: --dev porque sólo necesitamos esta herramienta cuando estamos desarrollando o ejecutando pruebas. Cuando termine, ejecuta
git status
para ver que la receta ha habilitado un bundle y también ha creado un archivo de configuración... que no necesitaremos mirar.
Fábricas: make:factory
En resumen, Foundry nos ayuda a crear objetos de entidad. Es... casi más fácil verlo en acción. En primer lugar, para cada entidad de tu proyecto (ahora mismo, sólo tenemos una), necesitarás una clase fábrica correspondiente. Créala ejecutando
php bin/console make:factory
que es un comando Maker que viene de Foundry. Luego, puedes seleccionar para qué entidad quieres crear una fábrica... o generar una fábrica para todas tus entidades. Nosotros generaremos una para VinylMix. Y... eso creó un único archivo: VinylMixFactory.php. Vamos a comprobarlo:src/Factory/VinylMixFactory.php.
| // ... lines 1 - 10 | |
| /** | |
| * @extends ModelFactory<VinylMix> | |
| * | |
| * @method static VinylMix|Proxy createOne(array $attributes = []) | |
| * @method static VinylMix[]|Proxy[] createMany(int $number, array|callable $attributes = []) | |
| // ... lines 16 - 27 | |
| */ | |
| final class VinylMixFactory extends ModelFactory | |
| { | |
| // ... lines 31 - 37 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories) | |
| 'title' => self::faker()->text(), | |
| 'trackCount' => self::faker()->randomNumber(), | |
| 'genre' => self::faker()->text(), | |
| 'votes' => self::faker()->randomNumber(), | |
| 'slug' => self::faker()->text(), | |
| 'createdAt' => null, // TODO add DATETIME ORM type manually | |
| 'updatedAt' => null, // TODO add DATETIME ORM type manually | |
| ]; | |
| } | |
| // ... lines 51 - 63 | |
| } |
¡Qué bien! Encima de la clase, puedes ver que se describen un montón de métodos... que ayudarán a nuestro editor a saber qué superpoderes tiene esto. Esta fábrica es realmente buena para crear y guardar objetos VinylMix... o para crear muchos de ellos, o para encontrar uno al azar, o un conjunto al azar, o un rango al azar. ¡Uf!
getDefaults()
El único código importante que vemos dentro de esta clase es getDefaults(), que devuelve los datos por defecto que deben utilizarse para cada propiedad cuando se crea un VinylMix. Hablaremos más de eso en un minuto.
Pero antes... ¡vamos a avanzar a ciegas y a utilizar esta clase! En AppFixtures, borra todo y sustitúyelo por VinylMixFactory::createOne().
| // ... lines 1 - 5 | |
| use App\Factory\VinylMixFactory; | |
| // ... lines 7 - 9 | |
| class AppFixtures extends Fixture | |
| { | |
| public function load(ObjectManager $manager): void | |
| { | |
| VinylMixFactory::createOne(); | |
| $manager->flush(); | |
| } | |
| } |
¡Ya está! Gira y vuelve a cargar los accesorios con:
symfony console doctrine:fixtures:load
Y... ¡falla! Boo
Tipo de argumento esperado "DateTime", "null" dado en la ruta de la propiedad "createdAt"
Nos está diciendo que algo intentó llamar a setCreatedAt() en VinylMix... pero en lugar de pasar un objeto DateTime, pasó null. Hmm. Dentro deVinylMix, si te desplazas hacia arriba y abres TimestampableEntity, ¡sí! Tenemos un métodosetCreatedAt() que espera un objeto DateTime. Algo llamado así... pero que pasa null.
Esto ayuda a mostrar cómo funciona Foundry. Cuando llamamos aVinylMixFactory::createOne(), crea un nuevo VinylMix y luego le pone todos estos datos. Pero recuerda que todas estas propiedades son privadas. Así que no establece la propiedad del título directamente. En su lugar, llama a setTitle() y setTrackCount()Aquí abajo, para createdAt y updatedAt, llamó a setCreatedAt()y le pasó a null.
En realidad, no necesitamos establecer estas dos propiedades porque las establecerá automáticamente el comportamiento timestampable.
Si probamos esto ahora...
symfony console doctrine:fixtures:load
¡Funciona! Y si vamos a ver nuestro sitio... impresionante. Esta mezcla tiene 928.000 pistas, un título aleatorio y 301 votos. Todo esto proviene del método getDefaults().
Datos falsos con Faker
Para generar datos interesantes, Foundry aprovecha otra biblioteca llamada "Faker", cuyo único trabajo es... crear datos falsos. Así que si quieres un texto falso, puedes decir self::faker()->, seguido de lo que quieras generar. Hay muchos métodos diferentes que puedes invocar en faker() para obtener todo tipo de datos falsos divertidos. ¡Es muy útil!
Creando muchos objetos
Nuestra fábrica ha hecho un trabajo bastante bueno... pero vamos a personalizar las cosas para hacerlas un poco más realistas. En realidad, en primer lugar, tener un VinylMix sigue sin ser muy útil. Así que, dentro de AppFixtures, cambia esto por createMany(25).
| // ... lines 1 - 11 | |
| public function load(ObjectManager $manager): void | |
| { | |
| VinylMixFactory::createMany(25); | |
| // ... lines 15 - 16 | |
| } | |
| // ... lines 18 - 19 |
Aquí es donde Foundry brilla. Si ahora recargamos nuestras instalaciones:
symfony console doctrine:fixtures:load
Con una sola línea de código, ¡tenemos 25 accesorios aleatorios con los que trabajar! Sin embargo, los datos aleatorios podrían ser un poco mejores... así que vamos a mejorar eso.
Personalizar getDefaults()
Dentro de VinylMixFactory, cambia el título. En lugar de text() -que a veces puede ser un muro de texto-, cambia a words()... y utiliza 5 palabras, y pasa true para que lo devuelva como una cadena. De lo contrario, el método words() devuelve una matriz. Para trackCount, sí queremos un número aleatorio, pero... probablemente un número entre 5 y 20. Para genre, vamos a buscar un randomElement() para elegir aleatoriamente pop o rock. Esos son los dos géneros con los que hemos trabajado hasta ahora. Y, vaya... asegúrate de llamar a esto como una función, ya está. Por último, para votes, elige un número aleatorio entre -50 y 50.
| // ... lines 1 - 28 | |
| final class VinylMixFactory extends ModelFactory | |
| { | |
| // ... lines 31 - 37 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| 'title' => self::faker()->words(5, true), | |
| 'trackCount' => self::faker()->numberBetween(5, 20), | |
| 'genre' => self::faker()->randomElement(['pop', 'rock']), | |
| 'votes' => self::faker()->numberBetween(-50, 50), | |
| 'slug' => self::faker()->text(), | |
| ]; | |
| } | |
| // ... lines 48 - 60 | |
| } |
¡Mucho mejor! Ah, y puedes ver que make:factory ha añadido aquí un montón de nuestras propiedades por defecto, pero no las ha añadido todas. Una de las que falta esdescription. Añádela: 'description' => self::faker()-> y luego usa paragraph(). Por último, para slug, no la necesitamos en absoluto porque se establecerá automáticamente.
| // ... lines 1 - 37 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // ... line 41 | |
| 'description' => self::faker()->paragraph(), | |
| // ... lines 43 - 45 | |
| ]; | |
| } | |
| // ... lines 48 - 62 |
¡Ufff! ¡Vamos a probarlo! Recarga los accesorios:
symfony console doctrine:fixtures:load
Luego dirígete y actualiza. Esto se ve mucho mejor. Tenemos una imagen rota... pero eso es sólo porque la API que estoy utilizando tiene algunas "lagunas"... nada de lo que preocuparse.
Foundry puede hacer un montón de cosas interesantes, así que no dudes en consultar su documentación. Es especialmente útil cuando se escriben pruebas, y funciona muy bien con las relaciones de la base de datos. Así que lo volveremos a ver de forma más compleja en el próximo tutorial.
A continuación, ¡vamos a añadir la paginación! Porque, al final, no podremos listar todas las mezclas de nuestra base de datos de una sola vez.
29 Comments
I'm getting some strange deprecation warnings from (it appears: ) foundry:
09:55:55 INFO [deprecation] User Deprecated: Method Doctrine\ORM\Event\LifecycleEventArgs::getEntityManager() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead. (LifecycleEventArgs.php:63 called by ORM.php:98, https://github.com/doctrine/orm/issues/9875, package doctrine/orm)
is this coming from foundry? I'm on symfony 6:
"zenstruck/foundry": "^1.35"
Is this due to Zenstruck being out of date with Doctrine?
Hey Cameron,
This is actually from doctrine/orm package. This might be an indirect deprecation for other packages that uses doctrine/orm in their dependecies including Foundry like you said. You can just ignore it for now and it should be fixed by package maintainers in future releases. Or, you can give it a try and help to find and fix it yourself if you want :)
Cheers!
so foundry generates a "Factory", so what do I call my acctual (non fixtures) factories then? Or is a foundry factory supposed to be used (i.e. replace my existing entity factories?) for generating non-fixtures also? It's a bit confusing.
Hey Cameron!
Yea, they sorta "take over" the factory namespace. But you're right: these factory classes are really meant to seed the database. You may, in your app, have other classes that have nothing to do with Foundry but which follow the factory pattern. Unless it's confusing for you, there is nothing wrong with having non-Foundry classes that also live in
src/Factory/. If you don't like that then, yea, you might need to think of some other name for your factories (I'm guessing you can also move the Foundry factory classes somewhere else if you want to).Cheers!
Thanks, i renamed foundry namespace from 'factory' to 'foundry' to keep it seperate from factories that do a better job building non-fixture entities.
Awesome! And thanks for sharing that this worked. I couldn't think of any reason why simply "renaming the namespace" would cause any issues, but I had never tried it before with Foundry!
I tried using foundry, but it's not clear how to override the createOne method (if you need to do some custom work). Is there somewhere that has more comprehensive information on how to use this? It would be great if you guys had some training on this - as I'm working on a project and I just can't use foundry or the factory because I don't understand it.
Hey @Cameron!
The docs are pretty complete at https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html. But, I might be able to help here too :). Why do you want to override
createOne()? Is there something custom that you want to do? Usually, the answer to these customizations is to use one of the hooks - e.g.->afterInstantiate()orafterPersist()- https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#events-hooksLet me know what you want to do, and my guess is we can find a solution.
Cheers!
I reviewed that documentation yes, thank you.
It was not clear to me that foundry was only for fixtures, as it's use of the 'factory' namespace (to me at least) implies it's purpose as a non-fixture factory also. I was looking to override createOne with this understanding in mind.
Hey @Cameron!
Yea, it's not very obvious the scope of the factories, actually, they are not limited to only fixtures, you can use them on your tests, but they are not meant to be used in production. You can see in the docs how they install it as a dev dependency.
Cheers!
Hey team,
I just followed the tutorial and got an Error. To blame me: I am using symfony 6.2.8.
But did I miss something? The new Method from MixController works fine and slug is created automatically, but deleting 'slug' => self::faker()->text() and executing "symfony console doctrine:fixtures:load" leads to
In ExceptionConverter.php line 47:
An exception occurred while executing a query: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "slug" of relation "vinyl_mix" violates not-null constraint
DETAIL: Failing row contains (9, id veniam ducimus exercitationem est, Totam dolor et tenetur fuga voluptas nisi rerum. Est animi corpo..., 6, rock, 2023-04-17 17:21:34, 20, 2023-04-17 17:21:34, null).
Any comment is appreciated.
Thank you.
Hey @Georg-L
That's unexpected. Did you add the Slug attribute to your property?
#[Slug(fields: ['title'])]like shown in this code block https://symfonycasts.com/screencast/symfony-doctrine/sluggable#codeblock-1fd8df0329
Cheers!
Hey @MolloKhan,
thank you for your message. Me stupid! I managed to somehow delete this line and didn't notice.
sorry for bothering you.
Have a nice day
No prob! it happens :)
Hey, great library. I need a bit of help with localization. If I want another language, how to do this? In the documentation I see
Faker\Factory::create('fr_FR');But how to set this in Symfony? Thank you!
Hey Rufnex,
Good question! If you're talking about how to localize Faker with the Foundry bundle - here's an example: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#faker . As you can see from the docs, you can do it via Symfony configuration:
I hope this helps!
Cheers!
Hi Victor,
i had already tried this. unfortunately, only the standard language is used. Any further ideas?
Greetings
Rufnex
Hey Rufnex,
Hm, could you clear the cache and try again? It might be a cache issue. If not, please, make sure your current config really has that locally, you can debug it with:
Is the locale set there? Also, make sure you're debugging this config for the environment that your Symfony application is currently using. It might be so that you set the locale for dev but load your app in prod.
Cheers!
HeyHo Victor,
the configuration looks correct, but still only the default language like "et accusamus sunt ipsum non". strange behavioer .. or is it required to download somewhere the language packs?
Hey Rufnex,
Ah, ok, it seems correct to me. Well, you don't need to install any extra localization pack, it's implemented via so called Provider in the faker bundle: https://github.com/FakerPHP/Faker/tree/main/src/Faker/Provider - you just need to specify a correct provider for the "locale" option I suppose. OK, could you try that
Faker\Factory::create('fr_FR')instead as described in the docs and try to generate data with it? Are those data really localized? If so, it sounds like a misconfiguration or a bug in the Foundry bundle, I'd recommend you to open an issue there.Btw, keep in mind that not all the data might be translated. Please, try on the specific methods like
name(),region(),vat(), orphoneNumber()as it's listed as translated in the docs: https://fakerphp.github.io/locales/fr_FR/ . It might be so that the method you're calling always returns Latin text.I hope this helps!
Cheers!
Sorry, can you explain me, where to add
Faker\Factory::create('fr_FR')? i'm a bit confused ;)I think Victor talk about something like this:
Thanks you! Unfortunately, only latain is produced here as well.
Hey Rufnex,
Hm, that's weird if even with the
Faker\Factory::create('fr_FR')you still get latin data. What method are you. calling on that custom localized faker? Could you show a part of your code where you're using it? Because as far as I understand your code it should just work this way, probably you're missing something obvious.Cheers!
Hey Rufnex,
Yep, exactly like Ruslan mentioned below in https://symfonycasts.com/screencast/symfony-doctrine/foundry#comment-27727
I.e. you create a faker instance manually via
$faker = Factory::create('fr_FR');passing the desired locale and then use it below.Cheers!
I followed the example .. so in my factory i have this code:
Hey Rufnex,
Oh, that's exactly what is in the docs actually, I'm not sure why it does not work. Well, the only explanation might be is that those methods you're using like
words()/text()are not localized to French. I'd recommend you to change that code to something like this:And re-load your fixtures. In the console, you should get a dumped French region and a French number that starts with +33 - it you see it - then localization actually work, but it would mean as I said before that those method you're using like
text()are not localized to French.But if you don't see a french region and french phone number dumped - than it might be a bug in the Faker library you have installed. I'd recommend you to upgrade to the latest version and double-check if it works there. if not - probably report a bug in the Faker repository. Though, it would be a very obvious bug I suppose so other devs had to notice it already. I'm leaning towards the fact that the methods you're using are just not localizaed.
Cheers!
Hi Victor,
I think I have found the problem. The formatters
text()andword()can't seem to be localized. To create a local text you can userealText()It seems that the translation only works for the following: https://fakerphp.github.io/formatters/
If someone has the same problem, the insight might be helpful. And yes RTFM ;o)
Anyway, with the translated results, I think I'd rather stay with Latin LOL.
Thanks for your help, your service is great!
Hey Rufnex,
That's exactly what I was trying to explain you a few times :) Yeah, not everything is localized, unfortunately... or fortunately, because it might be done on purpose, that's just a strategy of that lib.
Good catch on that
realText()method, and thanks for sharing it with others!Lol! But yeah, that's the reason why the "lorem ipsum" is so popular, it has no "semantic load", just text :)
Anyway, I'm really happy we figured this mystery out! ;)
Cheers!
"Houston: no signs of life"
Start the conversation!