Wednesday, November 11, 2009

Short remarks about " 30 symfony best practices "

Nicolas Perriault recently published some very interesting slides about symfony best practices.
I agree with 80% of the stuff pointed there but I've some doubts about the other 20%.

1. He says that using Doctrine_Query->execute() to obtain Doctrine_Records from the database is BAD because you should use fetchArray() method instead. This because you're supposed to have read-only data in your view.

I think it's often useful to fetch objects instead of arrays. The first reason is the possibility to delegate echoing to the __toString() method. If you have a Person Model and you already wrote a __toString() method to concatenate firstname and fullname for example, with arrays you're obliged to hardcode it every time ($person['name'].' '.$person['fullname']) in views. If one day you want to change it you have to go through all the occurrences. You can have a Person Helper to do something like "echo person_name($person)" but i find this an uglier way to promote "code reuse". When I want to be consistent, I'm used to delegate to __toString() and echo $objects.

Second reason: sometimes you have small test function in your models like $painter->hasColor('blue'). In my opinion you can safely use these small checks in your views without violating the MVC logic. Even designers will find those ones useful and clear.

Third reason: the Jobeet Tutorial uses objects everywhere and does not use fetchArray().

2. About "avoid using cache".

Hey, this sounds a little bit too drastic. Symfony runs with a huge overhead and it's probably
one of the slowest php framework around. If you want to obtain decent page loads in production, then using the cache is a must. If you think it's difficult to deal with it, well, you must do it anyway.

3. "There should never be any direct use of sfContext inside the Model layer. eg. sfContext::getInstance()"

Ok, that's true. But sometimes you cannot avoid it, really. For example, in the admin generator,
I doubt there's a way to filter results in table_method/peer_method based on user credentials
without calling sfContext (and table_method must belong to the Model).

More generally, you cannot "inject" objects in the admin generator Models
(read, auto_* classes). If you want to do that, you have to write your own.

Friday, August 28, 2009

symfony admin generator upload reloaded

the good old admin_input_file_tag



When you hear that symfony "admin generator system has improved", it usually means
that they got rid of some useful feature (ie. upload/form helpers in generator.yml, validation in yaml etc :)

To "emulate" the deprecated admin_input_file_tag behaviour without fall back to the sfCompat10Plugin (whose features will be deprecated in the next releases of the framework) I did something like this in lib/form/doctrine/BaseFormDoctrine.class.php:



abstract class BaseFormDoctrine extends sfFormDoctrine
{
protected function _getWebUploadDirPath($fieldName)
{
return '/'. basename(sfConfig::get('sf_upload_dir')) .'/files/'.
$this->getModelName() .'/'. $fieldName .'/';
}

protected function _getUploadDirPath($fieldName)
{
return sfConfig::get('sf_upload_dir') .'/files/'. $this->getModelName() .'/'. $fieldName .'/';
}

/**
* Get generator.yml configuration
*/
protected function _getFieldsForm() {
static $generatorYml = null;
if ($generatorYml == null) {
try {
$configClassName = sfContext::getInstance()->getModuleName() . 'GeneratorConfiguration';
$configuration = new $configClassName();
$generatorYml = $configuration->getFieldsForm();
}
catch (sfException $e) {
return array();
}
}
return $generatorYml;
}

public function setup()
{
parent::setup();

foreach ($this->_getFieldsForm() as $fieldName => $config) {
if (isset($config['ntcType']) and $config['ntcType'] == 'image_upload') {
$getterFunction = 'get'. sfInflector::camelize($fieldName);
$this->validatorSchema[$fieldName] = new sfValidatorFile(array(
'required' => FALSE,
'path' => $this->_getUploadDirPath($fieldName),
'mime_types' => 'web_images',
));
$this->validatorSchema[$fieldName .'_delete'] = new sfValidatorBoolean();
$this->widgetSchema[$fieldName] = new sfWidgetFormInputFileEditable(array(
'label' => sfInflector::humanize($fieldName),
'is_image' => TRUE,
'with_delete' => TRUE,
'delete_label' => 'remove current file',
'edit_mode' => !$this->isNew(),
'template' => '%input% %delete% %delete_label% %file%',
'file_src' => $this->_getWebUploadDirPath($fieldName) .
pathinfo($this->getObject()->$getterFunction(), PATHINFO_FILENAME) .'_thumb.jpg',
));
}
}
}

protected function processUploadedFile($field, $filename = null, $values = null)
{
$generatorYml = $this->_getFieldsForm();
$fileName = parent::processUploadedFile($field, $filename, $values);
/*
* create a thumbnail to show in the form
*/
if (isset($generatorYml[$field]['ntcType']) and
$generatorYml[$field]['ntcType'] == 'image_upload')
{
$dirPath = $this->_getUploadDirPath($field);
if (!empty($fileName)) {
$thumbnail = new sfThumbnail(150, 150);
$thumbnail->loadFile($dirPath . $fileName);
$thumbName = pathinfo($fileName, PATHINFO_FILENAME) . '_thumb.jpg';
$thumbnail->save($dirPath . $thumbName, 'image/jpeg');
}
}
return $fileName;
}
}



