Buy
This tutorial has a new version in planning, check it out!

Tip

A newer version of HauteLookAliceBundle has been released and portions of this tutorial won't apply to that new version.

There's a little issue. These two kittens are the exact same filename. The first is kitten2.jpg and so is the second. That's fine for us, but imagine if we could delete characters, and if doing that deleted the image. If we deleted this character, it would delete the image for the second one too. To be more realistic, each character needs a unique image.

NO problem. Setup a new $targetFilename instead of using the original filename. Set it to fixtures_ then mt_rand() and .jpg:

... lines 1 - 15
public function preProcess($object)
{
... lines 18 - 26
$targetFilename = 'fixtures_'.mt_rand(0, 100000).'.jpg';
... lines 28 - 36
}
... lines 38 - 49

Copy the file to this filename. And make sure that the avatarFilename is our new, random thing:

... lines 1 - 15
public function preProcess($object)
{
... lines 18 - 26
$targetFilename = 'fixtures_'.mt_rand(0, 100000).'.jpg';
... lines 28 - 29
$fs->copy(
$projectRoot.'/resources/'.$object->getAvatarFilename(),
$projectRoot.'/web/uploads/avatars/'.$targetFilename,
true
);
$object->setAvatarFilename($targetFilename);
}
... lines 38 - 49

Time to reload those fixtures:

php app/console doctrine:fixtures:load

In web/uploads/avatars, we see a bunch of random filenames. And when we refresh, they're all using different filenames.

Accessing the container in a Processor

You can do whatever you want inside a Processor, but with a glaring limitation so far: you don't have access to the container or any of your services.

Let's try to log the random filenames being used for each Character. That means we'll need the logger service, and right now we don't have access to anything. To get it, we'll treat AvatarProcessor like any other service and use dependency injection. Create a __construct() function, and type-hint the argument with LoggerInterface from PSR. That'll add my use statement. Now, set that on a logger property:

... lines 1 - 6
use Psr\Log\LoggerInterface;
... lines 8 - 9
class AvatarProcessor implements ProcessorInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 18 - 61
}

Before worrying about how we'll pass in the logger, go down below and log a debug message. Fill in the placeholders with the object's name, the $targetFilename and then the original avatarFilename:

... lines 1 - 9
class AvatarProcessor implements ProcessorInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 18 - 23
public function preProcess($object)
{
... lines 26 - 43
$this->logger->debug(sprintf(
'Character %s using filename %s from %s',
$object->getName(),
$targetFilename,
$object->getAvatarFilename()
));
$object->setAvatarFilename($targetFilename);
}
... lines 52 - 61
}

This class is not registered as a service - we just create it manually in AppFixtures:

... lines 1 - 7
class AppFixtures extends DataFixtureLoader
{
... lines 10 - 48
protected function getProcessors()
{
return array(
new AvatarProcessor()
);
}
}

Passing the logger in is simple. The base DataFixturesLoader class has the container and puts it on a $container property, just like a Controller. So we can say $this->container->get('logger'):

... lines 1 - 7
class AppFixtures extends DataFixtureLoader
{
... lines 10 - 48
protected function getProcessors()
{
return array(
new AvatarProcessor($this->container->get('logger'))
);
}
}

To test this out, open up a new tab and let's tail the app/logs/dev.log directory, because app/console runs in the dev environment by default. And let's grep it for the word Character:

tail -f app/logs/dev.log | grep "Character"

Now reload the fixtures!

php app/console doctrine:fixtures:load

No errors, AND we get our log messages. Btw, you can also see log messages directly when running a command by passing the -vvv option:

php app/console doctrine:fixtures:load -vvv

This can be pretty handy.

This means that there's nothing you can't do with a Processor. Need a service? Just use normal dependency injection, pass it in, do awesome things with your fixtures, then celebrate.

Cheers!

Leave a comment!

  • 2018-06-04 Victor Bocharsky

    Hey Matt,

    So yes, most of the time it's easier to manually load those data, probably you can just export / import the data. It'd not be 100% automated, but I think it's OK to start with it. Or, you can even create a one-time migration command, that you'll execute on the production after the first deploy and then remove it on the next deploy, but once again I do not think it's worth it. I'd prefer to manually import initial data after the first deploy.

    Cheers!

  • 2018-06-01 Matt Johnson

    > The only valid case I see is when you want to do the first deploy to production.

    Yeah,t hat's the case I'm thinking about. Migrations is an interesting idea...

  • 2018-06-01 Victor Bocharsky

    Hey Matt,

    Yeah, it sounds like fixtures... but this data already should be on production. What I mean is that you probably do not want to reload those data on production because you'll lose real data. The only valid case I see is when you want to do the first deploy to production. But on the second, 3rd, etc. deploy those data already *should* be there. But if you need to inject more data with further deploys - you probably talking about some kind of migrations.

    Cheers!

  • 2018-06-01 Matt Johnson

    Haha Yeah I know. Maybe what I'm referring to isn't fixtures? It's data that has to be there for the app to function...

  • 2018-06-01 Victor Bocharsky

    Hey Matt,

    Loading fixtures in production is not a good practice, well, because you probably have a real data in the DB, so overwriting it would be fatal :) That's why fixtures bundle usually load for dev/test environments only in kernel.

    Anyway, not sure it's possible with Alice bundle, but it's possible with DoctrineFixturesBundle, where you can easily access container from the fixtures: https://symfony.com/doc/mas... - which means you can get current environment like: $env = $this->container->getParameter("kernel.environment"); . So with simple if and return statements in the beginning of the fixtures file you can skip some fixtures for some environment.

    Cheers!

  • 2018-06-01 Matt Johnson

    Is there an easy thing to only have it load fixtures for certain environments?

    I.E. My app has fixtures that it needs to add in order to function. When I load it on prod (or even stage) I'll need to add those fixtures, but I'd also like to be able to load a bunch of fixtures for testing in the dev/test environment.

    Thanks!

  • 2015-06-03 weaverryan

    BEST COMMENT EVER :).

    Yea, it's ignored by VCS, so I wasn't too worried about it. But an alternative to letting the directory fill up with time would be to automate the proverbial burlap sack, and delete the directory automatically before loading your fixtures (the Filesystem class can remove the directory, then you can re-create it). You could do that in getFixtures() or override load(), do this, then call parent::load() if that feels a bit better.

    Cheers!

  • 2015-06-03 Michael Sypes

    What about all the built-up cruft you'd end generating over time in that files directory, i.e., the "kindle" of kitten photos?
    Or is this of no concern because such a directory would be ignored by your VCS, and you can easily just overwrite as you go? The alternative would be the need to periodically go through the directory with a burlap sack and cinder block, which would be as horrid as the image I've just used.