· 7 min read

Castor, le task runner des devs PHP ?

Découvrons Castor, un task runner pour les développeurs PHP. Il ce positionne comme un outil simple et efficace pour gérer les tâches récurrentes de vos projets, et comme alternative à des outils comme Makefile ou Taskfile.

Découvrons Castor, un task runner pour les développeurs PHP. Il ce positionne comme un outil simple et efficace pour gérer les tâches récurrentes de vos projets, et comme alternative à des outils comme Makefile ou Taskfile.

Un task runner ?

Un task runner est un outil qui permet de gérer et centraliser les tâches récurrentes d’un projet. Cela peut être des tâches de compilation, de tests, de déploiement, etc.

C’est un outil hyper intéressant pour les équipes de développement. Il permet de gagner du temps, de standardiser les commandes, de les documenter, etc. C’est un must-have pour tous projet sérieux (et c’est une aide précieuse pour l’onboarding des nouveaux membres de l’équipe).

J’en avais déjà parlé dans une précédente vidéo, en abordant l’interet d’un fichier Makefile pour les projets Symfony, et personnellement, j’utilise une variation de ce fichier dans l’ensemble des projets sur lesquels j’interviens. (Le dépot est disponible ici)

Castor ?

Alors qu’il existe déjà des outils comme Makefile ou Taskfile, pourquoi en créer un autre ? Pour améliorer la DX (Developer Experience) !

Castor est ecrit en PHP et il permet de bénéficier de l’ecosystème PHP pour nous simplifier la vie. Il propose de nombreuses fonctionnalités telle que que l’analyse transparente des arguments, le support de l’autocomplétion, et une série de fonctions intégrées pour simplifier le traitement des tâches courantes (exécution de commandes, parallélisation, logs, etc).

Et puis, si vous êtes dev PHP pourquoi apprendre un autre langage pour ça ? Autant rester dans votre zone de confort 🤣

Je profite de cet article pour remercier JoliCode pour le travail effectué sur cet outil (et tant d’autres).

Installation

Castos est dispo pour Linux, MacOS et Windows. (La suite de l’article sera basée sur une installation Linux)

curl "https://github.com/jolicode/castor/releases/latest/download/castor.linux-amd64.phar" -Lfso $HOME/.local/bin/castor && \
    chmod u+x $HOME/.local/bin/castor && \
    castor --version || \
    (echo "Could not install castor. Is the target directory writeable?" && (exit 1))

N’hésitez pas à consulter la documentation officielle pour plus d’informations.

Initier un fichier pour Castor

Pour initier un fichier Castor, il suffit d’éxécuter la commande suivante :

castor

L’outil va alors vous proposer de créer un fichier castor.php avec un exemple de tâche.

<?php

use Castor\Attribute\AsTask;

use function Castor\io;
use function Castor\capture;

#[AsTask(description: 'Welcome to Castor!')]
function hello(): void
{
    $currentUser = capture('whoami');

    io()->title(sprintf('Hello %s!', $currentUser));
}

En parrallèle, Castor va également créer un fichier .castor.stub.php à la racine de votre projet. Ce fichier permet de définir des variables globales pour votre projet.

Ce fichier contient certaines définitions de classes et méthodes de Castor et de certaines de ses dépendances.

Cela s’avère utile lorsque vous installez Castor depuis un PHAR (c’est le cas dans notre exemple). Sans ce fichier, votre IDE signalerait qu’il ne comprend pas certaines classes et ne fournirait pas d’autocomplétion dans vos fichiers castor.

La doc nous invite à ajouter ce fichier à notre .gitignore pour éviter de le partager avec les autres membres de l’équipe.

Exécuter une tâche

Pour exécuter une tâche, il suffit de lancer la commande suivante :

castor hello

Ce qui devrait vous afficher un message de bienvenue de ce type :

> castor hello       

Hello yoan!
===========

Testons les possibilités de Castor

Les fonctions IO

Castor propose une série de fonctions pour gérer les entrées/sorties. Par exemple, io()->title() permet d’afficher un titre, io()->section() pour afficher une section, io()->text() pour afficher du texte, etc.

En arrière plan, Castor utilise la librairie Symfony Console, si vous êtes familier avec cette librairie, vous ne serez pas dépaysé.

#[AsTask(description: 'Test IO functions')]
function testIo(): void
{
    io()->title('Title');
    io()->section('Section');
    io()->text('Text');
    io()->success('Success');
    io()->error('Error');

    // Bar de progression
    io()->progressStart(100);
    for ($i = 0; $i < 100; ++$i) {
        io()->progressAdvance();
        usleep(1000);
    }
    io()->progressFinish();
}

Passer des arguments à une tâche

Il est possible de passer des arguments à une tâche. Par exemple, pour passer un argument nameet age à une tâche test-input :

#[AsTask(description: 'Test Input functions')]
function testInput(
    string $name,
    int $age
): void
{
    $name = input()->getArgument('name');
    $age = input()->getArgument('age');

    io()->text(sprintf('Salut %s, tu as %d ans.', $name, $age));
}

Et pour lancer la tâche :

castor test-input yoan 39

