Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!

Processors: Do Custom Stuff While Loading

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 $3.00

Tip

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

I don't want to brag, but these are probably the nicest super-hero fixtures ever. But we've neglected a column! The avatar.

Check out the Character entity - we have a column for this called avatarFilename:

... lines 1 - 10
class Character
{
... lines 13 - 54
/**
* @ORM\Column(nullable=true)
*/
private $avatarFilename;
... lines 59 - 139
}

It's going to hold just the filename part of an image, like trogdor.png or pac-man-is-the-man.jpg. This makes sense if you look in the template for the homepage. If there's an avatarFilename, we print an img tag and expect the image to be in some uploads/avatars directory, relative to web/:

... lines 1 - 16
{% for character in characters %}
<tr>
... lines 19 - 24
<td>
... lines 26 - 31
{% if character.avatarFilename %}
<img src="{{ asset('uploads/avatars/'~character.avatarFilename) }}" alt="{{ character.name }}"/>
{% endif %}
</td>
</tr>
... lines 37 - 45

Oh boy, so this means the avatar is a bit harder. Yea, we have to set the value in the database, but we also need to make sure to put a corresponding image file into this directory. I don't want a bunch of broken images!

Filling in avatarFilename Data

But, we'll worry about that later. First, let's get some values into the avatarFilename field. Open up characters.yml and start to set the avatarFilename.

AppBundle\Entity\Character:
... lines 2 - 9
character{2..10}:
name: <characterName()>
... lines 12 - 16
avatarFilename:

For any of this to work, we're going to need some real image files handy. Fortunately, I got some for us! They live in a resources directory at the root of the project:

resources/
    kitten1.jpg
    kitten2.jpg
    kitten3.jpg
    kitten4.jpg

But since I want to avoid any trademark legal battles with Nintendo, I've decided that instead of Mario and Yoshi, we'll use readily-available images of kittens. Thank you Internet.

So we need our value to be one of these. Let's setup a custom Faker formatter like we did before. Call this one avatar():

AppBundle\Entity\Character:
... lines 2 - 9
character{2..10}:
name: <characterName()>
... lines 12 - 16
avatarFilename: <avatar()>

Try reloading the fixtures now:

php app/console doctrine:fixtures:load

Ah, there's our error!

Unknown formatter "avatar"

Time to fix that! Open AppFixtures and create a new public function called avatar(). To keep things lazy, let's copy the guts of characterName() and update the options to be kitten1.jpg, then 2, 3 and 4. Sweet!

... lines 1 - 7
class AppFixtures extends DataFixtureLoader
{
... lines 10 - 36
public function avatar()
{
$filenames = array(
'kitten1.jpg',
'kitten2.jpg',
'kitten3.jpg',
'kitten4.jpg',
);
return $filenames[array_rand($filenames)];
}
}

Reload reload! ... the fixtures:

php app/console doctrine:fixtures:load

Great, and now reload our page. Ah, broken images! Yay! The img tags are printing out beautifully, but there isn't actually a kitten3.jpg file inside the uploads/avatars directory. We've got work to do!

Creating the Processor

This is where Processors come in. Whenever you need to do something other than just setting simple data, you'll use a Processor, which is like a hook that's called before and after each object is saved.

Step1! Create a new class. It doesn't matter where it goes, so put it inside ORM/ and call it AvatarProcessor. The only rule of a processor is that it needs to implement ProcessorInterface. And that means we have to have two methods: postProcess() and preProcess().

Each is passed whatever object is being saved right now, so let's just dump the class of the object:

<?php
namespace AppBundle\DataFixtures\ORM;
use Nelmio\Alice\ProcessorInterface;
class AvatarProcessor implements ProcessorInterface
{
/**
* Processes an object before it is persisted to DB
*
* @param object $object instance to process
*/
public function preProcess($object)
{
var_dump(get_class($object));
}
/**
* Processes an object before it is persisted to DB
*
* @param object $object instance to process
*/
public function postProcess($object)
{
// TODO: Implement postProcess() method.
}
}

Cool new processor class, check! To hook it up, go back into AppFixtures. The parent DataFixturesLoader class has an empty getProcessors() method that we need to override. Because it's empty, we don't need to call the parent. Just return an array with a new AvatarProcessor object in it:

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

Let's reload the fixtures to see what happens!

php app/console doctrine:fixtures:load

Cool! It calls preProcessor for every object - whether it's a Universe or a Character.

Moving Images Around

Ok, let's copy some images. First, we only want to do work if the object that's passed to us is a Character. So, if we're not an instance of Character, just return:

... lines 1 - 15
public function preProcess($object)
{
if (!$object instanceof Character) {
return;
}
... lines 21 - 33
}
... lines 35 - 46

Next, some Character's don't have an avatar, so if this doesn't have an avatarFilename, we'll just return - we don't need to move any files around:

... lines 1 - 15
public function preProcess($object)
{
... lines 18 - 21
if (!$object->getAvatarFilename()) {
return;
}
... lines 25 - 33
}
... lines 35 - 46

Now we know there's an avatarFilename. We also know that the originals live in this resources/ directory, so we just need to copy those into the web/uploads/avatars directory.

First, create a variable that points to the root directory of our project. This will get me all the way back to the root - there are other ways to do this, but this is simple.

To do the copying, let's use Symfony's Filesystem object - it does nice things like create the directory if it doesn't exist. And hey, that's nice! My editor just added the use statement for me. Now, call copy(). The original file is $projectRoot, resources, then the avatarFilename. The destination is $projectRoot again, then to web/uploads/avatars then the object's avatarFilename:

... lines 1 - 15
public function preProcess($object)
{
... lines 18 - 25
$projectRoot = __DIR__.'/../../../..';
$fs = new Filesystem();
$fs->copy(
$projectRoot.'/resources/'.$object->getAvatarFilename(),
$projectRoot.'/web/uploads/avatars/'.$object->getAvatarFilename(),
true
);
}
... lines 35 - 46

We're using this directory because that's what my app is expecting in the template. The third argument is whether to override an existing file. And that should get the job done! Reload those fixtures!

php app/console doctrine:fixtures:load

Now refresh! Ok, super-hero kittens! And if you want to know how to get access to the container in a Processor, keep watching.

Leave a comment!

4
Login or Register to join the conversation
Diaconescu Avatar
Diaconescu Avatar Diaconescu | posted 5 years ago | edited

in AppKernel.php I have the following declarations


$bundles[] = new Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle();
$bundles[] = new Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle();
$bundles[] = new Hautelook\AliceBundle\HautelookAliceBundle();

I made CharacterProcessor filename in src\AppBundle\DatFixtures\ORM:


namespace AppBundle\DataFixtures\ORM;

use Fidry\AliceDataFixtures\ProcessorInterface;
use AppBundle\Entity\Character;

class CharacterProcessor implements ProcessorInterface{
 /**
 * {@inheritdoc}
 */
  public function preProcess($object){
}
  
  public function postProcess($object){
    // TODO: Implement postProcess() method.
  }
}

In services.yml I put


services:
  alice.processor.character:
    class: AppBundle\DataFixtures\ORM\CharacterProcessor
    tags: [ { name: fidry_alice_data_fixtures.processor } ]

Every time I try to run 'app/console hautelook:fixtures:load' says:


PHP Fatal error:  Declaration of AppBundle\DataFixtures\ORM\CharacterProcessor::preProcess($object) must be compatible with Fidry\AliceDataFixtures\ProcessorInterface::preProcess(string $id, $object) in /home/petrero/www/symfony_3/Making_Fixtures_Awesome_with_Alice/src/AppBundle/DataFixtures/ORM/CharacterProcessor.php on line 8

I don't understand why is so. I forget to configure something or what?

We may clone the respective branch with 'git clone -b 2.Processors{Do_Custom_Stuff_While_Loading} git@github.com:petre-symfony/making-fixtures-awesome-with-Alice-knp-tutorial.git'

