Sunday 31 January 2010

Auto setting a user's group(s) using sfDoctrineGuardPlugin

I tried a number of different ways to achieve adding a user to a group automatically today before stumbling across the 'correct' way as per a forum reply from Fabien himself today.

I was trying all sorts of different methods, in the form, in the action, whereever... and finally it was pretty easy.

So let's assume you have RegisterForm which extends the sfGuardUserForm. The proper way to do it is in the doSave method of that form.

So your code might look something like...


class RegisterForm extends sfGuardUserForm
{
    //...
    protected function doSave($con = null)
    {
        $isNew = $this->isNew(); // set this here, because it will change before you want to test for it...
         // ... any other pre-processing that needs to be done...

        parent::doSave($con);

        if($isNew)
        {
            $group = Doctrine::getTable('sfGuardGroup')->findOneByName('customer');
            $this->getObject()->link('groups',$group->getId());
        }
    }
    //...
}

Quite easy when you know how and I guess this would be the way to auto-select other, required relationships too...

Monday 18 January 2010

Forms & Forms

Two little snippets this morning (been busy already).

Firstly, form widget errors and I18n...

Brilliant stuff - if you add the error string you've defined in your form class into your I18n catalogue, it'll automatically translate. No further work needed!

Secondly - and this a little pointer :) I had updated a choice widget to set a default value. However, I noticed that even though the HTML was showing it as selected, Firefox was still showing the previous 'default' - the first option in my alphabetical list.

Clearing cookies for the site (presumably so a new session was generated) resolved the issue and displayed the expected default item.

Friday 15 January 2010

Find a user's IP address within an action

Simple one but took some time to find the answer so I'm noting it here.

You could of course use standard PHP but that defeats the point of using a framework in my opinion...

But the quick answer is
$request->getHttpHeader('addr', 'remote'));

Easy :)

Custom Validators and messages

Only a quick one now but there's a couple of points here that are worth noting. Especially as I've just been going round in circles for 30 minutes trying to get my custom message to display when a Doctrine form field was empty.

To be fair, I'm not confident it was due to it being a Doctrine field but anyway...

so...

1. If you use the 'addMessage()' method to add a custom method for an error code that is part of the default messages array within the validator base it WON'T get overwritten!!! If a default message exists it will always take precedence over the new version....

2. It's also worth remembering that the sfValidatorError (that your validator should extend from) class has it's clean() method called and this, in turn, calls the doClean() method. During the clean() method however, the isEmpty() method is called if the 'required' option is set to true. If the isEmpty() method returns true then your doClean() method won't ever be called... The way to get around this is to do one of the following:
a) set the 'required' option to false... not ideal if your value is required.
b) override the isEmpty() method to always return false and allow your doClean code to be called.

Done & Done

Wednesday 13 January 2010

sfValidatorI18nChoiceLanguage "The following languages do not exist"

Another interesting issue with the symfony I18n system.

Using the language form from sfFormExtra plugin automatically uses the sfValidatorI18nChoiceLanguage object. We've set up our microsite to use the full format cultures made up of a 2 character language code, followed by an underscore and a 2 character code: e.g. en_GB for British english or fr_FR for French. This offers consistency with our other sites that also use this form of localisation selection.

However, while trying to use nl_NL, de_DE & fr_FR, we are told via a 500 server error that those languages don't exist.

Dumping out the languages obtained from sfCultureInfo::getInstance()->getLanguages(); shows that indeed, those 3 language selections don't exist. Only, nl, de, and fr do.

I don't know, as yet, why this is so - and i haven't found a consistent and 'correct' method to update my other languages but I shall update this post when I do find one...

sfValidatorI18nChoiceLanguage does not support the following options: 'culture'.

This is the first of two posts about the sfFormExtra plugin after finding a bug and an interesting 'inconsistency' yesterday.

We had need to use the language form bundled with the FormExtra plugin yesterday to build a microsite that needs to handle 4 languages. However, trying to implement the form resulted in the following error:

sfValidatorI18nChoiceLanguage does not support the following options: 'culture'.


The bug is logged in the Symfony ticket system but is not fixed yet. There is a patch at the link over there, but to be honest, it's just as easy to manually correct it until a new release fixes it.

Basically, change a line in the sfFormLanguage::configure() method from:
$this->setValidators(array(
       'language' => new sfValidatorI18nChoiceLanguage(array('culture' => $this->user->getCulture(), 'languages' => $this->options['languages'])),
));
to
$this->setValidators(array(
       'language' => new sfValidatorI18nChoiceLanguage(array('languages' => $this->options['languages'])),
));

