Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Custom Alice Faker Function

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

The Faker name() function gives us a poor genus name. Yea, I know - this is just fake data - but it's so wrong that it fails at its one job: to give us some somewhat realistic data to make development easy.

Here's our goal: use a new <genus()> function in Alice and have this return the name of a random ocean-bound genus:

AppBundle\Entity\Genus:
genus_{1..10}:
name: <genus()>
... lines 4 - 7

This shouldn't work yet - but try it to see the error:

./bin/console doctrine:fixtures:load

Unknown formatter "genus"

Faker calls these functions "formatters". Can we create our own formatter? Absolutely.

Adding a Custom Formatter (Function)

In LoadFixtures, break the load() call onto multiple lines to keep things short and civilized. Now, add a third argument - it's sort of an "options" array. Give it a key called providers - these will be additional objects that provide formatter functions - and set it to an array with $this:

... lines 1 - 9
class LoadFixtures implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$objects = Fixtures::load(
__DIR__.'/fixtures.yml',
$manager,
[
'providers' => [$this]
]
);
}
... lines 22 - 45
}

And we're nearly done! To add a new genus formatter, add public function genus(). I've already prepared a lovely list of some fantastic genuses that live in the ocean:

... lines 1 - 9
class LoadFixtures implements FixtureInterface
{
... lines 12 - 22
public function genus()
{
$genera = [
'Octopus',
'Balaena',
'Orcinus',
'Hippocampus',
'Asterias',
'Amphiprion',
'Carcharodon',
'Aurelia',
'Cucumaria',
'Balistoides',
'Paralithodes',
'Chelonia',
'Trichechus',
'Eumetopias'
];
... lines 41 - 44
}
}

Finish this with $key = array_rand($genera) and then return $genera[$key]:

... lines 1 - 9
class LoadFixtures implements FixtureInterface
{
... lines 12 - 22
public function genus()
{
... lines 25 - 41
$key = array_rand($genera);
return $genera[$key];
}
}

Let's give it a try:

./bin/console doctrine:fixtures:load

No errors! Refresh! Ah, so much better.

New Random Boolean Column

Now, hold on, we have a new requirement: we need the ability to have published and unpublished genuses - for those times when we create a new genus, but we're still trying to think of a fun fact before it shows up on the site. With our beautiful migration and fixtures systems, this will be a breeze.

First, open Genus and add a new private property - call it $isPublished. Next, use the "Code"->"Generate" shortcut - or Ctrl+Enter - to generate the annotations:

... lines 1 - 10
class Genus
{
... lines 13 - 39
/**
* @ORM\Column(type="boolean")
*/
private $isPublished = true;
... lines 44 - 89
public function setIsPublished($isPublished)
{
$this->isPublished = $isPublished;
}
}

Hey that was cool! Because the property started with is, PhpStorm correctly guessed that this is a boolean column. Go team!

At the bottom, generate just the setter function. We can add a getter function later... if we need one.

We need to update the fixtures. But first, find the command line and generate the migration:

./bin/console doctrine:migrations:diff

Be a responsible dev and make sure the migration looks right:

... lines 1 - 10
class Version20160207083347 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE genus ADD is_published TINYINT(1) NOT NULL');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE genus DROP is_published');
}
}

Actually, it looks perfect. Run it:

./bin/console doctrine:migrations:migrate

Last step: we want to have a few unpublished genuses in the random data set. Open the Faker documentation and search for "boolean". Perfect! There's a built-in boolean() function and we can control the $chanceOfGettingTrue. In the fixtures file, add isPublished and set that to boolean(75) - so that most genuses are published:

AppBundle\Entity\Genus:
genus_{1..10}:
... lines 3 - 6
isPublished: <boolean(75)>

Re-run the fixtures!

./bin/console doctrine:fixtures:load

Hey, no errors! Now, to only show the published genuses on the list page, we need a custom query.

Leave a comment!

24
Login or Register to join the conversation
Default user avatar
Default user avatar Kosta Andonovski | posted 5 years ago

hey mate. Thanks alot for this video series, its really great! during the very last step after i add the private $isPublished colums, i add the setter public function setIsPublished($isPublished). wehn i run the php bin/console doctrine:fixtures:load command i get the following error.

