introduction
This is just an easy tip on how to use symfony fixtures along with doctrine event listeners in a symfony project.
event listeners
Doctrine Event Listeners are the methods you define to trigger an activity always after/before a Doctrine Record is created, updated, deleted, etc. These can be: postInsert, preInsert, postDelete, preDelete and so on. They can be very useful to keep the code clean and logical. A good example of usage is uploading an image and creating many different files with different sizes, representing the same image (custom thumbnails). This could be done with overriding ImageForm upload mechanisms, but postInsert seems more clean and easier to maintain if uploading is done using more than one symfony form class.
fixtures
Fixtures are a built-in symfony feature which enables you to easily populate the database with some data. It is usually test data, but for some projects, some initial data can be held in fixtures (e.g. initial configuration values). They should be used in all projects, since it's a lot easier to spot any bug using data that pretends to be real.
the problem
The problem occurs when you combine those two features. Imagine you've got two models in your projects, A and B, related 1:1 (e.g. sf_guard_user with sf_guard_user_profile from sfDoctrineGuardPlugin). For each A record there has to exist the B record. This means, when the mother record is created (suppose A is the master model), the B record needs to be immediately created and related to A. A Doctrine Event Listener is what should be used - a A::postInsert() that creates the B record.
So far, so good - but we still want to use symfony fixtures. Let's say, we want to define 10 sample A records along 10 sample B records. So we write a fixture file with 10 A and 10 B fixtures. Next, we fire the
./symfony doctrine:build --all --and-loadcommand and what do we find? B records are doubled! And this can be quite difficult to spot!
Each time when a A fixture is saved, the postInsert is automatically executed, therefore new B record is created. So instead of creating records in the following order: A,A,A,...(10),B,B,B,...(10), we get A,B,A,B,A,B,...(10),B,B,B,...(10), having 10 A records and 20 B records. And this is not what we wanted. Fortunately, this can be quite easily fixed.
the solution
Just add one line to the Doctrine Event Listener:
public function postInsert($event) { if ('cli' != php_sapi_name()) { // some code here } }We're basing on the fact, that fixtures are loaded from a task, which uses command line interface (cli). Now, no fixtures use Doctrine Event Listeners.
Tomasz
ReplyDeleteMany thanks for this - I spent a whole day trying to work round the issue until I discovered your post.
Thanks again
Paul
yep, unfortunately, this problem is very difficult to spot... I'm glad I helped :)
ReplyDeleteI think there's a further problem here, as when using the functional test framework, this also runs in the CLI which means that whilst the above approach fixes the problem with fixtures, it will break functional testing. Any thoughts?
ReplyDeletePaul
Paul,I'm actually having sort of the same issue with unit tests and propel. If I run a single unit test everything goes well, if I call several from a batch script, a preDelete starts failing if I return true, if I don't return anything using your if ('cli' != php_sapi_name()) condition, the code behaves like expected. Haven't got a clue why though, very strange.
ReplyDeleteFinally.. Thanks you so much !
ReplyDelete