Exécuter des commandes externes

  • Exécuter simplement une commande

Il est possible d’exécuter des commandes externes (c’est même le coeur de cible d’un task runner). Par exemple, pour exécuter la commande ls -la :

// ...
use function Castor\run;
// ...

#[AsTask(description: 'Test command execution')]
function testCommand(): void
{
    run('ls -la');
}
  • Gérer la bonne exécution de la commande

Il est possible de gérer la bonne exécution de la commande. Par exemple, pour exécuter la commande ls -la et gérer la sortie :

// ...
use function Castor\run;
// ...

#[AsTask(description: 'Test command execution')]
function testCommand2(): void
{
    $result = run('ls -la');

    if ($result->isSuccessful()) {
        io()->success('Command executed successfully');
    } else {
        io()->error('Command failed');
    }
}
  • Gérer le repertoire de travail

Il est possible de gérer le répertoire de travail. Par exemple, pour exécuter la commande ls -la dans le répertoire /tmp :

#[AsTask(description: 'Test command execution')]
function testCommand3(): void
{
    run('ls -la', path: '/tmp');
}

Parallélisation

Castor propose également des fonctions pour exécuter des tâches en parallèle. Par exemple, pour exécuter 5 commandes en parallèle :

// ...
use function Castor\parallel;
// ...
#[AsTask(description: 'Test parallel execution')]
function testParallel(): void
{
    parallel(
        fn() => run('sleep 10 && echo "Lorem"'),
        fn() => run('sleep 5 && echo "Ipsum"'),
        fn() => run('sleep 1 && echo "Dolor"'),
        fn() => run('sleep 8 && echo "Sit"'),
        fn() => run('sleep 1 && echo "World"')
    );
}

Logs

Castor propose également des fonctions pour gérer les logs. Par exemple, pour logger un message :

// ...
use function Castor\log;
// ...
#[AsTask(description: 'Test logs')]
function testLogs(): void
{
    log('This is an info message', 'info');
    log('This is an notice message', 'notice');
    log('This is an warning message', 'warning');
    log('This is an error message', 'error');
}

Si on exécute la tâche testLogs, on obtient le résultat suivant :

> castor test-log     
14:12:54 WARNING   [castor] This is an warning message
14:12:54 ERROR     [castor] This is an error message

Pour voir plus de logs, il faut lancer la commande avec des flags pour préciser le niveau de log. Par exemple :

> castor test-log # Pour afficher les logs de niveau "warning" et supérieur
> castor test-log -v # Pour afficher les logs de niveau "notice" et supérieur
> castor test-log -vv # Pour afficher les logs de niveau "info" et supérieur

Notifications

Castor propose également des fonctions pour gérer les notifications. Par exemple, pour envoyer une notification :

// ...
use function Castor\notify;
// ...

#[AsTask(description: 'Test notify')]
function testNotify(): void
{
    notify('This is a notification');
}

Pratique pour être notifié lorsqu’une tâche (longue) est terminée !

Compiler votre projet ?

Ok, c’est bien beau, mais on ne pourrait pas compiler notre CLI en un seul fichier ? Evidemment, les développeurs de Castor ont pensé à tout !

Packager votre projet en un seul fichier .phar

Le fichier .phar (PHP Archive) est un format de fichier pour PHP qui permet de packager un projet en un seul fichier. Composer est un exemple de projet qui utilise ce format.

Il est possible de packager votre projet en un seul fichier .phar.

Pour cela, il faut au préalable installer Castor avec composer :

composer require jolicode/castor

Il faut également installer au préalable Box, un outil pour packager des projets PHP en un seul fichier .phar :

composer global require humbug/box

Ensuite, il suffit de lancer la commande suivante pour packager votre projet :

vendor/bin/castor repack

Et voilà, vous avez un fichier my-app.linux.phar qui contient votre projet est prêt à être distribué !

Vous pouvez alors lancer votre projet avec la commande suivante :

php my-app.linux.phar hello

Compiler le projet en un binaire ?

Ne nous arrêtons pas en si bon chemin, il est également possible de compiler votre projet en un binaire. C’est pas top ça ? Et encore une fois ça va être super simple !

vendor/bin/castor compile my-app.linux.phar

La commande va alors générer un fichier my-app.linux.x86_64 qui est un binaire prêt à être distribué.

Testons le binaire :

chmod +x my-app.linux.x86_64
./my-app.linux.x86_64

Et voilà, vous avez votre projet qui s’exécute en un binaire ! C’est pas beau ça ?

Conclusion

Castor est un outil hyper intéressant pour les développeurs et développeuses PHP. Il ce positionne comme un outil simple, efficace et extensible (nous n’avons que survolé les possibilités de Castor dans cet article).

Il est un excellent choix pour gérer les commandes récurrentes de vos projets, et comme alternative à des outils comme Makefile ou Taskfile.

La possibilité de packager votre projet en un seul fichier .phar ou en un binaire est un gros plus pour la distribution de vos projets, et je pense personnellement l’utiliser dans certains de mes projets (Aussi bien en local que pour des tâches de CI/CD).

Back to Blog