[modExtra] Update таблиц своего компонента

Добрый день!
Все мы знаем, что MODX прекрасен мощью решений с помощью компонентов. Я, например, всегда отдаю заказчику сайт в виде компонента, содержащего нужные элементы, зависимости от других компонентов, инициализацию нужных опций и т.д. В этом мне всегда помогал modExtra.
Но вот в один прекрасный момент я задумался, как сделать пользовательские таблицы в своём компоненте таким образом, чтобы при апдейте компонента они были способны расширяться, да и вообще поддерживаться в актуальном состоянии.
Итак задача:
  • У нас есть файл схемы mycomponent.mysql.schema.xml, который поставляется вместе с компонентом (или его новой версией) и содержит актуальную информацию о структуре пользовательских таблиц.
  • У нас есть БД в которой может не быть наших таблиц (установка с нуля), а могут быть (как правильной, так и неправильной структуры).
Цель: добиться идентичности схемы и реальных таблиц для нормальной работы компонента.

В этом нам помогут ресолверы таблиц, заготовка для которых есть в modExtra и хранится она тут: _build/resolvers/resolve.tables.php. Мы видим, что на каждую таблицу вызывается метод
$manager->createObjectContainer($tmp);
Если залезть в его потроха, то можно увидеть, что он находит (соответствующую пользовательскому классу) таблицу в БД и, если её не существует, создаёт её, в противном случае ничего не делает. Что ж, неплохое начало. Но на этом решение modExtra заканчивается.
Кроме этого можно посмотреть на решение Василия в Tickets, где он «руками» пытается недостающие поля таблиц добавить, а лишние удалить.
Порывшись в документации и коде MODX понял, что нас полностью бы устроило добавление строчки
$manager->alterObjectContainer($tmp);
, но у этой функции есть только прототип, а реализации для mySql пока нет.
Хочу предложить своё решение, которое должно подойти в подавляющем большинстве случаев:
foreach ($objects as $tmp) {
    $manager->createObjectContainer($tmp);

    $exist = array();
    $c = $modx->prepare("SHOW COLUMNS IN {$modx->getTableName($tmp)}");
    $c->execute();
    while ($cl = $c->fetch(PDO::FETCH_ASSOC)) {
        $exist[$cl['Field']] = $cl['Field'];
    }

    $fieldMeta = $manager->xpdo->getFieldMeta($tmp, true);
    while (list($key, $meta)= each($fieldMeta)) {
        if (array_key_exists($key, $exist)) {
            unset($exist[$key]);
            $manager->alterField($tmp, $key);
        } else {
            $manager->addField($tmp, $key);
        }
    }
    foreach ($exist as $key) {
        $manager->removeField($tmp, $key);
    }
}
break;
Несколько комментариев:
  • Сначала мы запоминаем название всех существующих колонок.
  • Потом пробегаемся по схеме и выполняем одно из действий: добавить колонку, удалить или обновить.
  • Если мы делаем апдейт, а схема не изменилась, то всегда будет вызываться alterField, который проверит изменился ли тип поля и исправит его ТОЛЬКО в том случае, если это необходимо.
  • Неважно, есть в таблице данные или нет, скрипт правильно отработает и ничего не пострадает.
Используя это, мы можем при разработке компонента всю структуру таблиц задавать только в файле схемы (что очень удобно!), а ресолвер уже сам позаботится о приведении БД в актуальное состояние.
Решение пусяковое, но оно делает разработку компонента несколько комфортнее.
Спасибо за внимание.
Михаил Малых
01 сентября 2015, 10:53
15
1 455
+13

Комментарии: 12

Илья Уткин
01 сентября 2015, 14:40
+3
Классно. Может, создать пулл-реквест в modExtra Василия? Думаю, он примет. Удобно будет всем.
    Сергей Шлоков
    01 сентября 2015, 21:21
    +2
    Василий уехал в отпуск. Пусть человек отдохнет.
    П.С. А ключик от modExtra он мне оставил.
