La SPL dispose en effet de tout l'arsenal nécessaire pour manipuler une arborescence de fichiers via la programmation orientée objet.

Les espaces de nommage sont quand à eux l'une des nouveautés phare de PHP 5.3, et j'avais envie de vous faire partager un retour d'expérience concret à ce sujet, notamment lorsque plusieurs espaces de nommage sont utilisés dans un même script.

Cerise sur le gateau, l'utilisation d'objet permettra d'avoir un code modulaire et évolutif, par exemple via l'héritage.

J'ai donc commencé par définir l'espace de nommage de ma classe permettant le clonage d'une arborescence.

Cette dernière étant dédiée à la gestion de projet, je me suis décidé pour \project\management, et elle portera le nom de cloner.

La déclaration de la classe est donc la suivante :

namespace project\management
{
class cloner
   {
...
   }
}

La classe est comprise dans le bloc correspondant à l'espace de nommage, puisque le script final sera contenu dans un unique fichier qui contiendra plusieur espaces de nommage, comme nous allons le voir par la suite.

La classe devant travailler à partir d'un répertoire source et pouvoir injecter des variables dans les fichiers de la nouvelle arborescence, elle doit contenir une propriété permettant l'accés à ce répertoire source et une autre contenant les variables.

Ces deux propriétés étant utiles uniquement à l'intérieur de la classe et ne devant pas pouvoir être modifiées de l'extérieur sans contrôle, elles sont donc déclarées en tant que protected, afin d'être accessible aux éventuelles classes héritant de project\management\cloner.

La classe ressemble alors à ceci :

namespace project\management
{
   class cloner
   {
protected $source = null;
protected $variables = array();
...
   }
}

Le constructeur de la classe doit prendre comme argument le répertoire source ainsi que le chemin d'accès au fichier contenant les variables à injecter dans la nouvelle arborescence.

Comme le but du script est d'utiliser la SPL, la variable définissant la source devra être de type splFileInfo.

Comme splFileInfo appartient à l'espace de nommage global, il est nécéssaire de préfixer le nom de la classe avec \,et il en va de même pour tous les noms de classe appartenant à l'espace de nommage global tel que exception, sinon, PHP cherchera à instancier la classe \project\management\splFileInfo ou \project\management\exception.

Le constructeur ressemble donc à cela :

...
public function __construct(\splFileInfo $source, $iniFilePath = null)
{
$this->setSource($source);

if ($iniFilePath !== null)
{
$variables = parse_ini_file($iniFilePath);

if ($variables !== false)
{
$this->setVariables($variables);
}
else
{
throw new cloner\exception('Unable to read variables in path \'' . $iniFilePath . '\'', 2);
}
}
}
...

Les méthodes \project\management\cloner::setSource() et \project\management\cloner::setVariables(), chargées de gérer l'accès aux propriétés de la classe, sont définies de la manière suivante :

...
public function setSource(\splFileInfo $source)
{
if ($source->isDir() === false)
{
throw new cloner\exception('Source \'' . $source . '\' is not a directory', 1);
}
else
{

$this->source = $source;
return $this;
}
}

public function setVariables(array $variables)
{
$this->variables = $variables;
return $this;
}
...

Une fois tout cela défini, il reste à faire la méthode qui fait réellement le travail. Son code est le suivant :

...		
public function cloneIn(\splFileInfo $destination)
{
if ($destination->isFile() === true)
{
throw new cloner\exception('Destination \'' . $destination . '\' is a file', 3);
}
else if ($destination->isDir() === false && mkdir($destination->getPathname(), 0777, true) === false)
{
throw new cloner\exception('Unable to create directory \'' . $destination . '\' to clone \'' . $this->source . '\'', 4);
}
else
{
try
{
foreach (new \directoryIterator($this->source->getPathname()) as $inode)
{
if ($inode->isDot() === false)
{
$this->cloneInode($inode, new \splFileInfo($destination . DIRECTORY_SEPARATOR . $inode->getFilename()));
}
}
}
catch (\unexpectedValueException $exception)
{
throw new cloner\exception('Source \'' . $this->source . '\' does not exist', 5);
}
}
}
...