PS D:\xampp\htdocs\aqua_note> php bin/console doctrine:fixtures:load

Careful, database will be purged. Do you want to continue y/N ?y

> purging database

> loading AppBundle\DataFixtures\ORM\LoadFixtures

[Doctrine\DBAL\Exception\NotNullConstraintViolationException]

An exception occurred while executing 'INSERT INTO genus (name, sub_family, species_count, fun_fact, is_published)

VALUES (?, ?, ?, ?, ?)' with params [null, "Culpa consequatur.", 93263, "Accusamus nihil repellat vero omnis volupt

ates id amet et.", 1]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null

[Doctrine\DBAL\Driver\PDOException]

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null

[PDOException]

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name' cannot be null

doctrine:fixtures:load [--fixtures [FIXTURES]] [--append] [--em EM] [--shard SHARD] [--purge-with-truncate] [--multiple-

transactions] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [

-e|--env ENV] [--no-debug] [--] <command>

why does is it trying to make name null at all, we only added that nullable=true stipulation to the $funFact column

also it did not do this until i added the isPublished column, before that it worked fine

1 Reply

Hi back to you mate! :) And thanks for the nice words - I love it!

Ok, the error itself is easy to read, but the cause is not so easy - you are correctly "confused" about why this would suddenly happen :). As you can see by looking at the query, a null value *is* being passed to the name field in the query (this is the "with params [null...]" part).

I can also see the is_published is receiving a value of 1 - which is totally normal. Ok, so why is this happening? Obviously, if you simply added a new isPublished key to the fixtures.yml file, it should *not* be happening - the changes should be unaffected. So, that's the mystery. Here's what I would do: add a var_dump($name) inside your setName() function. If you see the correct value dumped (a string, not null) , then we know that this method is being called (and that the problem is elsewhere). If you see null dumped, we know that we have a problem in fixtures.yml - something is telling Alice to call this with null. If you do *not* see the method called, then the problem is probably also in fixtures.yml - we are missing something that would cause setName to be called. Obviously, this would happen if the "name" key were missing from fixtures.yml, but I'm guessing you checked for that ;).

Anyways, do a little digging and let me know what you find out - I'm happy to help.

Cheers!

Reply
Jonas P. Avatar
Jonas P. Avatar Jonas P. | posted 4 years ago

This is amazing. This tool will replace my old "O Incrível Enchedor de Linguiça" !!!!

Reply

Hey Jonas P.

What tool is that? :)

Reply
Default user avatar

Hey guys, quick doctrine migration question. Is there a way to specify executing the last created diff? If there are old diffs in my DoctrineMigrations directory and I don't delete them it tries to run those before the latest diff. Is that normal behavior?

Reply

Hey Todd,

Quick answer: to know what diff is the latest use: "bin/console doctrine:migrations:latest" which give you YYYYMMDDHHMMSS ID of the latest migration, and then with "bin/console doctrine:migrations:execute YYYYMMDDHHMMSS" execute it.

But seriously, it sounds as normal behavior. You need to understand how migrations work: Doctrine stores all the IDs of executed migrations in your DB, that's how Doctrine know what migrations were executed in your system. So when you add more migrations and run "bin/console doctrine:migrations:migrate", Doctrine look into that table and execute only those which are not in that table yet, i.e. execute fresh migrations which have not been executed yet. But if you want to skip a migration for some reasons, you can do it with "bin/console doctrine:migrations:version YYYYMMDDHHMMSS" - use "--add" or "--delete" options to add the current migration to or delete from the table.

This docs could be useful for you I think: https://symfony.com/doc/mas... . Also, executing "bin/console list doctrine:migrations" will show you available commands and some description about each command - good way to quickly understand capabilities of the bundle.

Cheers!

Reply
Default user avatar
Default user avatar Simon Carr | posted 5 years ago

I need to generate a value that is less than the value faker has generated in another field. If I create a custom provider, how do I pass in the value that faker has created for the other field? i.e

stock: <numberbetween(100,5000)> //Produces 823
reorderQty: <customreorderqty($stock)> //Would return a value less than 823

Thanks