Then put in generator.yml of your model:


...

form:
fields:
my_image_field: {ntcType: image_upload}



You have to install sfThumbnailPlugin to make this work (or you just can drop the thumbnail generation code)

This will activate a file upload widget for every field configured with type "image_upload".

Thanks to searbe post for the inspiration.

Tuesday, August 18, 2009

cakephp vs symfony vs django admin site: why i chose to not choose the "best"

Recently I had to choose a framework to implement a web data entry application (dozens of forms and database tables). I looked for a rapid application development environment that does code generation for basic administration tasks (CRUD trough web forms) so I first discarded all the frameworks that does not have such a functionality: codeigniter, kohana, zend framework.

I neither want to use Ruby on Rails because i know PHP pretty well and, having a moderate understanding of python too, I didn't want to learn another language.

My research ended up comparing the code generation features of these three frameworks:

  • django

  • cakephp

  • symfony



I've used cakephp in the past for a couple of small projects, and, imho, it's a nice framework with some limits:


  • cakephp generates code for models, views, controller and templates. merely this code is customizable only modifying the already generated sources, so if you change something in your database you cannot just re-generate the admin interface because this will overwrite all of your customization

  • i don't like cakephp "convention over configuration" philosophy. expecially i don't like the fact that I must fight against the pluralization of model names which are by default inflected in english (that's not my language as you can guess)

  • no filters at all (ie. date range) are produced by the CRUD generator

  • development is very tight to the construction of models. it's not easy to implement something outside the mvc pattern (which is good for somebody but not in my case)



Once stunned by django admin generator I considered to use this amazing framework which has some very good points over symfony (the last framework I tried):


  • django it's very fast

  • the killer feature: django does not have code generation, it simply creates its admin site "on the fly" introspecting the models and the Admin classes. everything is magically reflected on the admin site without manually invoking some nasty cli command every time you change something.

  • django admin site has a nice clean design with calendar javascript widgets for dates and times

  • symfony+doctrine admin generator ships with ugly selects for date widgets and an ugly default admin theme

  • django admin site has a dashboard (a page with a list of all your models and a link to the common crud operations) with a list of the latest actions

  • django has a configurable automatic formsets generation to edit multiple entry of related data (ie. you have a book form and several categories text fields in the same page).

  • symfony comes with a similar functionality (called embedded forms) but they are harder to manage and there's no support at all in the admin generator

  • django admin site CRUD keeps revisions and records actions automatically (with symfony you can use a doctrine/propel behaviour for version history but I never tested it)

  • django syntax for ORM and database querying is just perfect

  • symfony default ORM (propel) has an insane syntax to cope with (Doctrine ORM is simple anyway and fun to code with)

  • django directory structure could not be simpler: you have a directory per module and everything is contained there (usually you have just three files for each module: admin.py, models.py, views.py) apart from templates

  • symfony module generation acts like a bomb dropped on your filesystem. it spreads files everywhere (filters, forms, modules, models, templates, actions, base classes, a dozen of yaml configuration files) and it's not easy, expecially for beginners, to understand what does what and where it is located (the worst thing is that the cli truncates the paths in the console output when the creation process is running, so you have to figure out yourself where everything's gone)

  • i was diffident about symfony YAML and configuration files



