Tworzenie modułu Prestashop cz. 3 – wykorzystanie własnych klas i kontrolerów
W tym poradniku ponownie wykorzystamy pliki z poprzednich części.
Przygotujmy
w pierwszej kolejności kontroler. W tym celu utwórzmy w głównym folderze
naszego modułu folder controllers
a wewnątrz niego folder admin
,
tam dodajmy plik myfirstmodule.php
Następnie
utwórzmy klasę dla naszych elementów, w tym celu utwórzmy folder classes
w głównym folderze modułu, a następnie w nim plik MyFirstModuleBlock.php
Zanim zajmiemy się naszym kontrolerem i klasą, dodajmy dodatkową treść do funkcji installDB()
w głównym pliku modułu.
$correct = Db::getInstance()->execute(' CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'myfirstmodule_blocks` ( `id_myfirstmodule_block` INT UNSIGNED NOT NULL AUTO_INCREMENT , `active` INT NOT NULL , `position` INT NOT NULL , PRIMARY KEY (`id_myfirstmodule_block`) ) ENGINE = ' . _MYSQL_ENGINE_ . '; '); $correct = Db::getInstance()->execute(' CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'myfirstmodule_blocks_lang` ( `id_myfirstmodule_block_lang` INT UNSIGNED NOT NULL AUTO_INCREMENT , `id_myfirstmodule_block` INT NOT NULL , `id_lang` INT NOT NULL , `name` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL , PRIMARY KEY (`id_myfirstmodule_block_lang`) ) ENGINE = ' . _MYSQL_ENGINE_ . '; ');
Tymi zapytaniami utworzymy podczas instalacji dwie dodatkowe tabele, będziemy tworzyć bloki a linki będą dodawane do bloków, dlatego musimy usunąć stare tabele i zmodyfikować zapytanie dla tabeli myfirstmodule
$correct = Db::getInstance()->execute(' CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'myfirstmodule` ( `id_myfirstmodule` INT UNSIGNED NOT NULL AUTO_INCREMENT , `id_myfirstmodule_block` INT NOT NULL , `blank` INT NOT NULL , PRIMARY KEY (`id_myfirstmodule`) ) ENGINE = ' . _MYSQL_ENGINE_ . '; ');
Kolumna id_shop
została zmieniona na id_myfirstmodule_block
Musimy jeszcze dodać kod do funkcji install()
za pomocą którego wyświetlimy własny odnośnik w menu Panelu Administracyjnego.
$tab = new Tab(); $tab->active = 1; $tab->class_name = 'MyFirstModule'; $tab->position = 3; $tab->name = array(); foreach (Language::getLanguages(true) as $lang) { $tab->name[$lang['id_lang']] = 'Mój pierwszy moduł'; } $tab->id_parent = (int) Tab::getIdFromClassName('CONFIGURE'); $tab->module = $this->name; $tab->add(); $tab->save();
Wymienione w tym miejscu pola to:
- active – czy nasz odnośnik ma być aktywy
- class_name – klasa odnośnika
- position – pozycja na liście
- name – to musi być tablica z wariantem dla każdego języka, dlatego niżej można zobaczyć pętlę foreach
- id_parent – id rodzica, uzyskujemy je
za pomocą funkcji
getIdFromClassName
, gdzie w panelu dostępne są 3 główne zakładki: SELL (Sprzedaż), IMPROVE (Ulepszenia), CONFIGURE (Konfiguruj), ponadto jeżeli chcemy dodać zakładkę do któregoś rozwijanego menu, to w tym momencie należy podać odpowiednią klasę dla tej zakładki - module – nazwa modułu, do którego odnosi zakładki
Niżej wywoływane są dwie funkcje, jedna dodaje zakładkę, druga zapisuje ustawienia.
Otwórzmy teraz nasz plik kontrolera i uzupełnijmy go następującą treścią:
<?php include_once(_PS_MODULE_DIR_.'myfirstmodule/classes/MyFirstModuleBlock.php'); class myfirstmoduleController extends AdminController { protected $position_identifier = 'id_myfirstmodule_block'; public function __construct() { $this->bootstrap = true; $this->table = 'myfirstmodule_blocks'; $this->list_id = 'myfirstmodule_blocks'; $this->identifier = 'id_myfirstmodule_block'; $this->className = 'MyFirstModuleBlock'; $this->lang = true; $this->_defaultOrderBy = 'position'; parent::__construct(); $this->fields_list = array( 'id_myfirstmodule_block' => array( 'title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs', ), 'name' => array( 'title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name', 'align' => 'left', ), 'position' => array( 'title' => $this->trans('Position', array(), 'Admin.Global'), 'filter_key' => 'a!position', 'position' => 'position', 'align' => 'center', 'class' => 'fixed-width-xs', ), 'active' => array( 'title' => $this->trans('Displayed', array(), 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'class' => 'fixed-width-sm', 'type' => 'bool', 'orderby' => false, ), ); $this->addRowAction('edit'); $this->addRowAction('delete'); $this->bulk_actions = array( 'delete' => array( 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), 'icon' => 'icon-trash', 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), ), ); } public function initPageHeaderToolbar() { if (empty($this->display)) { $this->page_header_toolbar_btn['new_block'] = array( 'href' => $this->context->link->getAdminLink('myfirstmodule', true, array(), array(' addmyfirstmodule_blocks' => 1)), 'desc' => 'Dodaj nowy blok', 'icon' => 'process-icon-new', ); } parent::initPageHeaderToolbar(); } }
Jest tutaj
do opisania wiele pól, na początku możemy zauważyć, że w funkcji __construct()
znajdziemy wiele ustawień:
- bootstap – czy chcemy wykorzystywać bootstrap w szablonach
- table – tabela, z której mają być
pobierane wartości, jeśli istnieje tabela z nazwą głównej tabeli i dopiskiem
_lang
to jej wartości również zostaną pobrane, tak jak w naszym przypadkumyfirstmodule_blocks
i tabelamyfirstmodule_blocks_lang
- list_id – id naszej listy w widoku
- identifier – po jakim polu w bazie mają być rozpoznawane poszczególne elementy
- className – nazwa klasy PHP dla widoku
- lang – czy dostępna wielojęzyczność
- _defaultOrderBy – domyślne sortowanie po
- fields_list – lista kolumn w tabeli,
jako indeksy w tablicy podajemy nazwy kolumn z bazy danych, dostępne atrybuty
to:
- title – tytuł kolumny
- align – wyrównanie tekstu w kolumnie
- class – klasa kolumny
- filter_key
– jaka wartość ma być wyświetlana, czasem, jeśli połączone jest kilka tabel to
mogą dublować się nazwy kolumn, tutaj precyzujemy z której tabeli dane chcemy
pobierać, tabela „a” jest główna, „b” to
_lang
- type – rodzaj pola, w przypadku bool wyświetla się „X” lub „fajka”
- orderby – jeśli ustawione na false, nie da się sortować po tym polu
- addRowAction – tą funkcją dodajemy dostępne akcje do każdej linii w tabeli
- bulk_actions – są to akcje dostępne po zaznaczeniu elementów na liście, my dodaliśmy tylko możliwość usuwania wielu naraz
Niżej możemy znaleźć kolejną funkcję jaką jest initPageHeaderToolbar
w której ustawiamy zawartość górnego paska, możemy dodać tam dodatkowe przyciski. W tym przypadku dodajemy przycisk przenoszący do formularza dodawania nowego bloku.
Gdy wszystko jest przygotowane poprawnie powinniśmy zobaczyć po instalacji zakładkę Mój pierwszy modułw zakładce Konfiguruj, po przejściu do niej zobaczymy widok jak powyżej.
Przejdźmy teraz do przygotowania widoku dodawania bloku w naszym kontrolerze.
W tym celu musimy przygotować nasz plik klasy, otwórzmy go i dodajmy tam następującą treść:
<?php class MyFirstModuleBlock extends ObjectModel { public $id; public $id_myfirstmodule_blocks; public $name; public $position; public $active; public static $definition = array( 'table' => 'myfirstmodule_blocks', 'primary' => 'id_myfirstmodule_block', 'multilang' => true, 'multilang_shop' => true, 'fields' => array( 'position' => array('type' => self::TYPE_INT), 'active' => array('type' => self::TYPE_BOOL), 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512), ), ); }
Są to ustawienia modelu, na podstawie którego będą się dodawać, aktualizować i usuwać dane.
Na początku ustalamy po prostu jakie pola będą dostępne, niżej znajdziemy zmienną definitione, gdzie znajdziemy takie ustawienia jak:
- table – główna tabela dla elementu
- primary – główna kolumna, za pomocą, której elementy są rozróżniane
- multilang – czy istnieje tablica z treścią dla języków
- multilang_shop – czy w tablicy dla
języków rozpoznawane są sklepy (czyli czy istnieje kolumna
id_shop
- fields – pola które są przechowywane
przez bazę danych, a dla nich możemy zobaczyć takie ustawienia jak:
- type – rodzaj pola
- lang
– jeśli
true
to zmienna znajduje się w tabeli_lang
- validate – na jakiej podstawie ma odbywać się walidacja
- size – rozmiar pola
Po zapisaniu ustawień, możemy już dodawać nasz blok w panelu, a on od razu wyświetli się na liście. Możemy go też również od razu usuwać i edytować, także mamy przygotowaną obsługę wszystkiego w dużo szybszy sposób.
Nie działa jednak jeszcze zmienianie pozycji, a dodając elementy można zauważyć, że każdy ma pozycję 1.
Aby to naprawić dodajmy do naszej klasy dwie funkcje:
public function add($autoDate = true, $nullValues = false) { $this->position = MyFirstModuleBlock::getLastPosition(); return parent::add($autoDate, true); } public static function getLastPosition() { $sql = ' SELECT MAX(position) + 1 FROM `' . _DB_PREFIX_ . 'myfirstmodule_blocks`'; return Db::getInstance()->getValue($sql); }
Pierwsza
funkcja jest wywoływana w momencie dodawania bloku do bazy danych, w tym
miejscu ją nadpisujemy i ustawiamy parametr position
równy temu co
zwróci kolejna funkcja getLastPosition()
w której pobieramy z bazy
danych najwyższą pozycję + 1
To pozwoli na poprawne ustawianie się pozycji nowych elementów, przeciąganie jednak niczego nie zmienia, musimy dodać funkcje do naszego kontrolera:
public function ajaxProcessUpdatePositions() { $way = (int)Tools::getValue('way'); $id_myfirstmodule_block = (int)Tools::getValue('id'); $positions = Tools::getValue('myfirstmodule_block'); if (is_array($positions)){ foreach ($positions as $position => $value) { $pos = explode('_', $value); if (isset($pos[2]) && (int)$pos[2] === $id_myfirstmodule_block) { if (isset($position) && $this->updatePosition($way, $position, $id_myfirstmodule_block)) echo 'ok'; else echo '{"hasError" : true, "errors" : "Can not update id '.(int)$id_myfirstmodule_block.' to position '.(int)$position.' "}'; break; } } } } public function updatePosition($way, $position, $id) { if (!$res = Db::getInstance()->executeS(' SELECT `id_myfirstmodule_block`, `position` FROM `'._DB_PREFIX_.'myfirstmodule_blocks` ORDER BY `position` ASC' )) return false; foreach ($res as $block) if ((int)$block['id_myfirstmodule_block'] == (int)$id) $moved_block = $block; if (!isset($moved_block) || !isset($position)) return false; var_dump($moved_block['position']); return (Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'myfirstmodule_blocks` SET `position`= `position` '.($way ? '- 1' : '+ 1').' WHERE `position` '.($way ? '> '.(int)$moved_block['position'].' AND `position` <= '.(int)$position : '< '.(int)$moved_block['position'].' AND `position` >= '.(int)$position.' ')) && Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'myfirstmodule_blocks` SET `position` = '.(int)$position.' WHERE `id_myfirstmodule_block` = '.(int)$moved_block['id_myfirstmodule_block'])); }
Jest to już z góry gotowe rozwiązanie, wykorzystywane też przy głównych funkcjach Prestashop. Pierwsza funkcja jest wywoływana w momencie zmiany pozycji poprzez ajax, przesyłane są wszystkie zmienione pozycje, każdego elementu, gdzie ta zmiana zachodzi.
Dla każdego
elementu jest uruchamiana funkcja updatePosition
która aktualizuje te
pozycje w bazie danych.
W ten sposób mamy ukończony prosty CRUD z wykorzystaniem kontrolera i modelu, jest to na pewno szybkie rozwiązanie.
Przygotujmy teraz możliwość dodawania odnośników do bloków. W tym celu musimy w funkcji __construct
dodać linijkę:
$this->addRowAction('view');
Dzięki czemu na liście będzie pojawiał się przycisk „Zobacz”
Następnie musimy dodać do naszego folderu classes
nową klasę o nazwie MyFirstModuleLink.php
i umieśćmy wewnątrz taki kod:
<?php class MyFirstModuleLink extends ObjectModel { public $id; public $id_myfirstmodule; public $id_myfirstmodule_block; public $name; public $url; public $blank; public static $definition = array( 'table' => 'myfirstmodule', 'primary' => 'id_myfirstmodule', 'multilang' => true, 'multilang_shop' => true, 'fields' => array( 'blank' => array('type' => self::TYPE_BOOL), 'id_myfirstmodule_block' => array('type' => self::TYPE_BOOL), 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512), 'url' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512), ), ); }
Jest to po prostu konfiguracja na tej samej zasadzie co w poprzedniej.
Otwórzmy teraz plik kontrolera i dodajmy do funkcji __construct
na górze treść:
$this->mode = 'block';
Następnie usuńmy tą treść, będziemy ją umieszczać w innym miejscu.
$this->addRowAction('view'); $this->addRowAction('edit'); $this->addRowAction('delete');
I dodajmy na samym dole funkcji:
$this->changeClass = false; if (Tools::isSubmit('addmyfirstmodule') || Tools::isSubmit('viewmyfirstmodule_blocks') || Tools::isSubmit('submitAddmyfirstmodule') || Tools::isSubmit('updatemyfirstmodule') || Tools::isSubmit('deletemyfirstmodule')) { if(Tools::isSubmit('addmyfirstmodule') || Tools::isSubmit('submitAddmyfirstmodule')){ $this->display = 'add'; $this->changeClass = true; } if(Tools::isSubmit('updatemyfirstmodule')){ $this->display = 'update'; $this->changeClass = true; } if(Tools::isSubmit('deletemyfirstmodule')){ $this->changeClass = true; } if($this->changeClass){ $this->table = 'myfirstmodule'; $this->className = 'MyFirstModuleLink'; $this->identifier = 'id_myfirstmodule'; $this->position_identifier = 'id_myfirstmodule'; $this->position_group_identifier = 'id_myfirstmodule_block'; $this->list_id = 'links_values'; $this->lang = true; } $this->mode = 'link'; }
W tym
miejscu sprawdzamy czy aktualnie jest realizowane zadanie dotyczące linku,
jeśli tak, to zmieniamy nazwę tabeli na ta posiadającą linki, tak samo zmieniamy
nazwę klasy itd. Zmienną $mode
będziemy wykorzystywać później.
Nie
zmieniamy klasy dla widoku viewmyfirstmodule_block
ponieważ wywołuje to
błąd musimy dokonać tej zmiany w innym miejscu.
Przejdźmy teraz do funkcji initPageHeaderToolbar
, dodajmy tam treść:
else { if (($id = (int) Tools::getValue('id_myfirstmodule_block'))) { $this->page_header_toolbar_btn['new_value'] = array( 'href' => self::$currentIndex . '&addmyfirstmodule&id_myfirstmodule_block=' . $id . '&token=' . $this->token, 'desc' => 'Dodaj nową wartość', 'icon' => 'process-icon-new', ); } }
Dzięki czemu w górnym menu będzie wyświetlać się przycisk pozwalający dodać wartość.
Teraz dodajmy dwie nowe funkcje do naszego kontrolera:
public function renderView() { if (($id = (int) Tools::getValue('id_myfirstmodule_block'))) { $this->table = 'myfirstmodule'; $this->className = 'MyFirstModuleLink'; $this->identifier = 'id_myfirstmodule'; $this->position_identifier = 'id_myfirstmodule'; $this->position_group_identifier = 'id_myfirstmodule_block'; $this->list_id = 'links_values'; $this->lang = true; $this->context->smarty->assign(array( 'current' => self::$currentIndex . '&id_myfirstmodule_block=' . (int) $id . '&viewmyfirstmodule_blocks', )); $this->fields_list = array( 'id_myfirstmodule' => array( 'title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs', ), 'name' => array( 'title' => $this->trans('Nazwa', array(), 'Admin.Catalog.Global'), 'width' => 'auto', 'filter_key' => 'b!name', ), 'url' => array( 'title' => $this->trans('Adres URL', array(), 'Admin.Catalog.Global'), 'width' => 'auto', 'filter_key' => 'b!url', ), 'blank' => array( 'title' => $this->trans('Nowa karta', array(), 'Admin.Catalog.Global'), 'width' => 'auto', 'type' => 'bool', 'orderby' => false, ), ); $this->_where = 'AND a.`id_myfirstmodule_block` = ' . (int) $id; $this->_orderBy = 'id_myfirstmodule'; self::$currentIndex = self::$currentIndex . '&id_myfirstmodule_block=' . (int) $id . '&viewmyfirstmodule_blocks'; $this->processFilter(); } else { $this->addRowAction('view'); } $this->addRowAction('edit'); $this->addRowAction('delete'); return parent::renderList(); } public function renderList() { if($this->mode != 'link'){ $this->addRowAction('view'); } $this->addRowAction('edit'); $this->addRowAction('delete'); return parent::renderList(); }
Pierwsza
pozwala przygotować inną tabelę dla naszych elementów, składnia taka sama jak
poprzednio w funkcji __construct
, z tym wyjątkiem, że nie dodajemy
przycisku Zobacz do tej listy.
Druga
funkcja odpowiada za wyświetlanie na stronie głównej modułu po zaktualizowaniu
bloku, jest to osoba funkcja, dlatego musimy tutaj tylko dodać przyciski,
reszta jest brana na podstawie ustawień z funkcji __construct
Zmodyfikujmy kolejno funkcje renderForm
:
if($this->mode == 'link'){ $this->fields_form = array( 'tinymce' => true, 'legend' => array( 'title' => $this->l('Link'), 'icon' => 'icon-folder-close', ), 'input' => array( array( 'type' => 'text', 'label' => $this->l('Tekst odnośnika'), 'name' => 'name', 'lang' => true, 'size' => 20, 'required' => true ), array( 'type' => 'text', 'label' => $this->l('Adres odnośnika'), 'name' => 'url', 'lang' => true, 'size' => 20, 'required' => true ), array( 'type' => 'switch', 'label' => $this->l('Nowa karta'), 'name' => 'blank', 'is_bool' => true, 'required' => true, 'values' => array( array( 'id' => 'label3_on', 'value' => 1, 'label' => $this->l('Tak') ), array( 'id' => 'label3_off', 'value' => 0, 'label' => $this->l('Nie') ) ) ), array( 'type' => 'hidden', 'name' => 'id_myfirstmodule_block', 'value' => (int) Tools::getValue('id_myfirstmodule_block') ) ), 'submit' => array( 'title' => $this->trans('Save', array(), 'Admin.Actions'), ), ); } else {
Dodajemy
instrukcję warunkową, jeśli $mode == ‘link’
dzięki czemu wyświetlamy inny
formularz dla linków. Wewnątrz else
umieszczamy poprzednią zawartość funkcji.
Pozostało nam dodać dwie funkcje:
public function processAdd(){ if(Tools::isSubmit('submitAddmyfirstmodule')){ $this->redirect_after = self::$currentIndex . '&viewmyfirstmodule_blocks=&id_myfirstmodule_block=' . (int) Tools::getValue('id_myfirstmodule_block') . '&token=' . $this->token; } return parent::processAdd(); } public function processUpdate() { if(Tools::isSubmit('submitAddmyfirstmodule')){ $this->redirect_after = self::$currentIndex . '&viewmyfirstmodule_blocks=&id_myfirstmodule_block=' . (int) Tools::getValue('id_myfirstmodule_block') . '&token=' . $this->token; } return parent::processUpdate(); }
Uruchamiają się kolejno po dodaniu i po zaktualizowaniu w bazie danych. Ustawiamy tylko parametr, gdzie ma przekierować po zapisaniu, ponieważ domyślnie przenosi na stronę główną modułu.
Gdy wszystko w porządku zobaczymy taki widok i będziemy mogli dodawać/edytować/usuwać wartości.
Z pliku modułu
usuwamy nie potrzebną treść, zostawiamy tylko formularz $mode == 0
dotyczący konfiguracji i tak samo zapis tego pola, pozostałości po dodawaniu
odnośników można usunąć, musimy jednak zmodyfikować plik tak, aby zmienić widok
na stronie głównej.
Dodajmy więc
najpierw dwa bloki do bazy a do nich po dwa odnośniki, a następnie przejdźmy do
pliku modułu myfirstmodule.php
Zmieńmy treść funkcji hookDisplayHome
na następującą:
$id_shop = (int)Context::getContext()->shop->id; $id_lang = (int)Context::getContext()->language->id; $blocksArray = []; $blocks = Db::getInstance()->ExecuteS(" SELECT * FROM `" . _DB_PREFIX_ . "myfirstmodule_blocks` INNER JOIN `" . _DB_PREFIX_ . "myfirstmodule_blocks_lang` ON " . _DB_PREFIX_ . "myfirstmodule_blocks.id_myfirstmodule_block = " . _DB_PREFIX_ . "myfirstmodule_blocks_lang.id_myfirstmodule_block WHERE `id_shop` = " . $id_shop . " AND `id_lang` = " . $id_lang . ' ORDER BY `position` ASC' ); foreach($blocks as $block){ $block['elements'] = Db::getInstance()->ExecuteS(" SELECT * FROM `" . _DB_PREFIX_ . "myfirstmodule` INNER JOIN `" . _DB_PREFIX_ . "myfirstmodule_lang` ON " . _DB_PREFIX_ . "myfirstmodule.id_myfirstmodule = " . _DB_PREFIX_ . "myfirstmodule_lang.id_myfirstmodule WHERE `id_shop` = " . $id_shop . " AND `id_lang` = " . $id_lang . ' AND `id_myfirstmodule_block` = ' . $block['id_myfirstmodule_block'] ); $blocksArray[] = $block; } $this->context->smarty->assign( array( 'myfirstmodule' => Configuration::get('myfirstmodule'), 'blocks' => $blocksArray ) ); return $this->display(__FILE__, 'views/front/myfirstmodule.tpl');
Teraz
pobierane będą bloki a dla bloków elementy. Przejdźmy do pliku views/front/myfirstmodule.php
I zmieńmy treść na:
<h3>{$myfirstmodule}</h3> {if count($blocks) > 0} <div class="row" style="margin-top: 50px;"> {foreach from=$blocks item=block} <div class="col-md-3"> <h5>{$block.name}</h5> <ul> {foreach from=$block.elements item=element} <li><a href="{$element.url}" {if $element.blank == 1} target="_blank" {/if}>{$element.name}</a></li> {/foreach} </ul> </div> {/foreach} </div> {/if}
Będziemy wyświetlać bloki a wewnątrz nich elementy.
Jeśli wszystko jest w porządku powinniśmy zobaczyć taki widok jak powyżej.
W powyższy sposób przygotowaliśmy rozbudowany moduł z użyciem kontrolera, możemy oczywiście tworzyć wiele kontrolerów z wieloma widokami.