La première version de mon script ne pouvait donc pas fonctionner :

<?php

$databaseClient->lockTable();
$databaseClient->extractData();

$continue = false;

while ($continue !== true) {
	$continue = (trim(fgets(STDIN) === ‘continue’);
}

// After this point, the database connection may be dead

$databaseClient->updateData();
$databaseClient->unlockTable();

?>

J’ai donc cherché une solution afin que la connexion entre ma base de données et mon script soit maintenue active tant que le script était en attente d’une réponse de l’utilisateur.

Or, il se trouve que l’entrée standard, matérialisée par la constante STDIN lorsque PHP est exécuté en ligne de commande, est un flux.

Et il se trouve que PHP dispose nativement d’une fonction permettant à un script d’attendre qu’un ou plusieurs flux soient disponibles pour l’écriture ou la lecture pendant un temps défini, à savoir stream_select().

Il m’a donc suffi de définir dans un tableau le seul et unique flux que je désirais lire, soit STDIN, et de le lui passer comme premier argument.

J’ai également défini deux autres variables que j’ai initialisées à NULL car les trois premiers arguments de stream_select() doivent être des références vers des tableaux, sous peine d’obtenir un avertissement de la part de PHP lors de l’exécution du script.

Enfin, j’ai défini via son quatrième argument le temps que stream_select() doit attendre avant de rendre la main au script, à savoir dans mon cas 5 secondes.

Une fois cela effectué, il n’y a plus qu’à faire envoyer au script une requête anodine du style « SELECT 1 » à la base de données ou bien de poursuivre la mise à jour des données en base en fonction de la valeur de retour de stream_select().

Cette dernière renvoie en effet 0 lorsqu’aucun des flux qui lui sont passés en argument ne sont modifiés ou bien le nombre de flux modifiés s’ils existent.

La seconde version de mon script était donc la suivante :

<?php

$databaseClient->lockTable();
$databaseClient->extractData();

$continue = false;

$read = array(STDIN);
$write = null;
$except = null;

while ($continue !== true) {
	if (stream_select($read, $write, $except, 5) == 0) {
		$databaseClient->query(‘SELECT 1’);
	$read = array(STDIN);
	} else {
		$continue = (trim(fgets(STDIN) === ‘continue’);
	}
}

// After this point, the database connection is always OK

$databaseClient->updateData();
$databaseClient->unlockTable();

?>

Sauf qu’elle ne marchait toujours pas, car par défaut, STDIN est un flux bloquant.

En clair, tant que l’utilisateur n’a pas saisi le caractère \n, il ne rend pas la main et bloque indéfiniment l’exécution du script, indépendamment de la valeur du quatrième argument de stream_select().

Pour remédier à cela, il faut donc indiquer à PHP que ce flux ne doit pas être bloquant, à l’aide de stream_set_blocking(), et bien évidemment, il ne faut pas oublier de le rendre à nouveau bloquant une fois que cela n’est plus nécessaire, sous peine d’avoir une surprise lors de l’exécution de la suite du script s’il est à nouveau utilisé.

La version définitive du script est donc la suivante :

<?php

$databaseClient->lockTable();
$databaseClient->extractData();

$continue = false;

$read = array(STDIN);
$write = null;
$except = null;

stream_set_blocking(STDIN, 0);

while ($continue !== true) {
	if (stream_select($read, $write, $except, 5) == 0) {
		$databaseClient->query(‘SELECT 1’);
		$read = array(STDIN);
	} else {
		$continue = (trim(fgets(STDIN) === ‘continue’);
	}
}

// After this point, the database connection is always OK

stream_set_blocking(STDIN, 1);

$databaseClient->updateData();
$databaseClient->unlockTable();

?>

Évidemment, ce mécanisme peut être mis en œuvre pour faire autre chose qu’entretenir une activité sur une connexion de base de données, et avec un peu d’ingéniosité, il peut même être rendu totalement générique :

<?php

function waitContinue($message, $timeout, closure $timeoutCallback) {
	$read = array(STDIN);
	$write = null;
	$except = null;

	$answer = '';

	fwrite(STDOUT, trim($message) . ': ');

	stream_set_blocking(STDIN, 0);

	while ($answer != 'continue') {
		if (stream_select($read, $write, $except, $timeout) != 0) {
			$answer = trim(fgets(STDIN));
		} else {
			$timeoutCallback();
			$read = array(STDIN);
		}
	}

	stream_set_blocking(STDIN, 1);
}

?>

Cette fonction reçoit ainsi respectivement comme premier, second et troisième argument le message devant être affiché à l’utilisateur, la valeur du délai d’attente et une fermeture lexicale permettant de définir le comportement de la fonction à l’expiration du délai d’attente.