Votre application PHP (script, pages, sites, application complexe) est avant tout développée pour être exécutée dans un contexte unique :

  • soit par serveur web
  • soit en ligne de commande
  • soit sous forme d'application graphique

(dans la majeure partie des cas)

Votre application est souvent "optimisée" pour son contexte d'exécution principal et même très souvent ne peut fonctionner que dans ce contexte (est-il possible de faire tourner votre site internet en ligne de commande ? d'accéder à des fonctionnalités de votre application graphique depuis un serveur web ? ...)

C'est normal.

Imaginons que vous souhaitiez réaliser des tests sur votre application (par exemple unitaire avec PHPUnit), pourrez vous tester unitairement une fonctionnalité qui a besoin d'une connexion à une base de données ou de lire dans un fichier de config, depuis la ligne de commande (exécutable PHPUnit ou bien même batch Phing) ?

Ou encore, si vous développez une fonctionnalité "non graphiques", comme par exemple la gestion des utilisateurs, l'appel d'un webservices distant, le stockage d'information en base de données, l'envoi de sms ou de mail, ... il peut être intéressant de pouvoir réutiliser vos développements (ou en tout cas une partie) dans un script de migration ou de mise à jour en ligne de commande, même si initiallement le développement a été réalisé pour être exposé sur le web.

Pour répondre à ce besoin "multi-contextes", voici ma méthode.

Différents contextes

  • Créer un répertoire "contexts/" à la racine de votre projet
  • Ajouter les différents contextes nécessaires à votre application :
    • web.php => le fichier inclu par votre index.php ou votre page.php
    • unit-tests.php => le fichier inclu systématiquement dans vos classes de tests unitaires
    • functional-tests.php => le fichier inclu systémtatiquement dans vos fixtures greenpeper-php ou fitnesse-php
    • ...
    • @common.php => le fichier central inclu par tous les autres, il est censé contenir les functions communes et les inclusions de vos développements

Concrètement ces fichiers (à part @common.php) contiennent en règle générale quelques lignes seulement. Voici quelques exemples :

Exemple: web.php

$env = $_SERVER['PHP_ENV'];
/**
 * Includes the test application functions.
 */
require dirname(__FILE__) . '/@common.php';
/**
 * Setup controller
 */
$controller = Zend_Controller_Front::getInstance();
$controller->addModuleDirectory('../application/default');
$controller->addControllerDirectory('../application/default/controllers');
$controller->setControllerDirectory('../application/admin/controllers');
$controller->throwExceptions(false); // should be turned on in development time
/**
 * Bootstrap Layouts
 */
Zend_Layout::startMvc(array(
    'layoutPath' => '../application/default/layouts',
    'layout' => 'main'
    ));
/**
 * Runs
 */
$controller->dispatch();

Exemple: unit-tests.php

$env = 'tests';

require dirname(__FILE__) . '/@common.php';

$svc = new DatabaseService();

$dbFile = dirname(dirname(__FILE__)).'/tmp/db/test.sqlite';
if(is_file($dbFile)) {
    @unlink($dbFile);
}
$dsn = 'sqlite://'.$dbFile;
$svc->create($dsn,dirname(__FILE__) ."/../config/db/{$env}/schema.xml");
$svc->update($dsn,dirname(__FILE__) ."/../config/db/{$env}/all.xml");

Exemple: @common.php

require 'Zend/Loader.php';
require dirname(__FILE__) . '/../application/functions.php';

/**
 * Registers autoload
 */
Zend_Loader::registerAutoload();

/**
 * Sets the root directory of the application
 */
$root = dirname(dirname(__FILE__));

/**
 * Sets the global include path of the application
 */
set_include_path(// the list of include paths
    $root                                   . PATH_SEPARATOR .
    $root."/application/default/controllers". PATH_SEPARATOR .
    $root."/application/default/models"     . PATH_SEPARATOR .
    ...
    get_include_path());

/**
 * Loads the main configuration from the specific environment section of the
 * configuration file.
 */
if(!$env) {
     die('You must set $env variable.');
}
$cnf = new Zend_Config_Ini(dirname(__FILE__).'/../config/run.ini', $env);
Zend_Registry::set('_cnf', $cnf);

// ...

/**
 * Sets the database connection configuration
 */
if (@$cnf->database!==null) {
    Zend_Db_Table_Abstract::setDefaultAdapter(Zend_Db::factory($cnf->database));
}

Comment utiliser ces "contextes" ?

Exemple d'utilisation du contexte web dans un fichier html/index.php

<?php
require_once dirname(__FILE__).'/../contexts/web.php';

Exemple d'utilisation du contexte unit-tests dans une classe de tests PHPUnit dans tests/MyServiceTest.php

<?php
require_once dirname(__FILE__).'/../contexts/unit-tests.php';

// no requires anymore since we use autoloading

class CityServiceTest extends PHPUnit_Framework_TestCase {

    /**
      * @expectedException NullPointerException
      */
    public function testGetCityNameFromZipCodeForNullZipCodeThrowException() {
         $svc = new CityService();
         $svc->getCityNameFromZipCode(null);
    }

}

Exemple d'utilisation du contexte cli dans un script en ligne de commande scripts/migrate-data.php

<?php

require_once dirname(__FILE__).'/../contexts/cli.php';

// no requires anymore since we use autoloading

$svc = new MigrationService();

try {
    $result = $svc->migrateFromTo($from,$to);
} catch(Exception $e) {
    die("Error occured when data migration : ".$e->getMessage());
}

echo "Migration completed";