Ещё один пример использования замыканий в PHP

в 7:00, , рубрики: php, Веб-разработка, замыкания, транзакции, метки: , ,

На Хабре уже было несколько статей с примерами использования замыканий в 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);
}

Но тут возникают две проблемы:

  1. Писать такое в каждом методе не очень хочется
  2. Что, если в каком-то из методов (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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js