Reply
Default user avatar

Sorry, I just answered my own question. The above code works, just use a $fieldName to refer to a field value in the same record

Reply

Hey Simon,

Glad you got it working so fast :)

Cheers!

Reply
Default user avatar
Default user avatar Maciek Kasprzak | posted 5 years ago | edited

Hi Guys. First of all great work; love your tutorials. I was trying to code along using latest Alice version and obviously run into buch of problems. I tried to use there solution, however I couldn't use <i>$this->container</i> inside <em>LoadFixtures.php</em>; duno why :/ So I played with it for couple hours and came up with this:

<strong>1.</strong> I read <a>https://github.com/nelmio/alice/blob/master/doc/customizing-data-generation.md#custom-faker-data-providers&lt;/a&gt; and based on docs created my <i>GenusProvider</i> class like this:


namespace AppBundle\DataFixtures\ORM;
class GenusProvider
{
    public function genus()
    {
        $genera = [
            'Octopus',
            'Baleana',
            'Orcinus',
            'Hippocampus',
            'Asterias',
            'Amphiprion',
            'Carcharodon',
            'Aurelia',
            'Cucumaria',
            'Balistoides',
            'Paralithodes',
            'Chelonia',
            'Trichechus',
            'Eumetopias'
        ];

        $key = array_rand($genera);

        return $genera[$key];
    }
}

If you look carefully, you can see I didn't extend BaseProvider. I tried but it didn't work in my case.

<strong>2.</strong> Then you should:
<blockquote>Then you can add it to the Faker Generator used by Alice by either overriding the NativeLoader::createFakerGenerator() method.</blockquote>
This was the hardest part because in tutorial we don't mess with the loader class. This is where I spent most of the time. My solution is to extend the <i>NativeLoader</i> class like this:


namespace AppBundle\DataFixtures\ORM;

use Nelmio\Alice\Loader\NativeLoader;
use Faker\Factory as FakerGeneratorFactory;
use Faker\Generator as FakerGenerator;

class NewNativeLoader extends NativeLoader
{
    protected function createFakerGenerator(): FakerGenerator
    {
        $generator = FakerGeneratorFactory::create(parent::LOCALE);
        $generator->addProvider(new GenusProvider());
        $generator->seed($this->getSeed());

        return $generator;
    }
}

As you can see this the place where all the add-provider-magic happens. I overrode <i>createFakerGenerator</i> method and used new fance <i>GenusProvider</i>

<strong>3. </strong> Now let's go back to original <i>LoadFixtures.php</i>. Instead of using <i>NativeLoader</i> i used my <i>NewNativeLoader</i>. Code looks like this:


namespace AppBundle\DataFixtures\ORM;

use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;

class LoadFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $loader = new NewNativeLoader();

        $objectSet = $loader->loadFile(__DIR__.'/fixtures.yml')->getObjects();
        foreach($objectSet as $object) {
            $manager->persist($object);
        }
        $manager->flush();
    }
}
  • All the files mentioned (<i>GenusProvider.php</i>, <i>LoadFixtures.php</i>, <i>NewNativeLoader.php</i>) live in the same ...\ORM directory
  • I didn't mess with the services as it had no impact and i still ended up seeing "unknown formatter" message in the console
  • I will try to put my code on github as soon as I finish the Symfony 3 tutorials

I bet there is a better way to solve this; mine just works ;)

Reply

Hey Maciek,

Thanks for sharing it with others. Also, keep in mind that we're using "nelmio/alice" of version v2.1 in this tutorial. If you install the latest bundle - it has some BC breaks and its usage may be different than that we're showing in this screencast.

Cheers!

Reply
Richard Avatar
Richard Avatar Richard | posted 5 years ago

Why is my phpstorm adding comments for the return/parameter type when I create a getter or setter using Code Generation? Abd its only calling the type "mixed". Not boolean, for example even though it's an "isPublished" type format for the name. I see your PHPStorm doesn't produce these php doc type comments. eg

/**
* @param mixed $isPublished
*/
public function setIsPublished($isPublished)
{
$this->isPublished = $isPublished;
}

Reply

Hey Richard ,