En résumé, la méthode fait appel à l'itérateur directoryIterator de la SPL pour parcourir l'arborescence source et si le fichier rencontré par l'itérateur ne commence pas par ., le fichier est cloné.

Le travail de clonage proprement dit est effectué par les méthodes suivantes :

...
protected function cloneInode(\splFileInfo $inode, \splFileInfo $destination)
{
switch (true)
{
case $inode->isFile():
return $this->cloneFile($inode, $destination);

case $inode->isDir():
return $this->cloneDirectory($inode, $destination);

default:
throw new cloner\exception('Unable to clone path \'' . $inode . '\'', 6);
}
}

protected function cloneFile($inode, \splFileInfo $destination)
{
$fileContents = file_get_contents($inode->getPathname());

if ($fileContents === false)
{
throw new cloner\exception('Unable to clone file \'' . $inode . '\' because it is not readable', 7);
}
else
{
foreach ($this->variables as $name => $value)
{
$fileContents = str_replace('%{' . $name . '}', $value, $fileContents);
}

if (file_put_contents($destination->getPathname(), $fileContents) === false)
{
throw new cloner\exception('Unable to clone file \'' . $inode . '\' because destination \'' . $destination->getPathname() . '\' is not writable', 8);
}
else
{
return $this;
}
}
}

protected function cloneDirectory($inode, \splFileInfo $destination)
{
$cloner = new self($inode);
$cloner->setVariables($this->variables)->cloneIn($destination);
return $this;
}
...

Rien de bien particulier si ce n'est l'appel récursif dans \project\management\cloner::cloneDirectory() pour permettre le clonage des répertoires.

Les lecteurs attentifs auront noté les exceptions \project\management\cloner\exception qui sont jetées tout au long des méthodes pour permettre la gestion des erreurs rencontrées par le script au niveau du système de fichier.

La définition de cette classe d'exception est on ne peut plus standard, au détail près qu'elle est faite dans l'espace de nommage de la classe cloner :

...
namespace project\management\cloner
{
class exception extends \exception {}
}
...

Il est à noter que dans le code de la classe \project\management\cloner, il est inutile de préfixer le nom de la classe d'exception cloner\exception avec \project\management lors d'une instanciation puisque cette dernière est incluse dans l'espace de nommage \project\management par définition.

Il ne reste donc plus qu'à utiliser tout cela dans l'espace de nommage global, qui doit obligatoirement être défini vu qu'il y a d'autres espaces de nommage dans le  script :

...
namespace
{
try
{
if ($argc < 3)
{
echo 'Syntax: php ' . basename(__FILE__) . ' <src> <dest> [variables.ini]' . "\n";
// Go to HELL !
return 666;
}
else
{
$cloner = new \project\management\cloner(new \splFileInfo($argv[1]), isset($argv[3]) === false ? null : $argv[3]);
$cloner->cloneIn(new \splFileInfo($argv[2]));
return 0;
}
}
catch (\exception $exception)
{
echo $exception->getMessage() . "\n";
return $exception->getCode();
}
}

Il suffit donc, pour cloner une arborescence d'appeler le script de la manière suivante :

php cloner.php /path/to/source /path/to/destination [/path/to/variables.ini]

Pour ceux qui sont intéressés par le code dans son intégralité, il est disponible ici.

Et pour répondre à la question qu'Olivier pose à la fin de son billet, j'utilise en général un Makefile pour faire ce genre de choses, étant donné que [g|c]make est disponible nativement sur l'intégralité de mes serveurs.

Et oui, \ comme séparateur pour les espaces de nommage, c'est de la merde moche et emmerdant embétant à taper sur un clavier AZERTY, mais c'était apparament le meilleur choix, au bout d'un moment, il devient invisible, et il est donc inutile de polémiquer à nouveau à ce sujet.