• Shortcuts : 'n' next unread feed - 'p' previous unread feed • Styles : 1 2

» Publishers, Monetize your RSS feeds with FeedShow:  More infos  (Show/Hide Ads)


Date: Wednesday, 05 May 2010 16:08

Ahem, pour ceux qui ne l’auraient pas compris, les billets techniques seront désormais publiés sur le blog de ma société, Akei.

Tenez c’est drôle, je viens justement d’en publier un sur l’installation de Django dans un environnement virtuel Python avec pip, virtualenv et virtualenvwrapper, c’est fou ça.

Pour faire valoir ce que de droit, veuillez madame, monsieur…


Ce billet intitulé Juste pour rappel a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, akei, dev, django, pip, python, vir..."
Send by mail Print  Save  Delicious 
Date: Tuesday, 20 Apr 2010 19:12

Ceux qui me suivent sur twitter en ont eu la primeur, mais j’officialise sur ce blog : Akei, ma société, est née.

Akei

Akei met en avant une offre de services axée sur le conseil, la formation et le développement Web, autour de technologies variées comme PHP, Symfony, Python, Django ; mais plus que tout sur la qualité.

Je ne m’étalerai pas sur les longueurs et turpitudes administratives proprement kafkaïennes qu’il m’a fallu affronter pour accoucher du bébé (et qui durent encore, soit dit en passant), mais je veux juste profiter de l’instant pour vous inciter à regarder ce site, à en lire les contenus, et à prendre contact si vous vous reconnaissez dans la vision présentée, pour éventuellement - pourquoi pas ? - travailler ensemble sur vos projets Web.


Ce billet intitulé Faire part de naissance a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Divers, akei, conseil, dev, django, form..."
Send by mail Print  Save  Delicious 
Date: Thursday, 18 Feb 2010 07:52

D’aucuns ne le sachant peut-être pas, je suis cette année co-responsable - avec Thomas Parisot - de l’organisation des sessions Internet aux Rencontres Mondiales du Logiciel Libre qui se tiendront à Bordeaux du 6 au 11 juillet 2010.

Si vous travaillez sur (ou avec) un projet libre ayant un rapport avec Internet (Web, XMPP, IRC, email, etc.), n’hésitez pas à nous proposer un sujet de conférence technique en rapport. Trois formats d’intervention sont disponibles : 20, 40 ou 60 minutes (incluant la séance aux questions). Les conférences peuvent se tenir en français, en espagnol ou en anglais.

Si vous souhaitez proposer un sujet de conférence, il vous suffit d’envoyer un simple email d’une dizaine de ligne présentant dans les grandes lignes votre thématique, à l’adresse suivante : internet@listes2010.rmll.info.

Par contre, la date limite de dépôt des candidatures est fixée au 1er mars, autant vous dire qu’il vous faut vous dépêcher si vous souhaitez postuler ;)


Ce billet intitulé RMLL 2010 : appel à conférences a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Libritudes, communauté, conférence, in..."
Send by mail Print  Save  Delicious 
Date: Wednesday, 17 Feb 2010 08:04

While sadly not being at Symfony Live 2010 with pals, I’m drowning my sorrow into tech tutorials writing.

Sometimes you want to design forms and contextualize them regarding the current user session. For example:

  • Manage the display of some fields regarding the user authentication status and credentials;
  • Filter some queries used to get the values available in a <select/> tag;
  • Allow multiple steps form validation with step state persistence (wizzard);
  • etc.

So how can we achieve this without using an evilish sfContext::getInstance()->getUser() call in the configure() method of the form?

Symfony 1.2 1.3 and 1.4 introduced the generation of a project-wide BaseForm class we can use here to setup some convenient methods to allow user setters and getters:

 php
<?php
class BaseForm extends sfFormSymfony
{
  static protected $user = null;

  static public function getUser()
  { 
    return self::$user;
  }

  static public function getValidUser()
  {
    if (!self::$user instanceof sfBasicSecurityUser)
    {
      throw new RuntimeException('No valid user instance available');
    }
    
    return self::$user;
  }

  static public function setUser(sfBasicSecurityUser $user)
  {
    self::$user = $user;
  }
}

Okay, we now can set a user instance as a static property of all available forms within the project, nice. Wait, how the hell will we set the instance, and when?

Symfony dispatch a very useful context.load_factories event when all the factories - including the user one - are instanciated and available in the context. So we can listen to this event and set the user instance to the forms when it’s ready. We’ll do this in the ProjectConfiguration.class.php:

 php
<?php
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // ...
    
    $this->dispatcher->connect('context.load_factories', array($this, 'listenToLoadFactoriesEvent'));
  }

  public function listenToLoadFactoriesEvent(sfEvent $event)
  {
    BaseForm::setUser($event->getSubject()->getUser());
  }
}

So now we’ve set a static user instance in every form at project configuration time, we can use it in any form.

For example, imagine a form where only authenticated users having an admin credential can see, fill and submit an hypothetic is_published field:

 php
class myForm extends BaseForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = self::getValidUser();
    
    $this->setWidgets(array(
      'title' => new sfWidgetFormInputText(),
      'body' => new sfWidgetFormTextarea(),
    ));
    
    $this->setValidators(array(
      'title' => new sfValidatorString(array('min_length' => 5)),
      'body' => new sfValidatorString(array('min_length' => 20)),
    ));
    
    if ($user->isAuthenticated() && $user->hasCredential('admin'))
    {
      $this->widgetSchema['is_published'] = new sfWidgetFormInputCheckbox();
      $this->validatorSchema['is_published'] = new sfValidatorBoolean(array(
        'required' => false,
      ));
    }
    
    // ...
  }
}

Here a basic user won’t see and won’t be able to submit any value for the is_published field. Note that all those changes won’t ever affect the controllers or the model, and the forms will still be easily testable because the forms and the user instance are not tightly coupled.

As a side note, you can also use the form options to eventually pass a user instance and fallback on the static instance in case it’s not available:

 php
class myForm extends BaseForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = $this->getOption('user', self::getValidUser());
    
    // ...
  }
}

In a controller, you would use it this way:

 php
<?php 
class fooActions extends sfActions
{
  public function executeBar(sfWebRequest $request)
  {
    $this->form = new myForm(array('user' => $this->getUser()));
    
    // ...
  }
}

A typical unit test suite of the form would be:

 php
$t = new lime_test(2, new lime_output_color());

$user = new sfBasicSecurityUser(new sfEventDispatcher(), new sfSessionTestStorage(array(
  'session_path' => sys_get_temp_dir(),
)));

$form = new myForm(array('user' => $user));
$t->is(count($form->getWidgetSchema(), 2, '->configure() displays 2 fields when user is not authenticated'));
$user->setAuthenticated(true);
$user->addCredential('admin');
$t->is(count($form->getWidgetSchema(), 3, '->configure() displays 3 fields when user is authenticated and is an admin'));
// ... I'll let you complete the suite by yourself

Now imagine we want to filter the choices offered by a one to many relationship field of a Doctrine form regarding user credentials:

 php
class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = $this->getOption('user', self::getValidUser());
    
    // ...
    
    // Category choices
    $categoryQuery = $this->getUserCategoryQuery($user);
    $this->widgetSchema['category_id'] = new sfWidgetFormDoctrineChoice(array(
      'model' => 'Category',
      'query' => $categoryQuery,
    ));
    $this->validatorSchema['community_id'] = new sfValidatorDoctrineChoice(array(
      'model' => 'Category',
      'query' => $categoryQuery,
    ));
  }
  
  /**
   * Please note that this method would naturally better fit in the model, in the 
   * ArticleTable class; We put this here for conciseness
   */
  protected function getUserCategoryQuery(sfBasicSecurityUser $user)
  {
    $query = Doctrine:getTable('Category')->createQuery('c');
    
    // If user is not an admin, only allow choice of published categories
    if (!$user->hasCredential('admin'))
    {
      $query->where('c.is_published = 1');
    }
    
    return $query;
  }
}

