If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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!
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!
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:
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
.
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.
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!
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.
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!
// 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
}
}
in AppKernel.php I have the following declarations
I made CharacterProcessor filename in src\AppBundle\DatFixtures\ORM:
In services.yml I put
Every time I try to run 'app/console hautelook:fixtures:load' says:
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'