Dans le cadre de mon projet, j'ai le code suivant :
class foo
{
protected $methods = array();
public function __set($method, \closure $closure)
{
if (array_key_exists($method, $this->methods) === false)
{
throw new \logicException('Method \'' . $method . '\' does not exist');
}
}
}
Ainsi, si je veux affecter une fermeture à une méthode qui n'est pas connue de l'objet, une exception sera lancée.
Ce code est utilisé par un processus PHP, créé à l'aide de proc_open()
.
Ce processus doit communiquer le résultat de son exécution, représenter par un score
, à son processus père.
Pour cela, il écrit sur la sortie standard le résultat de la sérialisation de son score
, et la sortie standard est ensuite lue et interprétée par le processus père.
Or, ce score
stocke les éventuelles exceptions générées lors de l’exécution du processus.
Donc, si le code du processus fait appel à la méthode foo::__set()
avec un nom de méthode invalide qui génère une exception, cette dernière est stockée dans le score
.
Et lorsque PHP crée une exception, il stocke dans cette dernière la pile d’exécution qui a conduit à sa création.
Cette pile contient, entre autre chose, les arguments qui ont été passé aux différentes méthodes appelées.
Le score
contient donc dans mon cas une exception qui contient une fermeture, puisque c'est l'un des arguments de foo::__set()
.
Or, PHP refuse catégoriquement de sérialiser les fermetures.
Si vous essayez, vous obtiendrez le message suivant :
PHP Fatal error: Uncaught exception 'Exception' with message 'Serialization of 'Closure' is not allowed' in /usr/home/fch/tmp/closure.php:5
Du coup, dans ce cas, ma communication inter-processus ne peut plus fonctionner, vu que :
- la sérialisation du
score
entraîne la sérialisation de l'exception - la sérialisation de l'exception entraîne celle de la fermeture, via la pile d'exécution contenue dans l'exception
- Une exception est généré PHP et écrite sur la sortie standard, que le processus père n'est pas capable d'interpréter.
La solution à ce problème ?
J'ai bien pensé à utiliser l'interface serializable
au niveau de mon score
, mais la pile d'exécution contient un objet de la classe \closure
,
qui ne peut être redéfinie.
De plus, il n'est pas non plus possible de modifier la pile d'exécution d'une exception puisque cette dernière est en lecture seule et que la propriété correspondante est privée.
La seule solution consiste donc à ne pas utiliser d'exception si une fermeture est susceptible de se retrouver dans sa pile d'exécution et qu'elle est susceptible d'être sérialisée.
Autant dire que cette solution est inapplicable dans les faits, puisqu'il faudrait au minimum être voyant extralucide pour pouvoir dire comment sera utilisé dans le futur un code conçu maintenant.
Il est également possible d'utiliser trigger_error()
pour générer une erreur et non une exception, mais suivant le contexte, ce n'est pas forcément le comportement voulu.
La meilleure solution est donc que le fonctionnement de PHP soit modifié afin qu'il n'empêche plus la sérialisation d'objet contenant des fermetures.
10 réactions
1 De jpauli - 08/07/2010, 13:38
Même chose lorsque tu as une instance de PDO qui traine dans la stack, elles ne sont pas sérialisables.
2 De desfrenes - 08/07/2010, 15:03
jpauli > dans le cas d'une instance de PDO il y a une ressource (la connexion à la bdd), c'est ce qui doit empêcher la sérialisation et je le comprend bien, mais pour une closure ?
3 De metagoto - 08/07/2010, 16:40
Sérialiser les func anonymes est un problème ardu dont les éventuelles solutions ne font pas consensus. Bref, c'est mal barré.
4 De tight - 08/07/2010, 23:02
Jette un oeil à cet article : http://fabien.potencier.org/article...
L'idée est d'utiliser pour ton exception qui pose problème une classe qui étend Exception (forcément) et implémente Serializable dans le but de ne pas stocker la trace qui pose problème.
HTH
5 De fylefou - 09/07/2010, 07:01
de meme si tu affecte une closure a une propriete d'un objet (à la javascript par exemple) , tu ne peux peu pas l'executer telle quelle tu doit "extraire" de l'objet:
$obj->onEvent = function(){};
//ne marche pas
$obj->onEvent();
//marche
$clo = $obj->onEvent;
$clo();
6 De gdelamarre - 09/07/2010, 09:43
j'ai lu ton post un rapidement (pour ne pas dire de travers, je suis fort mal réveillé ;)), mais ne pourrais-tu pas simplement implémenter une méthode sleep() pour virer toute référence à tes closures avant la sérialisation, quitte à les remplacer par un identifiant qui te permettrait de les restaurer avec un wakeup() ?
7 De tight - 09/07/2010, 09:44
@fylefou
Tu peux utiliser __invoke() pour exécuter ta closure en attribut
$obj->onEvent->__invoke()
cf. http://blog.mageekbox.net/?post/201...!
8 De mageekguy - 09/07/2010, 10:11
@tight : Je confirme pour le __invoke(), qui est d'ailleurs ce que j'utilise dans ce cas de figure.
Sinon, la méthode de ton lien n'est pas applicable dans mon cas.
Il faudrait que je n'ai que des exceptions dérivant de mon type d'exception, qui implémenterait la solution proposée (à laquelle j'avais déjà pensé, d'où ma référence à serializable dans mon billet).
Or, ce n'est pas forcément le cas, je suis aussi amené à stocker dans mon score des exceptions venant soit de PHP lui même, soit d'un code tiers.
Et puis, en plus, la pile d'exécution est trop intéressante dans mon cas pour la sacrifier ainsi.
9 De mageekguy - 09/07/2010, 10:17
@gdelamarre : Oh le has been qui utilise __sleep() et __wakeup() :).
Trêve de plaisanterie, l'idée est bonne (mise en oeuvre avec l'interface serializable, évidement), si seulement je pouvais/voulais dériver mes exceptions, ce qui n'est pas le forcément le cas.
Ces dernières sont en effet soit mes exceptions, et dans ce cas, pas de problème si ce n'est que je réinvente la roue en réimplémentant les exceptions de la SPL, soit des exceptions générée par un code tiers ou PHP lui-même, et dans ce cas, je suis coincé.
Et comme il n'est pas possible de dériver les fermetures...
Pour l'instant, par facilité, j'ai choisi pour l'instant de stocker dans mon score les exceptions sous forme de chaîne de caractères via un appel à __toString().
J'ai longuement hésité entre cette solution et stocker uniquement les propriétés de l'exception en faisant le ménage parmi la pile au passage, mais si jamais j'ai une exception spécifique avec une ou plusieurs propriétés supplémentaires par rapport à l'exception de base, je perdrais de l'information.
Avec la solution que j'ai retenu, c'est de la responsabilité du développeur de l'exception de faire figurer les informations absolument nécessaire dans la chaîne renvoyée par __toString(), et non de la mienne :).
10 De jpauli - 09/07/2010, 14:57
Tout ca pour dire que les fonctions anonymes sont encore un peu jeunes dans le Core de PHP. Va falloir attendre pas mal de temps avant que tous les problèmes notés (dans ce sujet, mais aussi dans les commentaires) soient résolus, s'ils le sont un jour par le PHPGroup