There are many more cool things you can achieve by offering user session access to a form, while keeping all the stuff easy to test.

As usual, if you have a better way, feel free to tell us about it in the comments.


Ce billet intitulé User Dependent Forms with Symfony a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, best practices, credentials, forms,..."
Send by mail Print  Save  Delicious 
Date: Monday, 25 Jan 2010 08:39

Stop

In some hours, maybe days, my flickr account will be destroyed, and its content will be gone, definitely.

Looks like Flickr doesn’t allow "sales links" within photo description:

Flickr: no sales links allowed

That doesn’t fit my way of thinking my expensive hobby. I know, by putting links to redbubble I’m breaking the terms of service agreement, so I’m the culprit, and Flickr is safe.

Bye Flickr, after 5 years of paying my "pro" account fees, I’m a bit sad to leave the community, but I can’t stand paying for a service which doesn’t allow me to manage my stuff the way I want. Especially when some Interesting photos of the day, highlighted by Flickr itself in its explore RSS feed, are using the same kind of links, but are still there, safe.

Double standards? Not for me, thanks. Bye.

PS: When I’ll have a new dedicated self-hosted website, I’ll keep you informed here. In the meanwhile, feel free to reach me at redbubble.

Edit: Done, flickr account is gone. All the data it contained have been retrieved using the awesome photobackup utility, give it a go, even for backuping your flickr stream: it just works.


Ce billet intitulé Bye, Flickr a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev"
Send by mail Print  Save  Delicious 
Date: Thursday, 07 Jan 2010 13:29

Bruno Bord a eu l’excellente idée de proposer une chaîne littéraire autour du concept de ”polar geek”. J’avais une heure à tuer, en voici donc le modeste résultat.

La main qui secoua énergiquement mon bras me fit sursauter ; plus exactement, le mouvement me tira de la sieste que je tentai de mener à terme tant bien que mal. La silhouette qui me faisait face se dessinait progressivement peu à peu, passant du flou au moins flou, un peu à la manière d’un Canikax d1000sx à lentille digiphotosensible intégrée. Je réactivai donc ma correction bio-optométrique en effectuant deux soulèvements de sourcils consécutifs. J’avais pris l’habitude de la désactiver avant de dormir, car cela pouvait parfois engendrer un conflit avec le pilote de mon régulateur de sommeil paradoxal dont le cerebrofirmware n’avait pas été mis à jour par Applesoft suite à une upgrade malheureuse, partiellement liée au bug de l’an 2048. Par Saint-Jobs, qu’il ne fait pas bon vieillir…

Je distinguais maintenant clairement une jeune femme relativement agitée, qui semblait mouvoir ses lèvres, sans pourtant qu’aucun son n’en sorte. Je mis encore quelques secondes avant de réaliser que je n’étais pas sorti du mode sourdine que j’avais cru bon d’enclencher pour tenter de m’assoupir plus efficacement. Satané matos intégré, qu’il était doux le temps où l’on pouvait s’en passer ! J’en venais presque à regretter ma vieille paire de lunettes et mon casque intra-auriculaire du siècle dernier, finalement… Bref, deux pressions sur mon lobe gauche et j’accédais enfin au discours de mon interlocutrice.

— Inspecteur Knox… INSPECTEUR KNOX ! m’entendez-vous ?
— Je vous entends mademoiselle, je vous entends… Veuillez m’excuser mais j’ai vraiment du mal à me faire à ma nouvelle cyberassistance…
— Inspecteur, désolé de vous importuner de la sorte, mais vous ne répondiez pas à mes holomails, aussi j’ai cru bon de passer directement à votre bureau…
— Je ne relève plus mes holomails. Je ne reçois que de la pub. C’est toujours un peu déroutant de voir, à l’ouverture, débarquer dans la pièce des sexes en érection sautillants, une togolaise fraîchement endeuillée qui tombe en larme, des grassouillets qui fondent en quelques secondes… Même holographiques, ces images me perturbent et m’empêchent de me concentrer. Je crois que je m’y ferai jamais. Tenez l’autre jour, ma femme en a reçu un qui…

Elle ne me laissa pas finir ma phrase.

— Je comprends. Mais le sujet pour lequel je viens vous consulter aujourd’hui est sérieux, et nécessite les compétences d’un fin limier expérimenté tel que vous ; la société pour laquelle je travaille vient de fusionner avec une SSII du Poitou, et nous récupérons leurs anciens applicatifs métiers.
— Jusque là, rien que de très banal.
— Les fichiers récupérés sont dans un langage étrange, qu’aucun de nos ingénieurs ne parvient à déchiffrer.
— Les concepteurs originels devraient pouvoir intervenir et vous éclairer…
— C’est compliqué. Ils ont pour la plupart démissionné, sont tombés en arrêt maladie longue durée ou partis à la retraite. Certains seraient actuellement en poste chez France Telecom, y trouvant les conditions de travail plus sereines. Mais aucun ne répond à nos appels.
— Bon. Mais l’applicatif tourne ?
— Oui. Enfin, non. C’est étrange, ça semble fonctionner, mais parfois les pages sont blanches sans qu’on ait la moindre explication. Forcément, personne n’arrive à en reprendre la source pour en assurer la maintenance et l’évolution. Des fois, seul un message texte abscons est affiché. Dernièrement nous avons eu droit à ce message pour le moins cryptique à la soumission d’un formulaire de contact :

Parse error: syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM in Command line code on line 1

A peine ces mots furent prononcés que de lointaines bribes de souvenirs resurgirent des limbes de mon esprit, en un éclair. J’avais déjà été confronté à ce type de messages, j’en étais certain. Cette sensation douloureuse, ce sentiment d’impuissance, cette rage pour en comprendre la cause, qui souvent restait vaine… Mais impossible de remettre un nom sur ce langage diabolique, qui avait pourtant causé de nombreuses dépressions et départs à la retraite anticipés dans le métier.

— Fournissez-moi un accès submercuritaar que j’examine le code en question.
— C’est que…
— Quoi ?
— Le code en question réside sur une machine virtuelle configurée d’époque, et tourne sur ce qui reste du cloud, en Moldavie. Le code n’est pas versionné, aucun gestionnaire de code source n’y avait été installé à l’époque. Ou du moins, il semble qu’ils utilisaient alors un outil du nom de « FTP » pour gérer les déploiements. Un de nos directeurs projet, qui avait commencé comme intégrateur ColdFusion au siècle dernier, a pu récupérer les sources sous forme d’archive compressée en reverse-engineerant le protocole en question. Mais il est malheurseusement mort quelques heures plus tard, en délirant. Son dernier mot a été "CyberDuck", mais nous n’avons pas compris ce qu’il voulait nous transmettre… Un mot de passe, peut-être ?

D’un coup, ma migraine, cette vieille garce, se réinvitait dans ma boîte crânienne. Ce satané mal de tête qui survenait à coup sûr quand j’étais confronté à mes vieux démons, ceux qui avaient causé mon retrait de la production pour me consacrer à l’investigation technologique dans les années dix. J’activai d’une pression sur mon téton droit la console HUD de mes lentilles de contact, et lançai une instance du programme de régulation de pression intracrânienne que m’avait fourni mon biosysadmin-traitant.

— Je vais voir ce que je peux faire. Transférez-moi l’archive par pneuoveripv6, je tâcherai d’y voir un peu plus clair. Je vous tiens au courant.

- Fin de la première partie -


Ce billet intitulé Polar Beer a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Divers, polar-geek"
Send by mail Print  Save  Delicious 
Date: Friday, 01 Jan 2010 15:18

