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.
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:
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.
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'