Well, it depends on PhpStorm version. Btw, do you use @var notation for class properties? PhpStorm based on var type, try to generate setter/getter with this code:


class YourEntityName
{
  /**
   * @var bool
   * @ORM\Column(...)
   */
  private $isPublished;
}

When you specify var types explicitly, PhpStorm will use it in annotation for setters/getters.

Cheers!

Reply
Default user avatar
Default user avatar Syed | posted 5 years ago | edited

If anyone is using Alice 3.0, you will need to create a GenusProvider class based off https://github.com/nelmio/alice/blob/master/doc/customizing-data-generation.md#custom-faker-data-providers. I stuck it in the same folder as my LoadFixtures.php so it had the namespace AppBundle\DataFixtures\ORM and dropped the genus() function in there.

You also need to changed services.yml to get Alice to pick up the new provider. Under the services:


AppBundle\DataFixtures\ORM\GenusProvider:
   tags: [nelmio_alice.faker.provider]

After that run ./bin/console doctrine:fixtures:load again and it should be working OK.

Reply

Yo Syed!

This is awesome! Thanks for posting this! We're going to use your suggestions here to make an official note that we can add to the tutorial to help others :).

Cheers!

Reply
Authoritas Avatar
Authoritas Avatar Authoritas | Syed | posted 5 years ago | edited

Ok, I've adjusted the services.yml file as above and created the GenusProvider class. However, I'm getting an error.

Tried this:


    public function load(ObjectManager $manager)
    {
        $loader = new NativeLoader(null, ['providers' => [$this]]);
        $objectSet = $loader->loadFile(__DIR__.'/fixtures.yml')->getObjects();

        foreach($objectSet as $object) {
            $manager->persist($object);
        }
        $manager->flush();
    }

..and this:


    public function load(ObjectManager $manager)
    {
        $loader = new NativeLoader();
        $objectSet = $loader->loadFile(__DIR__.'/fixtures.yml',
            ['providers' => [$this]])->getObjects();

        foreach($objectSet as $object) {
            $manager->persist($object);
        }
        $manager->flush();
    }

Same result:


[InvalidArgumentException]  
  Unknown formatter "genus"
Reply
Default user avatar
Default user avatar Syed | Authoritas | posted 5 years ago | edited

Hey!

As Authoritas already mentioned, it's probably best to just stick to Alice 2 for now.

However, I have uploaded my working version to GitHub - https://github.com/sjhuda/knp-aqua_note. Everything should be in the same place as the videos so far, it might be worth comparing my code to what you have.

I just ran it right now and I get


➜  aqua_note ./bin/console doctrine:fixtures:load
Careful, database will be purged. Do you want to continue y/N ?y
  > purging database
  > loading Doctrine\Bundle\FixturesBundle\EmptyFixture
  > loading AppBundle\DataFixtures\ORM\LoadFixtures

Oh, I also forgot to mention that I switched over to using the service that comes with Alice 3 (as opposed to the NativeLoader class), that might be causing your issue (see my comment on the previous video).

2 Reply

Thanks for sharing it!

We added a note to the script so people don't get confused anymore

Cheers!

1 Reply
Default user avatar

I tried your code for Alice 3.1, but it threw strange error.

A "tags" entry must be an array for service "AppBundle\DataFixtures\ORM\GenusProvider" in /home/dd/dd/start/app/config/services.yml. Check your YAML syntax.

Do you have idea how to solve this?

Reply

Hey Danijela

How did you define the Provider service? I think you only have to sorround the tags property with `[` `]`

Cheers!

1 Reply
Default user avatar

thanks!

Reply

Authoritas can check the codes which Authoritas shared on git. its working with Alice 3. just copy the codes from fixtures.yml GenusProvider.php and LoadFixtures.php and paste them urs. and add the service. its working for me

Reply

Hey Mesut,

Thanks for confirming that solution works for you, I'm glad to hear it.

Cheers!

Reply

Hey Authoritas

I assume you are using Alice 3. That version contains many changes, actually the developers said that it shares nothing to version 2, even the persistent layer was removed
I can advice you to use in addition AliceBundle (hautelook/alice-bundle) so you can persist your objects

I hope it helps you :)
Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice