Symfony: Contrôle de la disponibilité de la base de données avec un event

Symfony, depuis la version 1.2, offre un système d’événements (sfEventDispatcher et sfEvent). Cela va permettre la mise en place de notre contrôle. Pour cela nous allons nous connecter à l’événement « doctrine.configure_connection ». Nous allons effectuer cela dans notre application configuration (frontend) avec le code ci-dessous:

class frontendConfiguration extends sfApplicationConfiguration
{
  public function configure()
  {
    $this->dispatcher->connect('doctrine.configure_connection', array($this, 'listenToAddCheckDatabaseConnection'));
  }
  
  public function listenToAddCheckDatabaseConnection(sfEvent $event)
  {
    $actions = $event->getSubject();
    $actions->getCurrentConnection()->connect();
    sfConfig::set('sf_database_is_connected', $actions->getCurrentConnection()->isConnected());
  }
}

Nous allons ensuite intercepter notre nouvelle configuration en personnalisant notre frontController. Pour cela, nous allons ajouter une nouvelle classe dans notre projet sous lib/controller/mySfFrontWebController.class.php

class mySfFrontWebController extends sfFrontWebController
{
  public function dispatch()
  {
    if (!sfConfig::get('sf_database_is_connected', true))
    {
      $request    = $this->context->getRequest();
      $moduleName = $request->getParameter('module');
      $actionName = $request->getParameter('action');
      
      if ((!$module = sfConfig::get('app_database_not_connected_module')) ||
      (!$action = sfConfig::get('app_database_not_connected_action')))
      {
        throw new sfConfigurationException('Missing parameter(s): app_database_not_connected_module and app_database_not_connected_action are required in app.yml');
      }
      else
      {
        if (($moduleName != $module) && ($actionName != $action))
        {
          $this->forward($module, $action);
          exit;
        }
      }
    }
    parent::dispatch();
  }
}

Pour charger notre nouvelle classe, nous allons le faire dans le fichier factories.yml de notre application (frontend):

all:
  controller:
    class: mySfFrontWebController

Nous allons définir nos nouveaux paramètres dans le fichier app.yml de notre application (frontend):

all:
  database_not_connected:
    module:       main
    action:       notconnected

Il nous reste maintenant à définir notre nouvelle action dans le module main (ou autre).

Voilà. La mise en place du contrôle de connexion sur notre base de données est terminée.

J’espère que ce petit bout de code vous servira.

Share

Symfony: Afficher un message en cas de non disponibilité de la base de données

Symfony ne proposant pas une fonctionnalité me permettant de définir un message en cas de non disponibilité de la base de données, j’ai réalisé un filtre pour contrôler cela. Pour le rendre flexible, j’ai ajouté deux options permettant la définition du module et de l’action appelé lors de l’erreur.

J’ai commencé par créer dans mon module default, une action checkAvailibility me permettant de réaliser un template pour l’affichage du message (je passe sur cette étape car je pense que vous savez le faire). Ensuite, il suffit de les définir dans le fichier app.yml:

all:
  checkdb:
    module: default
    action: checkAvailibility

J’ai ensuite créé mon fichier filtre appelé « checkAvailibilityDbFilter.class.php » dans le dossier /lib de mon projet:

class checkAvailibilityDbFilter extends sfFilter
{
  public function execute($filterChain)
  {
    if ($this->isFirstCall())
    {
      $context = $this->getContext();

      $module = sfConfig::get('app_checkdb_module', 'default');
      $action = sfConfig::get('app_checkdb_action', 'error404');

      if (($module != $context->getModuleName()) || ($action != $context->getActionName()))
      {
        $configuration = sfProjectConfiguration::getActive();
        $db = new sfDatabaseManager($configuration);
        
        foreach ($db->getNames() as $connection)
        {
          try
          {
            @$db->getDatabase($connection)->getConnection();
          } 
          catch(Exception $e)
          {
             $context->getController()->forward($module, $action);
             exit;
          }
        }
      }
    }
    
    $filterChain->execute();
  }
}

Il reste encore à activer ce filtre pour que cela fonctionne. J’ai ajouté les lignes suivantes dans le fichier filters.yml du dossier config de l’application:

rendering: ~
security:  ~

# insert your own filters here
db:
  class:  checkAvailibilityDbFilter

cache:     ~
common:    ~
execution: ~

J’espère que cette petite astuce vous sera utile.

Share

sfDoctrineGuardPlugin: Récupération de l’id utilisateur

Il y a souvent des questions concernant la récupération de l’id utilisateur lorsque l’on utilise sfDoctrineGuardPlugin. En faite vous avez le choix entre deux possibilités. La première va lancer une requête dans la base de données, la seconde va récupérer l’id dans la session courante.

Exemple 1:

$this->user_id = $this->getUser()->getGuardUser()->getId();

Exemple 2:

$this->user_id = $this->getUser()->getAttribute(
'user_id', null, 'sfGuardSecurityUser');

Nous allons maintenant créer une nouvelle fonction dans notre classe myUser permettant d’accéder directement à notre id:

class myUser extends sfGuardSecurityUser
{
  public function getId()
  {
    return $this->getAttribute('user_id', null, 'sfGuardSecurityUser');
  }
}

Après avoir créer cette nouvelle fonction, vous avez accès à votre id en exécutant le code suivant:

$this->user_id = $this->getUser()->getId();

Voilà pour la petite mise en place.

Share

Filtre de redirection basé sur le langage

Pour compléter mon article précédent De la culture au language, j’ai réalisé un filtre permettant de faire une re-direction sur la page d’accueil selon le langage du navigateur web. Si ce langage n’est pas disponible, le navigateur sera automatiquement redirigé vers la langue par défaut.

Nous allons commencer par définir quelques paramètres dans le fichier app.yml du dossier config:

all:
  language:
    homepage_route:   home_language
    allowed:
      fr: fr_CH
      de: de_CH
      en: en_US

Nous allons également définir la route par défaut inscrite dans le paramètre « homepage_route »:

home_language:
  url:  /:sf_language
  param:  { module: main, action: index }

Nous poursuivons par la mise en place du filtre. Ajoutons le fichier nommé « languageFilter.class.php » dans le dossier lib de votre application:

class languageFilter extends sfFilter
{
  public function execute($filterChain)
  {
    // Execute this filter only once
    if ($this->isFirstCall())
    {
      $is_correct_url = true;
      
      $context = $this->getContext();

      $language = $context->getRequest()->getParameter('sf_language');
      
      $language_allowed = sfConfig::get('app_language_allowed');

      // Si la langue n'est pas dans l'url
      if($language == NULL)
      {
        // Récupération de la langue préférée du navigateur
        $language = substr($context->getRequest()->getPreferredCulture(), 0, 2);
        
        // Nous contrôlons si cette langue est dans la liste des langues valident
        // Si ce n'est pas le cas, nous prenons la langue par défaut
        if(!array_key_exists($language, $language_allowed))
        {
          $language = substr(sfConfig::get('sf_default_culture'), 0, 2);
        }
        $is_correct_url = false;
      }
      
      // Si la langue n'est pas définie comme active
      // Nous redirigeons sur la page erreur 404
      if(!array_key_exists($language, $language_allowed))
      {
        $module = sfConfig::get('sf_error_404_module', 'default');
        $action = sfConfig::get('sf_error_404_action', 'error404');
        $context->getController()->forward($module, $action);
        exit;
      }
      else
      {
        // Nous récupérons la culture valide selon le code de langue
        $user_culture = $language_allowed[$language];

        // Si la culture est différente de la culture de session
        // Nous la changeons
        if($user_culture != $context->getUser()->getCulture())
        {
          $context->getUser()->setCulture($user_culture);
        }
        
        // Nous renseignons la valeur meta de la langue
        $context->getResponse()->addHttpMeta('content-language', $language);
        
        // Nous effectuons la redirection
        $actionname = $context->getActionName();
        if($is_correct_url == false && $actionname != 'error404')
        {
          $context->getController()->redirect(
                             sfConfig::get('app_language_homepage_route'));
        }
      }
    }
    
    // Execute next filter
    $filterChain->execute();
  }
}

Il nous suffit maintenant d’activer ce filtre en ajoutant ces lignes dans le fichier filters.yml du dossier config:

rendering: ~
security:  ~

# insert your own filters here
language:
  class:  languageFilter
  
cache:     ~
common:    ~
execution: ~

Dès à présent, si votre navigateur est en langue française, si vous tapez « http://monsite.com », vous allez être redirigé vers l’adresse « http://monsite.com/fr ». Dans le cas d’une langue non reconnue, vous obtiendrez une erreur 404.

Avec ce filtre, nous avons une solution robuste pour gérer correctement notre langue.

Bonne découverte.

Share

De la culture au langage

Symfony ne proposant pas de système pour la gestion du langage selon la culture utilisée, j’ai cherché une solution viable. Je m’explique. Dans mon cas, un utilisateur reçoit une culture « fr_CH » pour permettre un formatage correct sur les nombres. Par contre, dans les urls du site, je désire y mettre uniquement la première partie « fr ». Je vous propose dans ce post de vous livrer ma solution. J’ai pour cela utilisé le système d’Event, qui au passage est vraiment un truc super. Je vous conseille vraiment de découvrir cette petite merveille.

Pour débuter, j’ai rajouté ces quelques lignes dans mon fichier myUser.class.php se trouvant dans le dossier lib de l’application:

class myUser extends sfGuardSecurityUser
{
  public function initialize(sfEventDispatcher $dispatcher, sfStorage $storage, $options = array())
  {
    $dispatcher->connect('user.change_culture', array($this, 'listenToChangeCultureEvent'));
    parent::initialize($dispatcher, $storage, $options);
  }
  
  public function listenToChangeCultureEvent(sfEvent $event)
  {
    $result = explode('_', $this->culture);
    $this->setLanguage($result[0]);
  }
  
  public function setLanguage($language)
  {
    $this->language = $language;
  }
  
  public function getLanguage()
  {
    return $this->language;
  }
}

Comme vous pouvez le voir dans ce code, je me suis connecté à l’event « user.change_culture ». Avec cette fonction, je vais scinder les deux parties de la culture pour ne garder plus que le terme « fr ».

Je voulais ensuite avoir la possibilité d’accéder au paramètre « sf_language » dans mon fichier de routing. J’ai pour cela surchargé la méthode « listenToChangeCultureEvent » de la classe « sfPatternRouting ». J’ai créé un nouveau fichier nommé « mysfPatternRouting.class.php » dans mon dossier lib (toujours dans mon app) avec le code suivant:

class mysfPatternRouting extends sfPatternRouting
{
  public function listenToChangeCultureEvent(sfEvent $event)
  {
    $result = explode('_', $event['culture']);
    $this->setDefaultParameter('sf_language', $result[0]);
    parent::listenToChangeCultureEvent($event);
  }
}

Pour charger ma classe routing, j’ai également modifié le fichier factories.yml en activant cette partie.

all:
  ...
  routing:
    class: mysfPatternRouting
    param:
      load_configuration: true
      suffix:             .
      default_module:     default
      ...

J’ai remplacé le contenu du tag class par « mysfPatternRouting » (Valeur par défaut: sfPatternRouting).

Ensuite, il suffit de vider le cache de symfony pour qu’il charge la nouvelle classe

$ ./symfony cc

Dès maintenant, on aura accès au nouveau paramètre « sf_language » dans notre fichier de routing.yml. Voici un exemple

user_index:
 url:  /:sf_language/user
 param:  { module: user, action: index }
 requirements:
   - sf_language: (?:fr|en|de)

J’espère que cette solution vous sera utile dans vos prochains développements 😛

Share

Passage de votre site en maintenance lors de vos mises à jour

Prérequis: L’activation du plugin sfDoctrineGuardPlugin

Dans le cadre d’un développement, je cherchais à passer à mon application en mode maintenance pour un utilisateur normal mais que le super admin puisse continuer à visualiser les pages du site. La tâche symfony n’est pas satisfaisante dans ce cas car elle verrouille complètement l’accès à celui-ci. Voici donc ma solution:

Premièrement, j’ai activé le paramètre check_lock dans le fichier settings.yml de mon application.

Path: /apps/[NomApp]/config/settings.yml

all:
  .settings:
    check_lock:             on

Pour stocker mon fichier de verrouillage, j’ai créé un dossier « lock » dans le dossier data

mkdir data/lock

Attention: ce dossier doit être en écriture

J’ai ensuite inséré le code suivant dans ProjectConfiguration

Path: /config/ProjectConfiguration.class.php

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    ...
  }
  
  public function getApplicationLockFile()
  {
    return sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.
               'lock'.DIRECTORY_SEPARATOR.$this->getApplication().'.lck';
  }
}

Vient maintenant la partie contrôle de l’accès. Pour cela j’ai créé un filtre que voici

Path: /lib/isApplicationActivatedFilter.class.php

class isApplicationActivatedFilter extends sfFilter
{
  public function execute ($filterChain)
  {
    if ($this->isFirstCall())
    {
      if (sfConfig::get('sf_check_lock'))
      {
        $context = $this->getContext();
        $config = $context->getConfiguration();
        $user = $context->getUser();

        $fileLock = $config->getApplicationLockFile();
        if (file_exists($fileLock) && !$user->isSuperAdmin())
        {
          $files = array(
            sfConfig::get('sf_app_config_dir').'/unavailable.php',
            sfConfig::get('sf_config_dir').'/unavailable.php',
            sfConfig::get('sf_web_dir').'/errors/unavailable.php',
            sfConfig::get('sf_symfony_lib_dir').'/exception/data/unavailable.php',
          );
          
          foreach ($files as $file)
          {
            if (is_readable($file))
            {
              include $file;
              break;
            }
          }

          die(1);
        }
      }
    }
    $filterChain->execute();
  }
}

Il reste maintenant à charger ce filtre dans notre application

Path: /apps/[NomApp]/config/filters.yml

rendering: ~
security:  ~

# insert your own filters here
isApplicationActivated:
  class:  isApplicationActivatedFilter
  
cache:     ~
common:    ~
execution: ~

Il faut suffit maintenant de personnaliser votre page de maintenance en créant le fichier unavailable.php dans votre dossier config.

Dès à présent, si vous créer un fichier [NomApp].lck dans le dossier lock, votre application passera en mode maintenance.

Dans cette article, je ne détaille pas la création et la suppression de ce fichier lck. Je vous laisse le soin de le faire avec votre propre gestion.

Pour tester le bon fonctionnement de ce procédé, vous pouvez créer deux comptes dans votre base de données:

  • Compte admin avec le flag is_super_admin: true
  • Compte user sans le flag is_super_admin

Vous vous identifiez avec le compte admin et ensuite vous créer le fichier [NomApp].lck dans votre dossier lock et vous vous rendez compte que vous avez toujours accès au page de votre site. Si vous répétez la chose avec le compte user, vous verrez apparaître la page de maintenance.

J’espère que ce code vous servira dans votre prochain développement.

Bonne découverte.

Share

Intégration de phpexcel dans un projet symfony

Un petit article cette fois, pour vous expliquer la démarche à suivre pour intégrer phpexcel à votre projet symfony. Je suppose que votre développement est sous contrôle de version (subversion).

Nous allons commencer par créer notre dossier vendor dans notre dossier lib. Vous pouvez passer cette étape si vous possédez déjà votre dossier vendor (en général, c’est dans ce dossier que j’installe symfony en externals)

mkdir lib/vendor

Nous allons maintenant décharger les librairies phpexcel dans ce dossier avec un lien svn:externals

svn pe svn:externals lib/vendor
PHPExcel svn://svn.phpexcel.net/PHPExcel/branches/v1.6.7/Classes
svn up

A mesure que le projet phpexcel avance, vous avez la possibilité d’effectuer la mise à jour en changeant la chaîne « v1.6.7 » par celle de votre choix.

Attention: Ne pas oublier de vider votre cache

Il nous reste maintenant à ajouter un include path sur les librairies. Nous allons pour cela ajouter un peu de code dans le fichier ProjectConfiguration.class.php se trouvant dans le dossier config:

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
     ...

    // Path sur les lib phpexcel
    sfToolkit::addIncludePath(array(
      realpath(dirname(__FILE__).'/../lib/vendor/PHPExcel'),
    ));
  }
}

Vous êtes maintenant prêt à utiliser les fonctions de phpexcel.

Vous pouvez récupérer la documentation, le code et des exemples sur le site http://www.phpexcel.net.

Bonne découverte.

Share

sfZ3950Plugin: Une nouvelle manière de se connecter

Je viens de terminer la première version beta d’un nouveau plugin permettant la connexion à un serveur Z39.50. Ce protocole est surtout utilisé dans le domaine des bibliothèques pour rechercher des données bibliographiques. Le language d’interrogation est assez barbare. Un exemple de requête native:

@and @attr 1=1003 totok @attr 1=4 Handbuch

En utilisant ce plugin, la construction d’une requête sera facilitée. Il suffit pour cela de définir les index du serveur que l’on veut questionner. Exemple du fichier z3950.yml stocké dans le dossier config:

prod:
  connection_name:
    indexes:
      ti: 1=4
      au: 1=1003
      aw: 1=1035

Il faut également définir la connexion dans le fichier databases.yml de la manière suivante:

all:
  connection_name:
    class:          sfZ3950Database
    param:
      dsn:          yaz://user:pass@url:port/dbname
      options:
        protocol:   2
        group:      test
        cookie:     cookie_name
        proxy:      proxy_name
        persistent: false
        piggyback:  true
        charset:    UTF-8
        preferredMessageSize: 10240
        maximumRecordSize:  10240

Les options de la connexion ci-dessus ne sont pas obligatoires. Elles permettent d’affiner les réglages.

Maintenant, il vous suffit de définir une requête. Il se présente comme une requête SQL à la différence que dans la zone ‘from’, on insère le nom de la connexion:

$this->results = sfZ3950_Query::create()
->from('connection_name')
->where('au="totok" and ti="Handbuch"')
->orderBy('au ASC')
->execute();

Pour pouvoir utiliser ce plugin, vous devez au préalable avoir installé le client YAZ et le module PHP YAZ.

Vous pouvez trouver le plugin ici.

Vos retours seront le bienvenu pour améliorer encore ce plugin.

Bonne découverte.

Share

Symfony: Lister les modules d’une application

Aujourd’hui un poste très court mais qui peut vous servir si vous voulez lister les modules d’une application. Pour cela, nous allons utiliser la classe sfFinder:

$modules = sfFinder::type('directory')
                     ->relative()
                     ->maxdepth(0)
                     ->in(sfConfig::get('sf_app_module_dir'));

Si vous voulez en savoir plus sur cette classe, je vous conseille la page API.

Bonne lecture

Share

Le cache de résultat avec doctrine

Note préalable: Nous utilisons dans cette article le système de cache php APC. Vous devez installer celui-ci sur votre configuration pour que l’exemple ci-dessous fonctionne.

Doctrine a en standard la possibilité de mettre en cache les résultats d’une requête. Il propose entre autre Memcached, APC, Sqlite. Nous allons l’utiliser dans le cadre d’un exemple symfony.

Pour commencer, nous allons initialiser une variable contenant la durée de vie du cache dans le fichier app.yml de notre application:

all:
  cache:
    lifetime: 3600

Nous allons maintenant initialiser le cache APC de doctrine. Le code ci-dessous est à mettre dans la classe de configuration de votre application:

class frontendConfiguration extends sfApplicationConfiguration
{ 
  public function configure()
  {
  }

  public function configureDoctrine(Doctrine_Manager $manager)
  {
    /* Initialisation du cache Doctrine APC */
    $cacheDriver = new Doctrine_Cache_Apc();
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine::ATTR_RESULT_CACHE_LIFESPAN, sfConfig::get('app_cache_lifetime'));
  }
}

Pour cette exemple, j’utilise le modèle suivant:

---
dUser:
  tableName:  d_user
  columns:
    id:
      type: integer(3)
      primary:  true
      autoincrement:  true
    fullname:
      type: string(160)
      notnull:  true
    street:
      type: string(160)
    zip:
      type: string(10)
    city:
      type: string(80)
    country:
      type: string(60)

Dans la classe du modèle ci-dessus, nous allons utiliser le cache. La directive useResultCache est ajouté à la requête.

class dUserTable extends Doctrine_Table
{
  public function findFullnameOrderByAsc()
  {
      return $this->createQuery('u')
                         ->select('u.fullname, u.city')
                         ->orderBy('fullname')
                         ->useQueryCache()
                         ->useResultCache()
                         ->execute();
  }
}

Il vous suffit ensuite d’exécuter votre requête dans votre action avec le code suivant pour récupérer le résultat.

$this->users = Doctrine::getTable('dUser')->findFullnameOrderByAsc();

Si vous utilisez la debug toolbar, vous constaterez qu’à la première requête, vous aurez 1 appel à la base de donnée mais que si vous rechargez votre page html, aucune nouvelle requête n’est lancée. Le cache fonctionne.

J’espère que ce point de départ vous aidera dans vos découvertes. N’hésitez pas à poster vos découvertes dans les commentaires.

Merci à [MA]Pascal pour la piste de configuration.

Références:
Documentation Doctrine sur le cache
La documentation du cache PHP alternatif APC
L’extension php apc

Article:
Performance Tuning in PHP

Share