Reply

Hey Diaconescu ,

From the error I see that your preProcess() method must be compatible with "ProcessorInterface::preProcess(string $id, $object)" which means you have invalid method signature, i.e. you missed $id as the first argument. Check the preProcess() and postProcess() methods in "Fidry\AliceDataFixtures\ProcessorInterface" interface.

Cheers!

Reply
Diaconescu Avatar
Diaconescu Avatar Diaconescu | Victor | posted 5 years ago

I saw that. Indeed in vendor/theofidry/alice-data-fixtures/src/ProcessorInterface file is this code:

*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types = 1);

namespace Fidry\AliceDataFixtures;

/**
* Processor are meant to be used during the loading of files via a {@see LoaderInterface} in the scenario of having the
* loaded objects persisted in the database.
*
* @see \Fidry\AliceDataFixtures\Loader\PersisterLoader For an example of usage of processors
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface ProcessorInterface
{
/**
* Processes an object before it is persisted to DB.
*
* @param string $id Fixture ID
* @param object $object
*
* @return
*/
public function preProcess(string $id, $object);

/**
* Processes an object after it is persisted to DB.
*
* @param string $id Fixture ID
* @param object $object
*
* @return
*/
public function postProcess(string $id, $object);
}
Is true that they have an id parameter there.
But in vendor/theofidry/alice-data-fixtures/src/doc/processors.md they have an example how this processorInterface may be used:
# Processors

Processors allow you to process objects before and/or after they are persisted. Processors
must implement the [`Fidry\AliceDataFixtures\ProcessorInterface`](../src/ProcessorInterface.php).

Here is an example where we may use this feature to make sure passwords are properly
hashed on a `User`:

```php
namespace MyApp\DataFixtures\Processor;

use Fidry\AliceDataFixtures\ProcessorInterface;
use MyApp\Hasher\PasswordHashInterface;
use User;

final class UserProcessor implements ProcessorInterface
{
/**
* @var PasswordHashInterface
*/
private $passwordHasher;

/**
* @param PasswordHashInterface $passwordHasher
*/
public function __construct(PasswordHashInterface $passwordHasher)
{
$this->passwordHasher = $passwordHasher;
}

/**
* {@inheritdoc}
*/
public function preProcess($object)
{
if (false === $object instanceof User) {
return;
}

$object->password = $this->passwordHasher->hash($object->password);
}

/**
* {@inheritdoc}
*/
public function postProcess($object)
{
// do nothing
}
}
```

In Symfony, if you wish to register the processor above you need to tag it with the
`fidry_alice_data_fixtures.processor` tag:

```yaml
# app/config/services.yml

services:
alice.processor.user:
class: AppBundle\DataFixtures\Processor\UserProcessor
arguments:
- '@password_hasher'
tags: [ { name: fidry_alice_data_fixtures.processor } ]
```

Previous chapter: [Basic usage](../README.md#basic-usage)

Next chapter: [Purge data](purge_data.md)

As you may see in their example is only $object parameter. I must miss something. What? I created my code looking after their example. Teoretically should be worked. I don't understand how, because I saw this discrepancy myself.

Reply

Hey Diaconescu ,

That's mean their docs is outdated, so you have 2 ways:
1. Use the v1.0.0-beta.2 release which has the same signature of ProcessorInterface you see in examples,
2. Use the latest release version... and implement the new signature. Of course, feel free to open an issue about this discrepancy on their repo if it isn't yet, I bet guys who maintain the bundle will help 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.3.3",
        "symfony/symfony": "2.5.*", // v2.5.5
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.1.0
        "symfony/assetic-bundle": "~2.3", // v2.5.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.7
        "symfony/monolog-bundle": "~2.4", // v2.6.1
        "sensio/distribution-bundle": "~3.0", // v3.0.6
        "sensio/framework-extra-bundle": "~3.0", // v3.0.2
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "~0.1" // 0.1.5
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.4.0
    }
}
userVoice