На Хабре уже было несколько статей с примерами использования замыканий в PHP. Некоторые из них были достаточно абстрактными, некоторые нет. Я приведу ещё один способ применения замыканий в реальных условиях.
При добавлении нового функционала в один проект на PHP без фреймворка, возникла необходимость использования транзакций (используется MySQL c InnoDB и PHP 5.4 с MYSQLi).
В проекте по умолчанию autocommit
установлен в true
. Выключить его для всего проекта нельзя. Соответственно первой мыслью было перед выполнением SQL-запроса отключать autocommit
, а после всех действий (плюс commit
или rollback
в конце), включать autocommit
снова.
Но такой подход сразу оказался несостоятельным, так как обычно необходимо выполнять последовательно несколько методов, в которых делаются запросы и, если в каком-то из методов возникает исключение, делать rollback
. Если же делать commit
в каждом методе, то изменения будут фиксироваться раньше, чем выполнятся все запросы.
Другой вариант — отключать и включать autocommit
после выполнения каждой связанной группы методов. Условный код (действие происходит в классе):
public function save()
{
$result = $this->db->update(...);
//ошибка может быть не только из-за неверного запроса, но и в процессе валидации и пр.
if (!$result) throw new Exception('Error while saving');
}
public function append_log()
{
$result = $this->db->insert(...);
if (!$result) throw new Exception('Error while append');
}
public function add()
{
$this->db->autocommit(false);
try {
$this->save();
$this->append_log();
$this->db->commit();
} catch (Exception $e) {
$this->db->rollback();
}
$this->db->autocommit(true);
}
Но тут возникают две проблемы:
- Писать такое в каждом методе не очень хочется
- Что, если в каком-то из методов (
save()
илиappend_log()
) будет также исполняться несколько последовательных запросов, которые надо объединить в транзакцию? Тогда придётся определять, отключали или нетautocommit
, и выполнятьcommit
в зависимости от этого, так как если выполнитьcommit
, родительские изменения тоже будут сохранены.
Нужно сделать так, чтобы код проверки и фиксирования изменений выполнялся вокруг метода без нашего участия.
public function transaction(callable $block)
{
$exception = null;
if ($need_to_off = $this->isAutocommitOn())
$this->mysqli->autocommit(false);
try {
$block();
} catch (Exception $e) {
$exception = $e;
}
if ($need_to_off)
{
if ($exception == null) {
$this->db->mysqli->commit();
} else {
$this->db->mysqli->rollback();
}
}
if ($exception) throw $exception;
}
public function isAutocommitOn()
{
if ($result = $this->db->mysqli->query("SELECT @@autocommit")) {
$row = $result->fetch_row();
$result->free();
}
return isset($row[0]) && $row[0] == 1;
}
Мы посылаем методу transaction()
наш код внутри анонимной функции. Если autocommit
включен, transaction
его отключает, затем выполняет анонимную функцию. В зависимости от результата делает commit
или rollback
, а затем заново включает autocommit
. Если же autocommit
уже выключен, то просто выполняется анонимная функция — об autocommit заботятся где-то в другом месте.
Пример использования:
public function save_all()
{
$this->transaction(function(){
$this->save();
$this->append_log();
});
}
P.S.: $this
в замыканиях можно использовать, начиная с PHP версии 5.4
Автор: skat_sakh