Edit : C’est tout vendu, désolé pour ceux qui tomberaient sur ce post après la bataille. J’ai finalement acheté un Canon 5D d’occasion nanti d’un zoom grand angle 20-35mmL de la marque, pour moins de la moitié du prix d’un 5D mark II neuf. Je ne suis qu’un amateur, et je me voyais pas finir à faire mariages ou photos de bébé pour payer le matos. Je reste donc libre de shooter ce que je veux, quand je veux, mais en full-frame ;)

***

Une fois n’est pas coutume, je vais profiter de ce blog pour vous souhaiter une bonne année passer une petite annonce[1]. Suite à une trop grosse acquisition, je vends une grosse partie de mon matériel photo actuel :

Le matériel est en excellent état. Maintenant, voila le prix que j’en demande :

  • Pour 690€, je vous cède le 450D, le 18-55mm, le 55-250mm et le Sigma 30mm ;
  • Pour 490€, je vous cède le 450D + 18-55mm + 55-250mm ;
  • Le Sigma 30mm seul est proposé à 290€.

Je n’ai pas très envie de vendre le 55-250mm hors pack, désolé… mais le pack complet est - je pense - proposé ici à un tarif très intéressant, même pour de l’occasion.

Habitant sur Montpellier, je préfèrerai bien évidemment une remise en main propre ; si je dois expédier le barda, les frais d’expédition seront en sus.

Si vous êtes intéressé, contactez-moi :)

Notes

[1] Bon allez, c’est bien parce que c’est vous : bonne année.


Ce billet intitulé Je vends mon Canon 450D + 18-55mm f/3.5-5.6 + 55/250mm f/3.5-5.6 + Sigma 30mm f/1.4 a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Divers, canon, matériel, objectifs, occ..."
Send by mail Print  Save  Delicious 
Date: Wednesday, 16 Dec 2009 07:45

When I do consulting, a lot of people are really surprised that I use Texmate, a popular text editor for OS X, to develop on Symfony projects. Indeed, Textmate is a bit rough around the edge compared to bloated full featured IDE like Eclipse PDT or Netbeans, which are both Java based by the way.

So why using Textmate? No intelligent autocompletion, very basic project management, poor VCS native support, limited index search capabilities… First and while it’s mainly a matter of taste, I mainly use Texmate because it’s fast. Compared to PDT with which you often have to wait a bunch of seconds for the UI to respond on some action you make, mate will react quite instantaneously, and that is making big difference to me. Because when I’m concentrated, focused on some complex problem to solve, I demand my text editor to not make me wait [1].

Oppositely, not having full code introspection and autocompletion makes mate making me think about the code I write, instead of just consuming some API passively. I’ve been using phpeclipse and PDT for some years with Symfony, but I think I really began to understand the framework architecture when I switched to mate as my primary editor. Because every time you need to do something with the Symfony API, you have to open the file and read the code: then you learn a lot. And by the time, you end by knowing the core very well, and it’s incredibly efficient. One more time, this is just matter of taste.

Textmate quick tips

So whereas mate can be somewhat limited at first glance, a second look shows it provides some really effective commands to enhance your productivity. Let’s examine some of them.

Searching for a file within the project codebase

Just by pressing ⌘ + t, a snappy filename search window will pop up and allow you to search a file interactively by its name pattern. If you’re using the incredible Quicksilver app for osx, you got the picture.

interactive-filename-search.png

As Symfony names php files against the class name they contain, finding a class file is just easy as typing the class name in the search field!

Browsing the available functions, classes and methods within a php file

When a php file is opened, the ⌘ + shift + t command will show up the available sections of the document, with a search box you can use to filter their names, still interactively.

interactive-method-name-search.png

Note that this will also work for other file types (like CSS for instance).

Obtening help on native php functions

Move the carret onto some php native function and press ctrl + h and you’ll get the php.net related page in a pop up.

Want the function signature definition in a tooltip, like in PDT or Netbeans? Hit ⌥ + F1 and you’re done.

signature-tooltip.png

Using the mate command line utility

Textmate ships with a native command line interface executable called mate you can use from your term:

$ cd /path/to/project
$ mate .

You can also pipe some command to mate to open the output in it:

$ svn diff|mate

diff-mate.png

Taking part of Mate’s url-scheme capabilities with Symfony

I already blogged about this awesome feature available since Symfony 1.3, one more I won’t ever be able to live without.

Improve your productivity using bundles

The Ack in project bundle

One of the most annoying lack of mate is its internal search engine. It’s really damn slow. The best way to get something decent is to install the Ack in project bundle. You’ll then be able to access a fast and convenient fulltext search engine by pressing the ⌘ + shift + a command.

ack-in-project.png

The ProjectPlus bundle

If you’re looking after a better project browser and some VCS support in mate, you’ll love the ProjectPlus bundle. It will replace the default project drawer by a new one with finder label colors support, file sorting options and VCS icons integration. Give it a try, you won’t be able to live without it.

project-plus-drawer.png

The Symfony bundle

How could I write a blog post on Textmate and Symfony without mentioning the Symfony bundle? Denderello and contributors did a really good job taking over the original tool, which provides very convenient and effective shortcuts to write Symfony snippets of code quickly.

The GetBundle bundle

Last, if you didn’t find anything new or useful reading this blog entry, you might then be interested by installing the GetBundle bundle which will allow you to browse all available bundles and install any of them in one click!

GetBundle.png

Conclusion

So these are some of the reasons why I’m using Textmate when developing with Symfony (and other languages too). What are yours?

Notes

[1] You know the don’t make me think mantra? Well, now you got another one ;)


Ce billet intitulé Symfony Development using Textmate a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, osx, symfony, textmate"
Send by mail Print  Save  Delicious 
Date: Wednesday, 02 Dec 2009 10:43

Yesterday I made some consulting in a company where people asked me if it was possible/hard to setup some kind of search filtering persistence in a doctrine-admin-generated module in Symfony. I told them:

Well, it’s a twenty minutes job.

They logically answered me:

Haha, prove it.

Challenge and stress then started.

The examples below are based on the simple model I took for my previous blog post about embedding relations in Doctrine forms, which I will add fancy bookmarks tagging facilities to:

# in ./config/doctrine/schema.yml
User:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
  relations:
    Bookmarks:
      type: many
      class: Bookmark
      local: id
      foreign: user_id
      onDelete: CASCADE
 
Bookmark:
  actAs:
    I18n:
      fields: [name]
      actAs:
        Sluggable:
          fields: [name]
          uniqueBy: [name, lang]
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
    url:
      type: string(255)
      notnull: true
    user_id:
      type: integer(4)
      notnull: true
  relations:
    User:
      type: one
      local: user_id
      foreign: id
    Tags:
      class: Tag
      refClass: BookmarkTag
      local: bookmark_id
      foreign: tag_id
      foreignAlias: Bookmarks
 
Tag:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
 
BookmarkTag:
  columns:
    bookmark_id:
      type: integer(4)
      primary: true
      notnull: true
    tag_id:
      type: integer(4)
      primary: true
      notnull: true

No need to say you should rebuild your model, right?

Updated fixtures file:

# in ./data/fixtures/fixtures.yml
User:
  niko:
    name: niko
 
Bookmark:
  niko_bookmark1:
    User: niko
    name: Slashdot
    url: http://slashdot.org/
    Tags: [geek_tag, tech_tag, php_tag]
  niko_bookmark2:
    User: niko
    name: Delicious
    url: http://delicious.com/
    Tags: [geek_tag, tech_tag]
  niko_bookmark3:
    User: niko
    name: Digg
    url: http://digg.com/
    Tags: [geek_tag, php_tag]
 
Tag:
  geek_tag:
    name: geek
  php_tag:
    name: php
  tech_tag:
    name: tech

Generate the Bookmark admin

Let’s generate a backend app and a Bookmark admin module:

$ ./symfony generate:app backend
$ ./symfony doctrine:generate-admin backend Bookmark

Now let’s enhance a bit our admin by modifying the generator.yml file:

generator:
  class: sfDoctrineGenerator
  param:
    model_class:           Bookmark
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              Bookmark
    plural:                Bookmarks
    route_prefix:          bookmark
    with_doctrine_route:   true
    actions_base_class:    sfActions
 
    config:
      actions: ~
      fields:  ~
      list:
        display: [=name, url, User]
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

You should be able to browse the generated bookmarks admin interface:

basic.png

Storing filters in a dedicated Doctrine table, and managing them from the controller

We’ll use Doctrine to store saved filters, so let’s define a new Doctrine table definition in our schema.yml file to store stored filters:

SavedFilter:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
    type:
      type: enum
      values: [Bookmark, User]
      notnull: true
    filter:
      type: string()

Of course, still no need to say that you have to rebuild your model, right?

Okay, now we’re going to save the serialized filter values in the filter column, the name one will provide a convenient way to reference a filter set. The type column will reference the Doctrine table familly of the filtered object. Nothing difficult here.

In the admin generator module, the filters are storedin the tableName.filters attribute of the user session (where tableName is the name of the admin module where the filter parameters are operated[1]).

So let’s add a new executeSaveFilter() method in the bookmarkActions controller. And while we’re at it, let’s also add executeLoadFilter() and executeDeleteFilter() methods as well:

<?php
# in apps/backend/modules/bookmark/actions/actions.class.php
class bookmarkActions extends autoBookmarkActions
{
  public function executeDeleteFilter(sfWebRequest $request)
  {
    $this->forward404Unless($filter = Doctrine::getTable('SavedFilter')->findOneByTypeAndId('Bookmark', $request->getParameter('id')), sprintf('Bookmark filter #%d not found', $request->getParameter('id')));
    
    $filter->delete();
    
    $this->getUser()->setFlash('notice', sprintf('Bookmark saved filters "%s" deleted', $filter->getName()));
    
    $this->redirect('bookmark');
  }
 
  public function executeLoadFilter(sfWebRequest $request)
  {
    $this->forward404Unless($filter = Doctrine::getTable('SavedFilter')->findOneByTypeAndId('Bookmark', $request->getParameter('id')));
    $this->setFilters(unserialize($filter->getFilter()));
    
    $this->getUser()->setFlash('notice', sprintf('Bookmark saved filters "%s" loaded', $filter->getName()));
    
    $this->redirect('bookmark');
  }
  
  public function executeSaveFilter(sfWebRequest $request)
  {
    $name = trim($request->getGetParameter('name'));
    $savedFilter = new SavedFilter();
    $savedFilter->fromArray(array(
      'name'   => $name ? $name : 'Untitled filter',
      'type'   => 'Bookmark',
      'filter' => serialize($this->getUser()->getAttribute('bookmark.filters', array(), 'admin_module')),
    ));
    $savedFilter->save();
    
    $this->getUser()->setFlash('notice', 'Bookmark filters saved');
    
    $this->redirect('bookmark');
  }
}

Of course, we’ll need to add the corresponding routes to our routing.yml file:

# in apps/backend/config/routing.yml
bookmark_filter_delete:
  url: /bookmark/filter/:id/delete
  param: { module: bookmark, action: deleteFilter }
  requirements:
    id: \d+
 
bookmark_filter_load:
  url: /bookmark/filter/:id/load
  param: { module: bookmark, action: loadFilter }
  requirements:
    id: \d+
 
bookmark_filter_save:
  url: /bookmark/filter/save
  param: { module: bookmark, action: saveFilter }

Wait, we don’t have any link to save a filter from the admin interface! Let’s add one next to the Reset link of the filters column by overriding the _filters.php generated partial template:

// in apps/backend/modules/bookmark/templates/_filters.php from line 11
[...]
<tfoot>
  <tr>
    <td colspan="2">
      <?php echo $form->renderHiddenFields() ?>
      <a href="<?php echo url_for('@bookmark_filter_save') ?>" onclick="document.location = this.href+'?name='+prompt('Enter a name:');return false">
        <?php echo __('Save') ?>
      </a>
      <?php echo link_to(__('Reset', array(), 'sf_admin'), 'bookmark_collection', array('action' => 'filter'), array('query_string' => '_reset', 'method' => 'post')) ?>
      <input type="submit" value="<?php echo __('Filter', array(), 'sf_admin') ?>" />
    </td>
  </tr>
</tfoot>
[...]

Notice that a javascript prompt will ask you for a name before saving a filter set:

prompt.png

So we can now save a filter set in our database. Now, what about listing them below the filters form?

Listing existing saved filters

To me, the best suited place to retrieve saved filtered searches is the bookmarkGeneratorConfiguration class, which has been generated in the lib/ subfolder of the bookmark admin module. Let’s add a new getSavedFilters() method to it:

<?php
<?php
# in apps/backend/modules/bookmark/lib/bookmarkGeneratorConfiguration.class.php
class bookmarkGeneratorConfiguration extends BaseBookmarkGeneratorConfiguration
{
  public function getSavedFilters()
  {
    return Doctrine::getTable('SavedFilter')
      ->createQuery()
      ->where('type = ?', 'Bookmark')
      ->execute()
    ;
  }
}

So in the _filters partial template, which has already access to a generator configuration instance, we’re now able to iterate over retrieved saved filter objects to list them:

// in apps/backend/modules/bookmark/templates/_filters.php from line 11
  [...]
  <tr>
    <td colspan="2">
      <h3><?php echo __('Saved filters') ?></h3>
      <?php if (count($savedFilters = $configuration->getSavedFilters())): ?>
      <ul>
      <?php foreach ($savedFilters as $filter): ?>
        <li>
          <a href="<?php echo url_for('@bookmark_filter_load?id='.$filter['id']) ?>">
            <?php echo $filter['name'] ?>
          </a>
          (<a href="<?php echo url_for('@bookmark_filter_delete?id='.$filter['id']) ?>"></a>)
        </li>
      <?php endforeach; ?>
      </ul>
      <?php else: ?>
        <p>No filters saved</p>
      <?php endif; ?>
    </td>
  </tr>
</tbody>

So you can now save and list filters, load and run them against your objects list, and delete existing ones. If you thoroughly followed the steps of this tutorial, you should now see something like this below the filters form:

withlist.png

Twenty minutes, job done. Even if very quick and dirty.

Conclusion

Yes, I can hear you Symfony nerds, this could be heavily refactored, enhanced and maybe abstracted to provided generic filtering storage accross every generated admin module, but in 20 minutes? Really? ;-) Then, I’d say…

Haha, prove it :-)

Notes

[1] I’m not definitely sure of this, but who really cares?


Ce billet intitulé Saving Search Filters in Symfony's Doctrine Admin Generator a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, admin-generator, doctrine, filters,..."
Send by mail Print  Save  Delicious 
Date: Sunday, 29 Nov 2009 17:38

Symfony 1.3 and 1.4 have been released some days ago in RC2 and you should really take a look at it, because they improve a lot the way you work with the framework, especially the forms one. It’s been a couple of days I started to implement a new bookmarks management feature on a project I’m working on currently, and the new model relationship embedding feature of 1.3’s forms framework just saved me a lot of days.

Here’s a quick and dirty example on how to setup a willingly simplistic bookmarks management system using it

The schema

Here’s the naive Doctrine schema I’ll use for this example:

# in config/doctrine/schema.yml
User:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
  relations:
    Bookmarks:
      type: many
      class: Bookmark
      local: id
      foreign: user_id
      onDelete: CASCADE
 
Bookmark:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
    url:
      type: string(255)
      notnull: true
    user_id:
      type: integer(4)
      notnull: true
  relations:
    User:
      type: one
      local: user_id
      foreign: id

As you can see, it’s really naive. Anyway.

Some fixtures

What would be a Doctrine schema without fixtures? Quite nothing I guess, so here we go:

# in data/fixtures/fixtures.yml
User:
  niko:
    name: niko
 
Bookmark:
  niko_bookmark1:
    User: niko
    name: Slashdot
    url: http://slashdot.org/
  niko_bookmark2:
    User: niko
    name: Delicious
    url: http://delicious.com/
  niko_bookmark3:
    User: niko
    name: Digg
    url: http://digg.com/

Building stuff

You guessed it, after having suited the config/databases.yml file to set up a database of our choice, we can now build the database, all the classes we need and load the previously defined fixtures:

$ ./symfony doctrine:build --all --and-load

A basic user’s bookmarks management form

What we want do do now is to manage one User’s bookmarks in a single form. Let’s do it now in a new UserBookmarksForm.class.php file where we’ll build a form extending the autogenerated UserForm one:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  public function configure()
  {
    // We don't want to edit the User object
    unset($this['name']);
 
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
}

All the magic is done by calling the embedRelation() method here: all BookmarkForm instances will be embedded and managed automatically in and by our form.

Let’s use a simple controller to manage the form[1]:

<?php
class testActions extends sfActions
{
  public function executeBookmarks(sfWebRequest $request)
  {
    $this->form = new UserBookmarksForm($user = Doctrine::getTable('User')->findOneByName('niko'));
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('user_bookmarks')))
    {
      $this->getUser()->setFlash('notice', 'Bookmarks list updated');
      $this->redirect('test/index');
    }
  }
}

And a basic template:

<form action="." method="post">
  <?php echo $form->renderHiddenFields() ?>
  <?php echo $form->renderGlobalErrors() ?>
  <table>
    <?php echo $form ?>
    <tr>
      <td></td>
      <td><input type="submit"/></td>
    </tr>
  </table>
</form>

You should see something like this:

simple.png

The form is functional, you can edit several bookmarks at one. Neat? But wait, we can’t add a new one!

Embedding a bookmark creation form

Let’s enhance our form by embedding an empty BookmarkForm form instance in order to add a new bookmark:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  public function configure()
  {
    unset($this['name']);
    
    // Bookmark creation form
    $newBookmarkForm = new BookmarkForm();
    $newBookmarkForm->setDefault('user_id', $this->object->id);
    $this->embedForm('new', $newBookmarkForm);
    
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
  
  protected function doBind(array $values)
  {
    if ('' === trim($values['new']['name']) && '' === trim($values['new']['url']))
    {
      unset($values['new'], $this['new']);
    }
    
    parent::doBind($values);
  }
}

Here I just override the doBind() method to remove the bookmark creation embedded form if nothing has been submitted by the user in its fields; that’s probably because he just wants to edit existing bookmarks[2].

By refreshing the page you should now be able to see the new form:

withAdd.png

So now you’re able to add a new Bookmark and/or edit existing instances within the same simple form. Neat? Wait, we can’t delete existing bookmarks!

Deleting existing bookmarks

So you would like to have a checkbox next to every existing bookmark embedded form to schedule its deletion on saving the form? Wow, that’s getting tricky, I like that.

The Symfony forms framework has evolved a lot, but what we’re trying to achieve here will involve some fine-tunning. Don’t be too much afraid seeing the code ;)

First, we’ll have to add the checkbox widget to the BookmarkForm form, but only if an existing Bookmark object has been bound to it:

<?php
// lib/form/doctrine/BookmarkForm.class.php
class BookmarkForm extends BaseBookmarkForm
{
  public function configure()
  {
    $this->widgetSchema['user_id'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['url'] = new sfValidatorAnd(array(
      new sfValidatorString(array('max_length' => 255)),
      new sfValidatorUrl(),
    ));
    
    if ($this->object->exists())
    {
      $this->widgetSchema['delete'] = new sfWidgetFormInputCheckbox();
      $this->validatorSchema['delete'] = new sfValidatorPass();
    }
  }
}

Refreshing the form page should display something like that:

withAddAndDelete.png

The UserBookmarksForm now holds this code:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  /**
   * Bookmarks scheduled for deletion
   * @var array
   */
  protected $scheduledForDeletion = array();  
  
  /**
   * Configures the form
   *
   */
  public function configure()
  {
    unset($this['name']);
    
    // Bookmark creation form
    $newBookmarkForm = new BookmarkForm();
    $newBookmarkForm->setDefault('user_id', $this->object->id);
    $this->embedForm('new', $newBookmarkForm);
    
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
  
  /**
   * Here we just drop the bookmark embedded creation form if no value has been 
   * provided for it (it somewhat simulates a non-required embedded form)
   *
   * @see sfForm::doBind()
   */
  protected function doBind(array $values)
  {
    if ('' === trim($values['new']['name']) && '' === trim($values['new']['url']))
    {
      unset($values['new'], $this['new']);
    }
    
    if (isset($values['Bookmarks']))
    {
      foreach ($values['Bookmarks'] as $i => $bookmarkValues)
      {
        if (isset($bookmarkValues['delete']) && $bookmarkValues['id'])
        {
          $this->scheduledForDeletion[$i] = $bookmarkValues['id'];
        }
      }
    }
    
    parent::doBind($values);
  }
  
  /**
   * Updates object with provided values, dealing with evantual relation deletion
   *
   * @see sfFormDoctrine::doUpdateObject()
   */
  protected function doUpdateObject($values)
  {
    if (count($this->scheduledForDeletion))
    {
      foreach ($this->scheduledForDeletion as $index => $id)
      {
        unset($values['Bookmarks'][$index]);
        unset($this->object['Bookmarks'][$index]);
        Doctrine::getTable('Bookmark')->findOneById($id)->delete();
      }
    }
 
    $this->getObject()->fromArray($values);
  }
  
  /**
   * Saves embedded form objects.
   *
   * @param mixed $con   An optional connection object
   * @param array $forms An array of forms
   */
  public function saveEmbeddedForms($con = null, $forms = null)
  {
    if (null === $con)
    {
      $con = $this->getConnection();
    }
 
    if (null === $forms)
    {
      $forms = $this->embeddedForms;
    }
    
    foreach ($forms as $form)
    {
      if ($form instanceof sfFormObject)
      {
        if (!in_array($form->getObject()->getId(), $this->scheduledForDeletion))
        {
          $form->saveEmbeddedForms($con);
          $form->getObject()->save($con);
        }
      }
      else
      {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms());
      }
    }
  }
}

The code should be self-explanatory… or not. But anyway, you should now be able to add a new bookmark and delete existing ones as well, in a single form and process, and we’re still using the very same controller from the beginning!

Okay, let’s admit it, we’re reaching the limits of Symfony and probably those of my knowledge of it as well, but… IT WORKS.

But, what about the templating?

As of now we’ve used the convenient <?php echo $form ?> trick, but we now want to have full fine-grained control over the way our embedded form fields will look like.

Let’s update the form template accordingly:

<form action="." method="post">
  <?php echo $form->renderHiddenFields() ?>
  <?php echo $form->renderGlobalErrors() ?>
  <!-- Embedded new bookmark form -->
  <fieldset>
    <legend><?php echo __('Create a new bookmark') ?></legend>
    <?php echo $form['new']->renderError() ?>
    <div class="form-row">
      <?php echo $form['new']['name']->renderLabel() ?>
      <div class="form-field">
        <?php echo $form['new']['name']->render() ?>
        <?php echo $form['new']['name']->renderError() ?>
      </div>
    </div>
    <div class="form-row">
      <?php echo $form['new']['url']->renderLabel() ?>
      <div class="form-field">
        <?php echo $form['new']['url']->render() ?>
        <?php echo $form['new']['url']->renderError() ?>
      </div>
    </div>
  </fieldset>
  <!-- /Embedded new bookmark form -->
  <!-- Embedded existing bookmark forms -->
  <?php foreach ($form['Bookmarks'] as $i => $eForm): ?>
    <fieldset>
      <legend><?php echo sprintf(__('Edit bookmark #%d'), $i+1) ?></legend>
      <?php echo $eForm->renderError() ?>
      <div class="form-row">
        <?php echo $eForm['name']->renderLabel() ?>
        <div class="form-field">
          <?php echo $eForm['name']->render() ?>
          <?php echo $eForm['name']->renderError() ?>
        </div>
      </div>
      <div class="form-row">
        <?php echo $eForm['url']->renderLabel() ?>
        <div class="form-field">
          <?php echo $eForm['url']->render() ?>
          <?php echo $eForm['url']->renderError() ?>
        </div>
      </div>
      <div class="form-row">
        <div class="form-field">
          <?php echo $eForm['delete']->render() ?>
          <?php echo $eForm['delete']->renderError() ?>
          <?php echo $eForm['delete']->renderLabel(__('Delete this bookmark')) ?>
        </div>
      </div>
    </fieldset>
  <?php endforeach; ?>
  <!-- /Embedded existing bookmark forms -->
  <input type="submit" value="<?php echo __('Update your bookmarks') ?>"/>
