magento-dobry-upgrade-script

Magento – jak napisać dobry upgrade script

Dzisiaj powrócę do tematy kolegi Mariusza, który poruszał temat uprgradeów we wpisie „Upgrade i install script modułu Magento„. Ja od siebie dodam kilka ciekawych przykładów na wykorzystanie i objaśnię jak napisać dobry upgrade script.

Dodawanie nowych tabel/kolum

Nie będę omawiał jak wdrożyć do modułu install/upgrade-script – można to znaleźć bez problemu w internecie. Zacznę od przykładu (bardzo) złego install-scriptu.

<?php
$installer = $this;
$installer->;startSetup();

$sql=<<<SQLTEXT
    CREATE TABLE `nowa_tabela` (
      `nt_id` int(11) NOT NULL AUTO_INCREMENT,
      `wartosc1` int(11) NULL DEFAULT NULL,
      `wartosc2` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`nt_id`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=latin1
SQLTEXT;
$installer->run($sql);

Co stanie się po puszczeniu takiego skryptu? Możliwości jest kilka. Możemy mieć bardzo podstawową konfigurację Magento i wszystko pójdzie po naszej myśli. Tabela zostanie dodana. Możemy jednak używać w naszej instalacji magento prefixu do nazw tabel (nie spotkałem się produkcyjnie z takim rozwiązaniem osobiście – jeżeli macie jakieś przykłady z życia wzięte to piszcie w komentarzach). Wtedy tabel oczywiście zostanie dodana zgodnie z naszym kodem SQL, ale już nasz moduł (a właściwie modele w naszym module) nie będą w stanie z tej tabeli skorzystać (bo będą szukały tabeli prefix_nowa_tabela). Co możemy zrobić aby zapobiec takiemu scenariuszowi?

<?php
$installer = $this;
$installer->startSetup();

$sql = ‘
    CREATE TABLE '.$this->getTable('moj_modul/nowa_tabela').' (
      `nt_id` int(11) NOT NULL AUTO_INCREMENT,
      `wartosc1` int(11) NULL DEFAULT NULL,
      `wartosc2` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`nt_id`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=latin1’;
$installer->run($sql);

Wygląda to już trochę lepiej, bo nazwa tabeli będzie brana z mechanizmów magento i dlatego zostanie poprawnie dodana. Możemy jednak pójść nieco dalej w tych modyfikacjach.

<?php
$installer = $this;
$installer->startSetup();

$table = $installer->getConnection()
        ->newTable($installer->getTable('moj_modul/nowa_tabela'))
        ->addColumn('nt_id', Varien_Db_Ddl_Table::TYPE_INTEGER, 11, array(
            'identity'  => true,
            'unsigned'  => false,
            'nullable'  => false,
            'primary'   => true,
        ), 'klucz nowej tabeli')
        ->addColumn('wartosc1', Varien_Db_Ddl_Table::TYPE_INTEGER, 11, array(
            'unsigned' => false,
            'nullable' => true,
        ), ‘Wartosc 1')
        ->addColumn('wartosc2', Varien_Db_Ddl_Table::TYPE_INTEGER, 11, array(
            'unsigned' => false,
            'nullable' => true,
        ), 'Wartosc 2')


$installer->getConnection()->createTable($table);

Przyjrzyjmy się co teraz stało się z naszą nową tabelą. Oczywiście tak jak ostatnio mamy poprawną obsługę nazw tabel. Dodatkowo nie dodajemy wprost kodu SQL tylko posługujemy się warstwą abstrakcji, która może nawet obsłużyć nam różne bazy danych (np. MySQL i PostgreSQL). Podobnie postępujemy w przypadku dodawania kolumn do istniejących tabel.

<?php


$installer = $this;
$installer->startSetup();

$installer->getConnection()
    ->addColumn($installer->getTable('moj_modul/nowa_tabela'), ‘wartosc3’, array(
        'type'      => Varien_Db_Ddl_Table::TYPE_VARCHAR,
        'nullable'  => true,
        'comment'   => 'Wartość 3',
        'default'   => null
    ));

Co jednak się stanie gdy taka tabela (albo kolumna) będzie już w bazie danych? Można się zastanowić czy w ogóle taki przypadek powinien się zdarzyć.

Jeden przykład to deploy na produkcji bez downtime. Gdy mamy kilku userów na stronie, pierwszy odpala nam stronę (i tym samym install/upgrade-script), ale zanim nasz system zakończy upgrade i podniesie wersję modułu to już drugi użytkownik odpala upgrade script i mamy error użytkownikowi na twarz. Przy dodawaniu nowej tabeli czas tej aktualizacji będzie krótki i systemy z mniejszym ruchem mogę na tym nie ucierpieć. Przy dodawaniu jednak kolumny do istniejącej (i być może bardzo dużej) tabeli możemy spowodować error dla wielu użytkowników.

Kolejnym przykładem może być środowisko testowe. Przykładowo testujemy nasz moduł na środowisku testowym po czy aktualizujemy sobie bazę testową danymi produkcyjnymi. Jeżeli robimy to standardowym mysqldump + mysql to nowo dodana tabela na środowisku testowym nie będzie usunięta, ale wersja naszego modułu będzie wzięta ze środowiska produkcyjnego i dlatego zostanie odpalony install/upgrade-script.

Jest i na to rada.

if (!$installer->getConnection()->isTableExists( $installer->getTable('moj_modul/nowa_tabela'))) {
$installer->getConnection()->createTable($table);
}

Przed stworzeniem tabeli patrzymy czy faktycznie potrzebujemy tę tabelę stworzyć. Oczywiście można też przenosić bazę czyszcząc uprzednio bazę testową – też w takim wypadku pozbędziemy się problemów z dublami tabel.

Dodawanie kluczy obcych

Podobne błędy jak przy dodawaniu tabel czy kolumn możemy popełnić przy dodawaniu kluczy obcych. Poniżej przykład poprawnego dodania klucza obcego w install/upgrade-script

$table->addForeignKey(
    $installer->getFkName(
        ‘moj_modul/nowa_tabela',
        ‘wartosc1’,
        'customer/entity',
        'entity_id'
    ),
    ‘wartosc1’,
    $installer->getTable('customer/entity'),
    'entity_id',
    Varien_Db_Ddl_Table::ACTION_CASCADE,
    Varien_Db_Ddl_Table::ACTION_CASCADE
);

W taki sposób mamy dodany klucz obcy zgodnie z magento way.

Dodawanie atrybutów EAV

Dodanie atrybutów EAV mogłoby być też wykonane poprzez serię poleceń SQL, ale z tak skrajnym przypadkiem na szczęście się nie spotkałem. Nowe atrybuty dodajemy w taki sposób:

<?php
$setup = Mage::getModel('customer/entity_setup', 'core_setup');
$setup->addAttribute('customer', 'nowy_atrybut_klienta', array(
    'type' => 'int',
    'input' => 'text',
    'label' => 'Nowy atrybut klienta',
    'global' => 1,
    'backend'  => '',
    'source'   => '',
    'visible'  => true,
    'required' => false,
    'default' => '',
    'unique'     => false,
    'note'       => 'Pomocny atrybut w formularzach'
));

$attribute   = Mage::getSingleton(‘eav/config’)->getAttribute(‘customer’, 'nowy_atrybut_klienta');

$used_in_forms=array();
$used_in_forms[]='adminhtml_customer';
$used_in_forms[]='checkout_register';
$used_in_forms[]='customer_account_create';
$used_in_forms[]='customer_account_edit';
$used_in_forms[]='adminhtml_checkout';
$attribute->setData('used_in_forms', $used_in_forms)
          ->setData('is_used_for_customer_segment', true)
          ->setData('is_system', 0)
          ->setData('is_user_defined', 1)
          ->setData('is_visible', 1)
          ->setData('sort_order', 101);

Pierwsza część (czyli dodanie nowego atrybutu do klienta) jest zazwyczaj implementowana poprawnie. Dodanie jednak pola do formularz jest już bardzo często pomijane. Oczywiście jeżeli danych atrybut klienta nie jest odpowiedni dla formularzy pomijamy tę część.

Dodawanie bloków

Często wdrażanie systemu e-Commerce wymaga dodania treści. Możemy oczywiście przerzucić prace na klienta, ale lepiej będzie jeżeli zrobimy część pracy za niego i np. wdrożymy mu zestaw bloków html wykorzystywanych potem w systemie. Nie zajmie nam to dużo pracy, a możemy znacząco zwiększyć zadowolenie naszego klienta (i zmniejszyć czas potrzebny na odpowiadanie na proste pytania).

Oto jak dodajemy blok CMS:

<?php

Mage::getModel('cms/block')
    ->setData(
        [
            'title'      => 'Blok na stopkę - 1 kolumna',
            'identifier' => 'footer_block_first',
            'content'    => 'kod html albo treść bloku ląduje tutaj',
            'is_active'  => 1,
            'stores'     => [0],
        ]
    )
    ->save();

Tym samym wszyscy zajmujący się projektem także będą mieli ten blok u siebie w bazie i nie będzie to trzeba dodawać ręcznie.

W przypadku instalacji z wieloma sklepami należy ulepszyć także to co podajemy w parametrze stores. Możemy też sprawdzać czy przypadkiem blok o takim identyfikatorze nie istnieje (żeby nie nadpisać bloku, który jest już przygotowany).

$blockExists = $cmsBlock->load('footer_block_first')->getId();

if(!$blockExists) {
…
}

Podsumowanie

Mam nadzieję, że te klika przykładów pozwoli Wam ulepszyć jakość pisanych modułów do Magento i może kilka razy w przyszłości uniknąć stresów związanych z błędami ? (oby nie na prodzie).

Pamiętajcie, że w naszym zawodzie lepiej raz poświęcić więcej czasu na nauczenie się czegoś lepiej i tym samym zaoszczędzić czas w przyszłości na wracanie do starych “błędów”.