La raison pour laquelle je dis que les transactions n'appartiennent pas à la couche modèle est essentiellement la suivante :
Les modèles peuvent appeler des méthodes dans d'autres modèles.
Si un modèle essaie de démarrer une transaction, mais qu'il ne sait pas si son appelant a déjà démarré une transaction, alors le modèle doit conditionnellement démarrer une transaction, comme indiqué dans l'exemple de code dans @Bubba's answer . Les méthodes du modèle doivent accepter un indicateur afin que l'appelant puisse lui dire s'il est autorisé à démarrer sa propre transaction ou non. Ou bien le modèle doit avoir la capacité d'interroger l'état "en transaction" de son appelant.
public function setPrivacy($privacy, $caller){
if (! $caller->isInTransaction() ) $this->beginTransaction();
$this->privacy = $privacy;
// ...action code..
if (! $caller->isInTransaction() ) $this->commit();
}
Et si l'appelant n'est pas un objet ? En PHP, il peut s'agir d'une méthode statique ou simplement d'un code non orienté objet. Cela devient très désordonné et conduit à beaucoup de code répété dans les modèles.
C'est aussi un exemple de Couplage de contrôle , ce qui est considéré comme mauvais parce que l'appelant doit savoir quelque chose sur le fonctionnement interne de l'objet appelé. Par exemple, certains des méthodes de votre modèle peuvent avoir un paramètre $ transactionnel, mais d'autres méthodes peuvent ne pas avoir ce paramètre. Comment l'appelant est-il censé savoir quand le paramètre est important ?
// I need to override method's attempt to commit
$video->setPrivacy($privacy, false);
// But I have no idea if this method might attempt to commit
$video->setFormat($format);
L'autre solution que j'ai vue suggérée (ou même implémentée dans certains frameworks comme Propel) est de faire beginTransaction()
et commit()
no-ops lorsque le DBAL sait qu'il est déjà dans une transaction. Mais cela peut entraîner des anomalies si votre modèle essaie de s'engager et constate qu'il ne s'engage pas vraiment. Ou essaie de revenir en arrière et cette demande est ignorée. J'ai déjà écrit sur ces anomalies.
Le compromis que j'ai suggéré est que les modèles ne connaissent pas les transactions . Le modèle ne sait pas si sa demande à setPrivacy()
est quelque chose qu'il doit valider immédiatement ou fait-il partie d'une image plus large, d'une série de modifications plus complexes qui impliquent plusieurs modèles et ne doivent que être engagé si tous ces changements réussissent. C'est le but des transactions.
Donc, si les modèles ne savent pas s'ils peuvent ou doivent commencer et valider leur propre transaction, alors qui le sait ? GRASP inclut un modèle de contrôleur qui est une classe non-UI pour un cas d'utilisation, et il lui est attribué la responsabilité de créer et de contrôler toutes les pièces pour accomplir ce cas d'utilisation. Les contrôleurs sont au courant des transactions car c'est là que toutes les informations sont accessibles pour savoir si le cas d'utilisation complet est complexe et nécessite plusieurs modifications à effectuer dans les modèles, dans une transaction (ou peut-être dans plusieurs transactions).
L'exemple sur lequel j'ai déjà écrit, c'est de démarrer une transaction dans le beforeAction()
méthode d'un contrôleur MVC et validez-la dans afterAction()
méthode, est une simplification . Le contrôleur doit être libre de démarrer et de valider autant de transactions qu'il en a logiquement besoin pour terminer l'action en cours. Ou parfois, le contrôleur peut s'abstenir d'un contrôle explicite des transactions et permettre aux modèles de valider automatiquement chaque modification.
Mais le fait est que les informations sur les transactions nécessaires sont quelque chose que les modèles ne savent pas - ils doivent être informés (sous la forme d'un paramètre $ transactionnel) ou bien les interroger auprès de leur appelant, ce qui devrait de toute façon déléguer la question jusqu'à l'action du Contrôleur.
Vous pouvez également créer une Couche de service de classes qui savent chacune comment exécuter des cas d'utilisation aussi complexes et s'il faut inclure toutes les modifications dans une seule transaction. De cette façon, vous évitez beaucoup de code répété. Mais il n'est pas courant que les applications PHP incluent une couche de service distincte ; l'action du contrôleur coïncide généralement avec une couche de service.