</form>

Now you can really control the semantics of the HTML code of the form, field by field.

As always, if you have better solutions in mind, feel free to post them in the comments.

Notes

[1] I’ll leave you to create an app, module and route, okay?

[2] Of course this can be a lot improved, but the example is kept as simple as possible to ease understanding.


Ce billet intitulé Embedding Relations in Forms with Symfony 1.3 and Doctrine a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, doctrine, forms, php, symfony"
Send by mail Print  Save  Delicious 
Date: Wednesday, 04 Nov 2009 11:00

Just found this nice project contributed by the Facebook team, phpsh. Basically, it’s an interactive and advanced command line interface to php, a kind of super-php -r similar to what you can find in Python or Ruby. Ironically enough, phpsh is mostly written in Python, by the way.

To install phpsh, just get it from github[1]:

$ git clone git://github.com/facebook/phpsh.git
$ cd phpsh
$ python setup.py build
$ sudo python setup.py install

To run phpsh:

$ phpsh
Starting php
type 'h' or 'help' to see instructions & features
php> ="hello world"
hello world
php> =2 + 2
4
php> $a = 8

php> =$a
8

Note that you don’t have to open or close <?php tags, you can print something just by prepending an = sign to the command, and no need to type semicolum at the end of a call. Easy[2].

You can access a function documentation by just prepending the d keyword to its name:

php> d strlen
 
# strlen
 
(PHP 4, PHP 5)
 
strlen -- Get string length
 
### Description
 
int strlen ( string $string )
 
Returns the length of the given string . 
 
### Parameters
 
string     
 
The [string][1] being measured for length. 
 
### Return Values
 
The length of the string on success, and 0 if the string is empty. 
 
### 
 
   [1]: #language.types.string

You can define your own classes and functions, and run them:

php> function foo($a){echo $a.'!';}
php> foo('bar')
bar!

Same goes for classes:

php> class Foo {public function bar($a){echo $a.'!';}}
php> $f = new Foo()
php> $f->bar('baz')
baz!

You can even execute shell command from there:

php> ! ls -la

Of course, you can import include and require files. As a concrete example, let’s roughly play with the symfony API:

php> ! symfony -V
symfony version 1.2.2-DEV (/Users/niko/Sites/vendor/symfony12/lib)
php> c /Users/niko/Sites/vendor/symfony12/lib/autoload/sfCoreAutoload.class.php
Extra includes are: ['/Users/niko/Sites/vendor/symfony12/lib/autoload/sfCoreAutoload.class.php']
php> sfCoreAutoload::register()
php> =sfYaml::load('foo: bar')
array(
  "foo" => "bar",
)
php> =sfYaml::dump(array('foo' => array('bar' => 'baz')))
"foo:\n  bar: baz\n"

Last but not least, when enough playing, type q or press ctrl + d to quit phpsh.

Notes

[1] So yeah, so you need git. And Python, of course.

[2] Anybody putting "rather use python then" will have serious troubles, even if I strongly agree.


Ce billet intitulé An Interactive PHP Shell: phpsh a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, cli, php, shell, symfony"
Send by mail Print  Save  Delicious 
Date: Monday, 12 Oct 2009 16:22

I’ve just stumbled upon a nice new feature of Symfony 1.3, the ability to open any linked file within standard exception page with the text editor of your choice, at least if it provides an url scheme/protocol to open them. Texmate provides natively this feature, so let’s see how to configure a symfony project to allow its files to be opened directly in textmate when encountering a stack trace.

In the config/ folder of the project, create a settings.yml file with this content:

dev: #yeah, we never know
  .settings:
    file_link_format: "txmt://open?url=file://%f&line=%l"

That’s it. Now any standard exception page (in dev environment only, right?) will provide a txmt:// link to every PHP file listed in the stack trace. And at the correct line number. Neat.


Ce billet intitulé A Symfony 1.3 Gem: Open Files Listed in Exceptions with the Editor of your Choice a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, editor, php, symfony, textmate"
Send by mail Print  Save  Delicious 
Date: Monday, 12 Oct 2009 10:17

Si comme moi vous effectuez une veille quotidienne autour des outils et technologies Web, vous n’avez certainement pas pu passer à côté de l’engouement massif pour le langage Python ces derniers temps. De plus en plus de développeurs PHP passent sur ce langage[1], souvent par l’entremise de l’excellent framework Django, que présentera mon camarade de jeu David Larlet le vendredi 23 octobre prochain à Montpellier à l’occasion d’une conférence donnée dans le cadre de l’évènement Scrum.py.

L’efficacité potentielle du meilleur outil technique étant réduit à néant s’il n’est pas efficacement encadré par une méthodologie de conduite de projet adapté aux contraintes d’un métier souvent difficile, Claude Aubry, un des tous meilleurs experts français sur le sujet, présentera quand à lui les avantages et intérêt de Scrum durant cette même journée d’information gratuite, à laquelle je vous invite à vous inscrire en ligne dès à présent.

David proposera également un atelier Django les 18, 19 et 20 novembre prochains, tandis que Claude animera une formation Scrum les 2, 3 et 4 décembre.

L’évènement est organisé par le collectif Particul.es, et les places, tant pour les conférences que pour les formations, sont limitées ; alors n’hésitez pas à vous inscrire rapidement.

Notes

[1] Et j’avoue me réfréner fortement à faire de même tant les inconsistances de PHP me portent quotidiennement sur les nerfs


Ce billet intitulé Scrum.py, présentation et conférence Scrum et Python le 23 octobre à Montpellier a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Divers, django, montpellier, python, scr..."
Send by mail Print  Save  Delicious 
Date: Tuesday, 06 Oct 2009 13:36

I love tests, I just cannot program without them anymore, especially with Symfony. And as I write a lot of them on a daily basis, the full test suite can take a very long time to be executed ; running a symfony test:all command before every granular commit suppose you have to wait sometimes more than 5, 10 minutes or more for the whole tests to be ran: hardly acceptable. Post commit hooks can be a solution, but what if a lot of devs are working simultaneously on the same project? Added to the previously seen test execution duration, it can become a problem difficult to deal with.

Here comes continuous integration : regularly, eg. every hour, the symfony project will be built up and configured from scratch, then the full test suite will be executed and the resulting log will be aggregated. It’s a very convenient way to keep an eye on the overall code quality and integrity of the project.

There are several open source continuous integration software available on the market: Cruise Control (and phpUnderControl), Continuum, Integrity, the uncertain but awesomely looking Sismo which powers the Symfony continous integration server… But I recently had the opportunity to test Hudson, a java based one. It’s very simple to install, setup and configure, and has tons of plugins. So let’s see how it can be used to test a Symfony project.

img hudson_tests_weather

Hudson Installation

That’s really not the hardest part, because you just need a working JRE 1.5 installation on your machine, and to retrieve the latest version of the program.

Then, you can launch it directly from a shell prompt:

$  java -jar /path/to/hudson.war