Problem solved :)

Thursday 7 January 2010

sfDoctrineGuardPlugin login with email bug

There is a bug in 4.0.1, but it's been patched here

NB: you may need to subscribe to the symfony project site in order to see the ticket - but if you haven't already, why are you reading about symfony bugs! LOL

How to display source code with line numbers and formatting in blogger

While setting up this blog, I realised I wanted some nicely formatted source code. As I'm blogging about coding, even if it is for my own use mainly, I wanted a solution that made the source code easy to read.

I settled on (after not too long a search) these two ideas which I have combined into one....


OK, so the css solution works like a charm, no need to go into detail here just pop over to the link above for details. Kudos to The Blogger Guide.

However, I had to tweak the line numbers solution. Again, most of the description at that site is valid, but I changed the way the new content is generated, not least using lists rather than tables. This should still  allow for content to be copied and pasted ok too.

Enjoy :)


Anyway, I'm not going through everything here as it's all been covered in the two links above. but I have posted the javascript and css I'm using on this site as a starting point:

CSS
pre {     border:1px dashed #aaaaaa;     background:#eeeeee;     white-space:pre;     font-family:monospace;     font-size:9pt;     line-height: 10pt;     height:auto;     width:auto;     padding:5px;     overflow:auto; } ol.pre {     margin: 0 0 0 15px;      padding: 0px;      padding-left: 5px; } .pre li {     margin: 0;     padding: 0;      background: none !important; }
and the JavaScript
function showLineNumbers() {
    /****************************************
    * Written by Andreas Papadopoulos       *
    * http://akomaenablog.blogspot.com      *
    * akoma1blog@yahoo.com                  *
    *                                       *
    * And heavily refactored by Matt Keeble *
    * http://codinginharmony.blogspot.com   *
    ****************************************/
    
    var isIE = navigator.appName.indexOf('Microsoft') != -1;
    var preElems = document.getElementsByTagName('pre');
    if (preElems.length == 0) return;

    for (var i = 0; i < preElems.length; i++) {
        var pre = preElems[i];
        var oldContent = pre.innerHTML;
        oldContent = oldContent.replace(/ /g," ");
        oldContent = oldContent.replace(/\n/g, "<br />");
        var strs = oldContent.split("<br />");
        
        if (isIE) {
            strs = oldContent.split("<BR />");
        }
                
        if(oldContent.substring(0,4) == '<br />')
        {
            oldContent = oldContent.substring(4);
        }
        
        newContent = '<ol class="pre">';
        if(strs.length == 1)
        {
            newContent+= "<li>"+strs+"</li>";
        } 
        else
        {        
            for(var j=0; j < strs.length; j++) {
                if(strs[j] !== '')    newContent += "<li>"+strs[j]+"</li>"
            }
        }
        newContent+= "</ol>";
        pre.innerHTML = newContent;
    }
}

sfDoctrineGuardPlugin and custom algorithms

OK - so I have hunted around for a solution to this problem twice now and struggled to find it. So as my memory's crap, I've decided to start this blog in order to capture the solutions, hints & tips I find. (yeah, this is the first post...)

So the problem is, that when using the symfony doctrine:data-load to import new data into my app, the sf_guard_user table was not being updated with the correct algorithm. Whatever happened, the default 'sha1' algorithm was being used.

I finally realised, that my app specific settings for the alogrithm_callable option were not being called during the data-load process, even though they were within the app - this obviously caused problems when trying to log in users constructed during the build/load process from the command line.

There are two solutions: 

The hard way
The reason it's the hard way is because it relies on my memory... the solution is to add the application option to the data-load task. So instead of just calling symfony doctrine:data-load actually call
symfony doctrine:data-load --application=frontend
(replacing frontend with the correct application name of course!)

The easy way
This solution is more robust because I don't have to remember to do anything different from normal.... basically, create app.yml in the project's config directory and add the option there. This way, you don't need to remember any additional options when running data-load and if you do want a different alogrithm to be used for your backend application for example (to ensure your customers can't login to the admin section without worrying about permissions) you can override the option on an app-by-app basis.
    So all is good - should solve the problem! I'm off to refactor my code :)

    [UPDATE:] make sure you run
    ./symfony cc
    to clear the cache before you try and reload the data - otherwise it won't work