If you've ever tried to write a symfony unit test for a model class - or for any piece of code that interacts with the database - then you know what a headache it can be. sfPropelTestPlugin to the rescue! This plugin loads all the necessary symfony components for database interaction, and goes a step further: test data is automatically reloaded to the test database at the beginning of each test case, so you don't have to worry about your tests interacting with each other. Here's an example of a unit test written with sfPropelTestPlugin:

class myUnitTest extends sfPropelTest
{
  public function setup()
  {
    $this->bob = UserPeer::retrieveByPK('bob');
  }

  public function teardown()
  {
    $this->diag('Test method complete!');
  }

  public function test_user()
  {
    $this->is($this->bob->getId(), 'bob', 'Bob exists!');

    $user = new User();
    $user->setFirstName('Joe');
    $user->setLastName('Smith');
    $user->setUsername('joe');
    $user->save();

    $joe = UserPeer::getBy(UserPeer::USERNAME, 'joe');
    $this->isa_ok($joe, 'User', 'Joe exists!');
  }

  public function test_dataDelete()
  {
    $this->is($this->bob->getId(), 'bob', 'Bob still exists!');

    $joe = UserPeer::getBy(UserPeer::USERNAME, 'joe');
    $this->ok(!$joe, 'Joe no longer exists.');
  }
}

$test = new myUnitTest();
$test->execute();

If you've written tests in Ruby on Rails, the above should look familiar. If not, let's go through the code:

The first two lines are setup: The first line tells symfony what application to test - in this case, "myApp" - and the second line includes the code we need to test against the database. Next we define our "test case" - a class that extends sfPropelTest, which is itself a child of lime_test. Every method that begins with "test_" will be called in turn, with our test data being reloaded between each call. Also, the setup() method, if defined, will be called immediately before each test method, and the teardown() method, if defined, will be called immediately after. The last two lines create a new instance of our test suite, and call its execute() method, which runs our tests.

There you have it - a quick and painless way to run unit tests in symfony. One less excuse for not writing them!

I have updated my plugin for doing model-based validation in symfony. The new plugin uses the Propel builders instead of behaviors, so it is now called sfPropelValidatePlugin instead of the former (unwieldy) name sfPropelValidateBehaviorPlugin. If you have not investigated the Propel builder classes, I suggest you give them a look. By overriding the default classes used to build your Propel object and peer classes, you can write your modifications directly into your model's base classes. This is a much cleaner and more readable solution than using Propel behaviors, and you avoid the performance hit that behaviors incur.

The default Propel builders can be found in the symfony source in these places:

symfony/addon/propel/builder/sfObjectBuilder.php
symfony/addon/propel/builder/sfPeerBuilder.php

The classes these inherit from are buried. They can be found here:

symfony/vendor/propel-generator/classes/propel/engine/builder/om/php5/PHP5ComplexObjectBuilder.php
symfony/vendor/propel-generator/classes/propel/engine/builder/om/php5/PHP5ComplexPeerBuilder.php

SnippetsEmu allows you to create TextMate-style snippets in Vim which, together with Vim's native code-completion feature, makes Vim a top choice among lightweight text editors. When I installed Snippy (as its creator, ironically, likes to call it), I had one problem: SnippetsEmu maps its functionality to the <Tab> key, which is what I map code completion to. Thus, my mapping was causing Snippy not to work. As I was not about to oppose years of bash-trained muscle memory by mapping completion to a different key, I decided to bite the bullet (that hurts, by the way) and try to edit the plugin's vim script. It turned out to be a very easy edit. Apparently the saints were smiling on me - or, at least, the patron saint of Not Having to Learn Vim Scripting.

Note that if you are using the SuperTab script, support is built into SnippetsEmu. The following is for everyone not using that script who wants to map the tab key.