Finally, I explain why I ended up using symfony and not django for my project.


  • I'm more skilled with php than with python.

  • A huge limit of the admin site in django is that it does not have a "view" permission on models. You can assign "change", "delete", "create" rights on objects but the django developers think that admin users that can "view" something then automatically have the right to modify it.
    I find this absurd (I also wrote a trac ticket about this).

  • With django I haven't found an easy way to have a "dynamic" owner-like permission (something like row level access with symfony). There are a couple of plugins for this, but I feel they don't really adapt to the scope.

  • Let me say django admin filters are bad for your health. I wonder why an admin filter must be constrained to a multiple choice. What if I want to search with a __custom__ date range or a custom string ? Symfony has these two gadgets in the generator.

  • Once you get used to YAML, symfony generator.yml becomes your closest friend. I like the level of customization that you can reach editing that single file.

  • Symfony cli provides some extras out of the box: db migration (django has an external plugin called evolution), remote deplyment sync with rsync, etc.



My final opinion:

  • cakephp is a good, fast, relative small framework (well i know code igniter is faster and smaller but it does not have code generation). It's fun to work with it and easy to learn. The documentation has reached a good level.

  • django probably will be the best framework in the world, I'll use it when I've mastered python

  • symfony is the most complete php RAD out there that ships with code generation (if you relax this constraint I think that Zend Framework comes first) but it has a steep learning curve

Monday, August 17, 2009

Symfony: jquery calendar widget for doctrine generated filters

Javascript calendar widget for filters



This is a quick way to have a javascript calendar instead of the nasty
3 selects (y/m/d) for every date filter:

1. install sfFormExtraPlugin

2. generate admin backend as usual

3. put the following code in lib/filter/doctrine/BaseFormFilterDoctrine.class.php:


abstract class BaseFormFilterDoctrine extends sfFormFilterDoctrine
{
public function setup()
{
foreach ($this->widgetSchema->getFields() as $name => $widget) {
if ($widget instanceof sfWidgetFormFilterDate) {
$this->setWidget($name, new sfWidgetFormJQueryDate(array(
'image'=>'/images/calendar.gif',
'format' => '%day%/%month%/%year%'
)));
}
}
}
}

Sunday, June 21, 2009

Sending tokenized SMS with drupal to CiviCRM contacts and/or Views of... numbers

Drupal SMSframework to the rescue



For a I project I'm working on I had the need to organize huge lists of mobile phone numbers to send SMS to. Actually there's a nice drupal module, smsframework that lets you plug in your favourite SMS gateway to accomplish such a task. Merely there are still some limitations with smsframework:

  1. there is an interface to send SMS to drupal users (sms blast module), but you cannot apply any filter on it (you just can send SMS to all the users at once)

  2. it lacks an interface to send SMS to contact lists (ie. simple lists of numbers created with cck and views for example)

  3. there's no integration with CiviCRM so you cannot send SMS to civicrm contacts

  4. you cannot schedule jobs for sending SMS

  5. it lacks integration with token module


Send tokenized SMS to contact lists and civicrm users


To overcome this shortages I've written a small module that lets you send tokenized SMS to contact lists. It depends on smsframework module, views and views bulk operations for the GUI. Optionally you can install the token module.

Steps follow:

  1. install the modules then create a View with a field that contains the phone numbers (ie. a field of the user profile or a cck field)

  2. add some fields to the view that eventually you can use as tokens in the SMS text (ie. the name of the user)

  3. set the display mode to "views bulk operation" and select the action "Send SMS to..."

  4. optionally add some exposed filters to the view

  5. visit the view page, select the desidered rows, type the SMS text and send it