If you fear to break something or just want to give it a go without the hassle of firing a term, just launch the available Java Web Start version.

On debian/ubuntu, it’s just as easy as:

$ sudo echo deb http://hudson-ci.org/debian binary/ >> /etc/apt/sources.list
$ sudo apt-get update
$ sudo apt-get install hudson
$ sudo /etc/init.d/hudson start

Configuration and Symfony Project Integration

Once Hudson is installed an running, head up to http://localhost:8080/ and start playing with the web based user interface of Hudson. The navigation and configuration forms are quite obvious, and therefore efficient. Creating a project following the provided guidelines is easy and can be achieved within minutes:

First, create a new Job, choosing the Build a free-style software project option.

Then, configure the subversion repository[1] used by your project by entering its base URL in the Source Code Management field You can schedule builds, watch for SCM activity and even trigger builds from remote script using a neat XMLRPC interface[2].

img hudson_svn

img hudson_hudson_scheduling

Configuring the building process is as easy as writing down some commands, like the one you’d execute to setup your project on a new box:

img hudson_build_commands

As of Symfony 1.3, test result logs can be exported in a jUnit XML file, so Hudson will be able to parse them and provide some useful reports, metrics and charts:

huson_test_results.png

Of course, you can configure failure notifications: email, atom feed, there’s even a twitter plugin!

As a conclusion, Hudson is a very simple but efficient continous integration tool which can easily be configured to monitor the quality of your Symfony projects… at least if you write tests ;)

Notes

[1] If you’re not using Subversion, lot of plugins are available for other SCM in the huge Hudson plugins repository

[2] Useful if you wish to use post-commit build hook


Ce billet intitulé Simple Continuous Integration of a Symfony Project using Hudson a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, continuous integration, hudson, php..."
Send by mail Print  Save  Delicious 
Date: Tuesday, 15 Sep 2009 17:14

I’m currently working on a big Symfony project, with a lot of Doctrine models and complex queries to write. I found a way to organize all of them in an object-oriented and cleaner way than using the traditionnal addNamedQuery() and createNamedQuery() methods workflow[1].

The idea is to create dedicated query classes for a given model ; this way, you can provide useful methods to build the business-related parts of your query.

As usual, the theory is more understandable with a concrete example. Let’s consider this simple Doctrine model[2] :

Disclaimer: The provided examples have been written in a hurry, so mistakes might have been not detected by my attentive proof-reading ;)

BlogAuthor:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true  
    name:
      type: string(255)
  relations:
    Post:
      type: one
      class: BlogPost
      local: id
      foreign: author_id
 
BlogPost:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    author_id:
      type: integer(4)
      notnull: true
    title:
      type: string(255)
    content:
      type: string(65535)
  relations:
    Author:
      type: one
      class: BlogAuthor
      local: author_id
      foreign: id
    Comments:
      type: many
      class: BlogComment
      local: id
      foreign: post_id
 
BlogComment:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    post_id:
      type: integer(4)
      notnull: true
    author:
      type: string(255)
    content:
      type: string(5000)
  relations:
    Post:
      type: one
      class: BlogPost
      local: post_id
      foreign: id

Now let’s imagine a Query class dedicated to query the BlogPost table:

<?php 
class BlogPostQuery extends Doctrine_Query
{
  static public function create($conn = null, $class = null)
  {
    return parent::create($conn, 'BlogPostQuery')
      ->from('BlogPost p');
  }
  
  public function addPosts($fields = 'p.*')
  {
    return $this->addSelect('p.*');
  }
  
  public function addComments($fields = 'c.*')
  {
    return $this
      ->addSelect($fields)
      ->leftJoin('p.Comments c')
      ->addGroupBy('c.id');
  }
  
  public function addAuthors($fields = 'a.*')
  {
    return $this
      ->addSelect($fields)
      ->leftJoin('p.Author a')
      ->addGroupBy('a.id');
  }
  
  public function addCommentsCount($alias = 'nb_comments')
  {
    return $this
      ->addSelect(sprintf('COUNT(c.id) as %s', $alias))
      ->addGroupBy('c.id');
  }
  
  public function filterByAuthorName($authorName)
  {
    return $this
      ->andWhere('a.name = ?', $authorName);
  }
}

So how can we use this query object? Here are some sample uses:

// Retrieve all posts
$posts = BlogPostQuery::create()
  ->addPosts()
  ->fetchArray();
 
// Retrieve all posts with comments
$posts = BlogPostQuery::create()
  ->addPosts()
  ->addComments()
  ->fetchArray();
 
// Retrieve all posts with comments and their count per post
$posts = BlogPostQuery::create()
  ->addPosts()
  ->addComments()
  ->addCommentsCount('yataa')
  ->fetchArray();
 
// Retrieve all post with chuck as its author and related comments
$posts = BlogPostQuery::create()
  ->addAuthors()
  ->addPosts()
  ->addComments()
  ->filterByAuthorName('chuck')
  ->fetchArray();
 
// and so on... 

Of course, this example of use is not really relevant as our model is really simple, but when you’re dealing with dozens of internationalized objects, it can help cleaning your model classes, controllers and improving the organization of your work.

Update and important precisions

Some people are having negative feedback regarding this technique, claiming it will encourage people using the custom query object directly in the controllers; that’s absolutely not the case as the queries are to be used only within the model layer, for example in the BlogPostTable class:

<?php
class BlogPostTable extends Doctrine_Table
{
  static public function getPostsWithCommentsByAuthor($authorName)
  {
    return BlogPostQuery::create()
      ->addPosts()
      ->addComments()
      ->filterByAuthorName($authorName)
      ->fetchArray()
    ;
  }
}

And in a controller:

class blogActions extends sfActions
{
  public function executeListByAuthor(sfWebRequest $request)
  {
    $this->posts = BlogPostTable::getPostsWithCommentsByAuthor($request->getParameter('author'));
  }
}

Notes

[1] … or raw queries written directly within controllers, but you may know that this is really bad ;)

[2] I’m using Doctrine 1.2 beta (bundled with upcoming symfony 1.3) in the provided example.


Ce billet intitulé Optimize your Doctrine Workflow with Specialized Queries a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, bestpractices, doctrine, php, query..."
Send by mail Print  Save  Delicious 
Date: Friday, 04 Sep 2009 18:50

Hey there, it’s been a while, huh?

Today I gave a talk about Symfony best practices at the Symfony Day event in Cologne, Germany; you can get the slides on slideshare or directly browse them below:

The Symfony Day event has been purely awesomely incredibly well organized, many kudos and thanks to Interlutions and to all the attendees. You’re all great people. Thanks.

Symfony Day '09 Cologne

Ce billet intitulé 30 Symfony Best Practices, the slides from my talk at SymfonyDay Cologne '09 a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, bestpractices, php, sfdaycgn, symfo..."
Send by mail Print  Save  Delicious 
Date: Sunday, 26 Jul 2009 11:48

During a trip back to Paris by train, I quickly coded for a project of mine a new brush for SyntaxHighlighter, a client-side syntax coloring library powered by Javascript, in order to highlight YAML code snippets.

YAML syntax highlighting brush

You can get the code here and provide feedback in a dedicated thread on the official forum.

Edit: Oops, didn’t find this already existing brush (I hadn’t wifi access in the train). I think my version support more specific aspects of the syntax, though.


Ce billet intitulé YAML Brush for the SyntaxHighlighter Javascript Library a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, code, javascript, syntax highlighti..."
Send by mail Print  Save  Delicious 
Date: Friday, 17 Jul 2009 12:40

I wanted to test latest php 5.3 on my OSX box. So here’s a quick reminder on how to proceed to a full setup of apache2, mysql5 and php 5.3 using Macports.

First, get a fresh copy of the Macports installer for OSX. Install macports, then in a shell prompt, type:

$ sudo port install mysql5 +server
$ sudo port install php5 +apache2 +debug +pear +sqlite +mysql5 