Open snippetsEmu.vim and find the function s:ReturnKey. In my version, it's on line 627. This function gets called when no snippet is found; all it does is output the tab character (or whatever key the snippets are mapped to). We want this function to output Control-P instead, since that is Vim's native code-completion trigger. Find the line in s:ReturnKey that looks like this:

exe "return \"".substitute(g:snippetsEmu_key, '^<', "\\\\<","")."\""

This line might have been changed in your version, so just find the line that seems to do the same thing - output g:snippetsEmu_key. Replace that line with this:

return "\<c-p>"

That's it! If that doesn't work, all I know to do is pray. I suggest you pray to the patron saint of Damnit I Have to Learn Vim Scripting.

Through Flash's FileReference and FileReferenceList classes, you can create a powerful file uploader that allows the user to upload multiple files with a single form element. But beware: unless the user happens to be using IE, the upload will use a new browser session to upload the files. This means that if you require that a user be authenticated before uploading something (and you better be), the upload won't work - the request will be forwarded to the login form, or to wherever your system forwards unauthorized requests. This is a maddening bug to track down, and there is nothing you can do to make Flash use the right session. The work around is to send the session cookie in the url and, on the server side, use that to override the new (and wrong) session cookie sent by Flash. Here's how that works in symfony, although much of the following is useful for other languages or frameworks:

First, we have to tell our Flash component what the cookie is, so it can roll it into the URL. One solution would be to pass the session cookie as an argument in FlashVars, but then the user's session cookie is sitting unencrypted in the HTML, leaving them vulnerable to cross-site request forgery. Better to use javascript to fetch the cookie, and then put it in FlashVars. If you are using ufo.js, the first argument to UFO.create() should look like this:

{
  movie: '/flash/uploader.swf', 
  id: 'uploader',
  name: 'uploader',

  flashvars: 'cookie=' + document.cookie,

  // other options ...
}

Note that if you are dealing with multilple cookies, you will want to parse the output of document.cookie and send only the desired cookie.

Next, add the cookie to the URL in your ActionScript (this is for ActionScript 2.0):

var list:Array = myFileRefList.fileList;
var item:FileReference;
var url:String = _root.uploadURL + '?cookie=' + _root.cookie;

for (var i:Number = 0; i < list.length; i++) {
  item.upload (url);
}

That does it for the client side. Now, we need to tell PHP to use our cookie instead of the one the browser sent. This does the trick:

list($cookieName, $cookieValue) = str_split('=', $_GET['cookie']);

session_name($cookieName);
session_id($cookieValue);

session_start();

If you are using symfony, things are a little more complex. We need to call session_name() and session_id() before session_start(); looking at the symfony source code, we find that session_start() is called in sfSessionStorage::initialize(). So a simple solution is to extend the sfSessionStorage class:

class mySessionStorage extends sfSessionStorage
{
  public function initialize($context, $parameters = null)
  {
    if ( /* whatever the condition is when we want to do this */ ) {
      if ($cookie = $context->getRequest()->getParameter('cookie')) {
        $name = 'symfony';
        preg_match('/^' . $name.'=(.*)$/', $cookie, $asMatch);
        $value = $asMatch[1];

        session_name($name);
        session_id($value);
      }
    }

    parent::initialize($context, $parameters);
  }
}

Finally, tell symfony to use the mySessionStorage class by editting factories.yml:

all:
  storage:
    class: mySessionStorage
    param:
      session_name: symfony

And you're done! Clear your cache, and try it out. An easy way to check if it's working is to add the following line to the end of index.php (or frontend.php):

file_put_contents('testSessionOverwrite.txt', sfContext::getInstance()->getRequest()->getActionName());

Now try uploading a file. If the action name in testSessionOverwrite.txt is the action you're uploading to, you're golden. If instead it is the name of the action that authenticates users, you have a problem somewhere, and you get to have the enjoyable experience of debugging PHP without browser output. Remember to erase the debugging line from your front controller when you get everything working.

Next time, just make the users upload their damn files one-at-a-time...