[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
modx.pro
16
3 481
+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. Отлично получилось!
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          12