Сергей Шлоков
01 сентября 2015, 20:34
+2
Ну раз уж пошла такая пьянка, то я бы тогда добавил еще и это
$schemaFile = MODX_CORE_PATH . 'components/modextra/model/schema/modextra.mysql.schema.xml';
$objects = array();
if (is_file($schemaFile)) {
	$schema = new SimpleXMLElement($schemaFile, 0, true);
	if (isset($schema->object)) {
		foreach ($schema->object as $object) {
			$objects[] = (string)$object['class'];
		}
	}
	unset($schema);
} else {
	$modx->log(modX::LOG_LEVEL_ERROR, 'Could not get classes from schema file.');
}
Тады вааще все автоматом работать будет. А то я постоянно забываю добавлять классы.
Сергей Шлоков
01 сентября 2015, 21:18
+2
А вот так еще и индексы добавляем автоматом, чтоб уж совсем голова не болела.
$schemaFile = MODX_CORE_PATH . 'components/modextra/model/schema/modextra.mysql.schema.xml';
if (is_file($schemaFile)) {
	$schema = new SimpleXMLElement($schemaFile, 0, true);
	if (isset($schema->object)) {
		foreach ($schema->object as $object) {
			$objName = (string)$object['class'];
			$objects[] = $objName;
			// Indexes
			foreach ($object->index as $index) {
				$indexes[$objName][] = (string) $index['name'];
			}
		}
	}
	unset($schema);
} else {
	$modx->log(modX::LOG_LEVEL_ERROR, 'Could not get classes from schema file.');
}
//Работаем с таблицами
foreach ($objects as $tmp) {
   $manager->createObjectContainer($tmp);
   ...
}
//Работаем с индексами
foreach ($ClassIndexes as $class=>$indexes) {
	foreach($indexes as $index) {
		$manager->addIndex($class,$index);
	}
}
    Михаил Малых
    25 сентября 2015, 21:44
    0
    Сергей. В вашем закоммиченом коде ошибка.
    Замените, пожалуйста unset($tableFields[$field)]); на unset($tableFields[array_search($field, $tableFields)]);
    И в аналогичном месте с индексами unset($indexes[$name]); на unset($indexes[array_search($name, $indexes)]);
    Массивы же теперь стали не ассоциативные. В вашем примере в списке филдов и индексов реально не происходит удаления, зато потом происходит реальное удаление оставшихся в списке филдов и индексов.
    Спасибо.
      Сергей Шлоков
      25 сентября 2015, 21:49
      0
      Михаил, обновитесь, уже исправлено как несколько дней.
Василий Наумкин
02 сентября 2015, 08:22
0
Отличный способ, я тоже к чему-то такому же пришел при работе над одним проектом. Только там нужно было держать в форме таблицы товаров при изменении плагинов miniShop2.

Думаю, можно это и добавить в modExtra, как образец. Вместе с предложениями Сергея по индексам.
    Сергей Шлоков
    02 сентября 2015, 09:55
    +1
    Позволил себе добавить это решение на Github.
    Теперь в большинстве случаев вообще не нужно лезть в этот ресолвер. Спасибо Михаилу.
    П.С. Также я добавил код удаления таблиц при деинсталяции. Он закомментирован. Если вдруг кому-то нужно будет удалять таблицы, нужно просто раскомментировать.
      Василий Наумкин
      02 сентября 2015, 09:58
      +1
      Вот с удалением таблиц вопрос сложный. Неоднократно видел, что при возникновении проблем, люди удаляют и заново устанавливают компонент.

      С этим изменением в подобных случаях будут удалены и все данные в таблицах, чему многие не обрадуются.

      А, туплю, он же закомментирован по умолчанию. Тогда всё ок, вопросов нет.
        Сергей Шлоков
        02 сентября 2015, 10:02
        +1
        Поэтому он и закомментирован. При разработке и тестировании пакета периодически приходится удалять вручную таблицы. Для чистоты эксперимента, так сказать.
        А разработчик должен понимать всю ответственность такого решения.
      Михаил Малых
      02 сентября 2015, 12:20
      +1
      Спасибо за доработку и коммит в modExtra. Отлично получилось!