You have now the time to have lunch, dinner, sex or to watch an episode of Derrick[1].

To enable the php module for apache:

$ cd /opt/local/apache2/modules
$ sudo /opt/local/apache2/bin/apxs -a -e -n "php5" libphp5.so

Then, add this line in the /opt/local/apache2/conf/httpd.conf file:

Include conf/extras-conf/*.conf

Copy one of the standard php.ini files proposed by the default installation:

$ sudo cp /opt/local/etc/php5/php.ini-development /opt/local/etc/php5/php.ini

To start apache automatically at system startup:

$ sudo launchctl load -w /Library/LaunchDaemons/org.macports.apache2.plist

Or manually:

$ sudo /opt/local/apache2/bin/apachectl start

To initialize, configure and start MySQL automatically:

$ sudo -u mysql mysql_install_db5
$ sudo /opt/local/bin/mysql_secure_installation5
$ sudo /opt/local/etc/LaunchDaemons/org.macports.mysql5/mysql5.wrapper start
$ sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plist

Now launch your browser at http://localhost/: you’re done. Have some rest.

Notes

[1] Don’t try to do all that stuff at the same time, result cannot be guaranteed


Ce billet intitulé Installing php 5.3, apache2 and MySQL 5 on OS X using Macports a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Libritudes, apache, apple, howto, mac, m..."
Send by mail Print  Save  Delicious 
Date: Thursday, 16 Jul 2009 21:45

Hey, it’s been a long time I didn’t blog something clever here on Symfony, let me try to remedy this.

I’ve just stumbled upon this blog post about the use of sfValidatorCallback, which is quite cool because by using this particular validator you can virtually employ any kind of php callable to validate something. But as the author warned, it can be problematic to tie the symfony form validation framework to your model classes.

Personnaly, I rather prefer to declare a method in the form class itself to validate some value without boring myself writing each time a new sfValidatorBase derived class[1] :

<?php
class myForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name' => new sfWidgetFormInput(),
    ));
    
    $this->setValidators(array(
      'name' => new sfValidatorCallback(array('callback' => array($this, 'validateChuckNorris'))),
    ));
  }
  
  public function validateChuckNorris(sfValidatorBase $validator, $value)
  {
    // you can't validate chuck norris, but chuck norris can invalidate you
    if ('Chuck Norris' === $value)
    {
      throw new sfValidatorError($validator, 'invalid');
    }
  }
}

Of course, if you got the exact same need in another form, you should create a dedicated validator class. If you want custom error messages and options, you’ll have to create a dedicated class as well. But for simple and casual needs, this is just enough.

The neat thing with the sfValidatorCallback validator is you can even validate form schema values (say, an array containing all the values bound to the form) the same way, eg. using a post validator. Let’s see an example reusing the form shown previously:

<?php
class myForm extends sfForm
{
  static protected $choices = array(
    'none' => 'No emails', 
    'commercials' => 'Commercial emails', 
    'news' => 'Annoucement emails', 
    'alerts' => 'Alert emails',
  );
 
  public function configure()
  {
    $this->setWidgets(array(
      //...
      'email' => new sfWidgetFormInput(),
      'opt_in' => new sfWidgetFormSelectRadio(array('choices' => self::$choices)),
    ));
    
    $this->setValidators(array(
      //...
      'email' => new sfValidatorEmail(array('required' => false)),
      'opt_in' => new sfValidatorChoice(array('choices' => array_keys(self::$choices))),
    ));
    
    $this->validatorSchema->setPostValidator(new sfValidatorCallback(array(
      'callback' => array($this, 'validateSchema'),
    )));
  }
  
  // We want the user to provide his email if he choosed to receive stuff by email
  public function validateSchema(sfValidatorBase $validator, array $values)
  {
    if ($values['opt_in'] !== 'none' && !$values['email'])
    {
      throw new sfValidatorErrorSchema($validator, array(
        'email' => new sfValidatorError($validator, 'required'),
      ));
    }
  }
}

As a cool side effect, it will also make testing your form validation very easy, because you just have to test the callable method of your form.

Here’s another trick I stole to Kris on a project we worked together on lately, using a formatter callback to alter the presentation of a widget. Here’s an example showing how to get rid of the unordered list displaying a collection of checkboxes by default, by inlining them instead:

<?php
class myForm extends sfForm
{
  static protected $choices = array(
    'none' => 'No emails', 
    'commercials' => 'Commercial emails', 
    'news' => 'Annoucement emails', 
    'alerts' => 'Alert emails',
  );
 
  public function configure()
  {
    $this->setWidgets(array(
      //...
      'opt_in' => new sfWidgetFormSelectRadio(array(
        'choices' => self::$choices,
        'formatter' => array($this, 'formatInline'),
      )),
    ));
    
    $this->setValidators(array(
      //...
      'opt_in' => new sfValidatorChoice(array('choices' => array_keys(self::$choices))),
    ));
  }
  
  public function formatInline($widget, $inputs)
  {
    $formatted = array();
    
    foreach ($inputs as $input)
    {
      $formatted[] = $input['input'].' '.$input['label'];
    }
 
    return join(' ', $formatted);
  }
}

There are tons of little tricks like these which make the life of a developer using the forms framework easier, I’ll try to share them with you progressively.

Notes

[1] All my examples are using latest Symfony 1.2.


Ce billet intitulé Little Symfony Forms Tricks a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Dev, forms, php, symfony, validators, wi..."
Send by mail Print  Save  Delicious 
Date: Saturday, 04 Jul 2009 09:36

Ça fait quelques temps que nous envisagions, ma moitié, mon petit bonhomme et moi, d’aller nous installer dans la région de Montpellier. En effet, nous n’avons définitivement pas une vie parisienne ; nous habitons en banlieue, travaillons (beaucoup) (trop) en banlieue et les lumières de Paris n’éclairent que rarement nos mines grisées par tant de pollution.

De plus, le coût de la vie sur Paris est pour nous et en regard de nos moyens actuels totalement décourageant quand il s’agit de chercher de l’espace et de la qualité de vie, même si, entendons nous bien, il y a beaucoup plus malheureux que nous sur ce chapitre. Bref, tous les inconvénients sans les avantages, l’équation semblait insoluble…

Et hier, nous avons reçu une excellente nouvelle : Gaëlle a reçu une réponse positive à sa candidature à un poste dans la région, et commence le 1er septembre ! C’est donc très officiellement que nous entamons aujourd’hui notre processus de migration, qui commence dès aujourd’hui par la recherche d’une petite maison à louer sur Montpellier et alentours[1], idéalement meublée puisque nous comptons louer notre appartement d’Arcueil de la même façon[2]. Si par hasard vous connaissez quelqu’un qui libère quelque chose d’approchant sur Montpellier même ou dans les petites villes en bordure, n’hésitez pas à vous manifester en commentaire ou par email.

Pour ma part, je quitte donc Sensio dès le 10 juillet, au terme d’une collaboration très intéressante qui m’aura notamment appris à mieux me connaître, pour entamer les démarches de la création de mon entreprise, dans un premier temps individuelle, où je proposerai mes services en tant qu’architecte Web indépendant. J’ai d’ailleurs déjà pris quelques engagements sur des projets très motivants, stay tuned ;-)

Last Morning in Montpellier

Notes

[1] Jacou, Castelnau le Lez, Saint_jean de Védas, etc.

[2] J’ai d’ailleurs un lecteur de longue date déjà sur le coup ;-)


Ce billet intitulé Changement de vie a été rédigé par Nicolas Perriault et publié sur le blog Prendre un Café sous licence Creative Commons BY-NC-SA.

Author: "NiKo" Tags: "Divers, annonce, freelance, location, ma..."
Send by mail Print  Save  Delicious 
Next page
» You can also retrieve older items : Read
» © All content and copyrights belong to their respective authors.«
» © FeedShow - Online RSS Feeds Reader