[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, 13:53    Михаил Малых   
15    1217 +13


Комментарии ()

  1. Илья Уткин 01 сентября 2015, 14:40 # +3
    Классно. Может, создать пулл-реквест в modExtra Василия? Думаю, он примет. Удобно будет всем.
    1. Сергей Шлоков 01 сентября 2015, 21:21 # +2
      Василий уехал в отпуск. Пусть человек отдохнет.
      П.С. А ключик от modExtra он мне оставил.
    2. Сергей Шлоков 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.');
      }
      
      Тады вааще все автоматом работать будет. А то я постоянно забываю добавлять классы.
      1. Сергей Шлоков 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);
        	}
        }
        
        1. Михаил Малых 25 сентября 2015, 21:44 # 0
          Сергей. В вашем закоммиченом коде ошибка.
          Замените, пожалуйста unset($tableFields[$field)]); на unset($tableFields[array_search($field, $tableFields)]);
          И в аналогичном месте с индексами unset($indexes[$name]); на unset($indexes[array_search($name, $indexes)]);
          Массивы же теперь стали не ассоциативные. В вашем примере в списке филдов и индексов реально не происходит удаления, зато потом происходит реальное удаление оставшихся в списке филдов и индексов.
          Спасибо.
          1. Сергей Шлоков 25 сентября 2015, 21:49 # 0
            Михаил, обновитесь, уже исправлено как несколько дней.
        2. Василий Наумкин 02 сентября 2015, 08:22 # 0
          Отличный способ, я тоже к чему-то такому же пришел при работе над одним проектом. Только там нужно было держать в форме таблицы товаров при изменении плагинов miniShop2.

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

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

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