(it's easier to do than to explain :)

As you can create views of civicrm users with their mobile phone numbers, you can also use this with civicrm contacts.

I've applied for a cvs account in drupal contrib repository but the mantainers didn't answer me so I publish this here who is interested:

download smsbulk module

Please read the included README.txt first.

p.s. I've written a note to the mantainer of smsframework module asking for integration and I'm wating for a feedback.

Saturday, June 20, 2009

drupal SEO primer for the lazy

The state of the art of drupal SEO



I've read several articles about drupal SEO practices. The best one i met is
basic drupal seo on site optimization which is not really "basic" as it covers all the crucial aspects of drupal SEO. I add here some notes to that post:

1. the suggestion to "NOT install the Drupal Sitemap Module" sounds actually obsolete as the xmlsitemap project is now mature and ready for drupal 6.

2. the author is using rewrite rules to deal with canonical urls. at the time of writing, search engines didn't yet support the attribute rel="canonical". the meta tags drupal module now addresses that.

3. he doesn't list the brand new module seo checklist

Drupal SEO quickstart



If you don't have time to read SEO best practices or simply don't want to install a bunch of modules to cope with it, there is a simple shortcut I sometimes have used to achieve some simple improvements. Put this code in your theme template.php:


function __truncate($s) {
return htmlentities(drupal_html_to_text(truncate_utf8($s, 256, TRUE, TRUE)));
}

function phptemplate_preprocess_page(&$vars) {
$vars['meta'] = '';

if ($vars['is_front'] && $vars['mission'] != '') {
$description = $vars['mission'];
}
else if (!Empty($vars['node']->teaser)) {
$description = $vars['node']->teaser);
}
else if (!Empty($vars['node']->body)) {
$description = $vars['node']->body;
}
elseif (arg(0) == 'taxonomy' and arg(1) == 'term' and is_numeric(arg(2))) {
$description = db_result(db_query("SELECT description FROM {term_data} WHERE tid = %d", arg(1));
}

if (!Empty($description)) {
$vars['meta'] .= '<meta name="description" content="'. __truncate($description) .'" />'."\n";
}

if (isset($vars['node']->taxonomy)) {
$keywords = array();
foreach ($vars['node']->taxonomy as $term) {
$keywords[] = $term->name;
}
$vars['meta'] .= '<meta name="keywords" content="'. implode(',', $keywords) .'" />'."\n";
}

if (isset($_GET['page']) || isset($_GET['sort'])) {
$vars['meta'] .= '<meta name="robots" content="noindex, follow" />'. "\n";
}

if ($vars['node']->title) {
drupal_set_title($node->title .' | '. $vars['site_name']);
}
}


This is mostly inspired by the blueprint drupal theme template.php with a couple of twists. What it does is self explanatory

1. set up page title with node title if any
2. set up meta description with the node description or term description if any
3. if there are related tags then use those ones as meta keywords
4. add "noindex" if the page comes from a pager link to avoid indexing of duplicate content

After that just put



<?php print $meta; ?>



in the head section of your page.tpl.php.

--

Wednesday, June 3, 2009

ajax pagination and slideshow effects in drupal 6

The state of the art of drupal slideshow modules



As you can read in the well made comparison of slideshow modules in drupal there are at least ten modules that practically aim to fulfil nearly the same task: implement a slideshow of content with ajax pagination and easing transition effects.

My personal fav is views slideshow, which is simple and does the right things. Dynamic display block seems also promising but i don't like the fact that you must add templates (tpl) and styles in your theme folder to make it work. I prefer views slidehow because it works in a moment just out of the box.

Ajax calling



When i made my last project there was no support for ajax pagination in any of the summentioned modules. This trend is changing rapidly as ajax loading of content with the ability to add easing effects is being requested more and more.

In the best case this will go straight into the views module in a near future.

The quick workaround



As i couldn't wait, I had to use I small fix for my project. What I needed was a simple ajax loading of content with a fadeIn/fadeOut effect applied every time the user clicks on next/previous link in some blocks of content.

First i reviewed all the modules above, then i decided to use none: the views module in drupal 6 has an excellent ajax pager, the only thing it lacks are easing effects applied to the slide transition. I thought that a small workaround around this could be more convenient than wait.

This is what I reached:


if (Drupal && Drupal.jsEnabled && Drupal.Views) {

Drupal.Views.Ajax.ajaxViewResponse = function(target, response) {
if (response.debug) {
alert(response.debug);
}
var $view = $(target);

if (response.status && response.display) {
var $newView = $(response.display);
$view.fadeOut('fast', function() {
$view.replaceWith($newView);
$view = $newView;
$view.fadeIn('fast', function() { Drupal.attachBehaviors($view.parent()) });
});
}
if (response.messages) {
$view.find('.views-messages').remove().end().prepend(response.messages);
}
};



With this code placed in my custom theme javascript I've been able to simply overwrite
the views module handling of the ajax pager transition and to use the fadeIn/fadeOut effects.

You can put whatever effect you like in the code, in particular this is useful if you plan to use the jquery easing plugin for the dynamic effects.

Hint: if you, like me, have several blocks with different pagers in a single page, you must set a unique pager id for each one to make them work together. This is easily accomplished via panels module or using the pager properties when editing the views.


my 3¢

Monday, June 1, 2009

drupal nodereference 2.0 - tagging with reviews

Cascade delete referencing nodes



One of the most frequently requested feature about drupal nodereference is "on delete cascade" of referred nodes when you remove the referenced one. This is a pretty easy task to accomplish if you are using the nodereferrer module:


function mymodule_nodeapi(&$node, $op, $params = NULL, $page = NULL) {
switch ($op) {
case 'delete':
if ($node->type == 'my_content_type_name') {
$referrers = array_keys(nodereferrer_referrers($node->nid));
foreach ($referrers as $referrer) {
node_delete($referrer);
}
}
break;
}
}


you can obtain the same effect without nodereferrer module: you have to query the database to obtain the referencing nodes nids like this:


function mymodule_nodeapi(&$node, $op, $params = NULL, $page = NULL) {
switch ($op) {
case 'delete':
if ($node->type == 'my_referenced_content_type_name') {
$result = db_query("SELECT nid FROM {content_type_referrer_name} r WHERE
r.field_content_reference_nid = %d", $node->nid);
while ($referrer = db_fetch_object($result)) {
node_delete($referrer->nid);
}
}
break;
}
}


Ok i guess you already knew those ones :]

Notifications when adding referencing nodes (ie. reviews)



Now the pulp: another task I had to implement for a website was "notify users about new nodes added referencing a content type". In other words: notify users when a review is written about an article. "Review" is a content type referencing another content type "article". I use the notifications module and afaik there's nothing implemented there that take care of this issue.

I added this snippet in my custom module:


function _oninsert_notification(&$review, $op) {
if(module_exists('notifications')) {
$article = node_load($review->field_article_ref[0]['nid']);
$event = array(
'module' => 'node',
'uid' => $article->uid,
'oid' => $article->nid,
'type' => 'node',
'action' => $op,
'node' => $article,
'params' => array('nid' => $article->nid),
);
notifications_event($event);
}
}


and hooked this one into the nodeapi (review) insert operation.

This adds an event in the queue and will send an email to users that have subscribed an article once a review is written about it.

Tagging with referencing nodes



Last but not least: tagging. What if you have to tag an article writing a review ? You have to add the tags to the referenced node (the article) but they normally are assigned to the review only. To get this behaviour I added this code to my module:


function mymodule_nodeapi(&$node, $op, $params = NULL, $page = NULL) {
switch ($op) {
case 'insert':
if (isset($node->field_article_ref[0]['nid'])) {
$article = node_load($node->field_article_ref[0]['nid']);
$tags = array_keys(taxonomy_node_get_terms($node));
foreach ($tags as $tag_tid) {
db_query('INSERT IGNORE INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)',
$article->nid, $article->vid, $tag_tid);
}
break;
}
}


This works if you have a tag vocabulary assigned to both content types (this will save at least some joins :)

So now you have got tags assigned to both reviews and articles. Does it worth ? Sure !
You can now count how many times an article is tagged with a specified tag and create suggested tags and tag clouds:


/*
* nid is the article node nid
*/
function _most_used_tags($nid = NULL) {
$terms = array();
if (is_numeric($nid)) {
$referrers = array_keys(nodereferrer_referrers($nid));
if ($referrers) {
$result = db_query_range("SELECT t.*, COUNT(t.name) AS rweight FROM {term_data} t
INNER JOIN {term_node} n ON t.tid = n.tid WHERE t.vid = %d
AND n.vid IN (". db_placeholders($referrers) .")
GROUP BY t.tid ORDER BY rweight DESC",
array_merge(array(VID_TAGS) /* vocabulary vid */, $referrers),
0 /* starting offset */, 10 /* number of tags returned */);
while ($term = db_fetch_object($result)) {
$terms[$term->tid] = $term;
}
}
}
return $terms;
}


this will return the most used tags about the article. Those tags are added when inserting reviews.

Can the community tags module do this ? Absolutely it can't. It does not let you associate tags inline with content editing/writing. This is the reason i didn't use it in my project.

my 3¢