Поиск по блогу

четверг, 21 ноября 2013 г.

Создаем модуль в PrestaShop v.1.5+

 Любой "движок" требует соблюдения определенных правил при создании новых конструкций или модернизации существующих. Как правило никто эти нормы подробно не расписывает. И, чтобы встроить свой код в чужой движок иногда приходится много копать этого самого чужого кода. Данная статья написана в стиле As is - т.е. я не объясняю почему здесь должен быть этот div или за что отвечает этот метод. Если интересно - копайте код сами :)
Здесь предлагается набор конструкций (паззлов) из которых однозначно и достаточно быстро можно создать необходимый функционал



Достало меня каждый раз оформлять футер - выискивание модулей где располагаются ссылки, невозможно изменять название, количество блоков. Как-то все криво, нелогично и не в одном месте. Да и с мультиязычностью как всегда не все в порядке.
Футер, по моему глубокому разумению, должен содержать несколько колонок нужных ссылок и колонку с реквизитами.
Предлагаю совместить приятное с полезным и научиться создавать собственные модули с нуля на примере создания полезной конструкции :). К тому же создать модуль для Prestashop версии 1,5 совершенно не сложно.
Мы не будем создавать свою таблицу в БД сайта. Воспользуемся замечательной, по моему мнению, возможностью PrestaShop создавать и использовать переменные конфигурации. Они размещаются в базе автоматом.

Назовем модуль: realfooter.
Задача: Заменить стандартные ссылки футера. Ссылки будут располагаться в 4 колонках. Каждая колонка имеет свое название. Пятая колонка будет содержать контакную информацию.
Все языкозависимо (во какое слово :) )

В принципе, поняв суть, каждый в последствие сможет создавать свой уникальный футер (и не только футер) под свои потребности. Поэтому описывать создание я буду несколько шире, чем того требует наша конкретная задача.

 1. Создаем скелет нашего модуля

В каталоге www/modules создаем подкаталог realfooter
В нем создаем следующие подкаталоги и файлы:
  • cjs - подкаталог. Здесь будут располагаться файлы css и js если их много. Если у вас планируется один js и один css файл - тогда положите их в корне realfooter и в дополнительном подкаталоге cjs просто нет смысла
  • img - подкаталог. Здесь  будут располагаться картинки, опять же, если их много и в этом подкаталоге есть смысл.
  • translations - подкаталог. Это обязательный подкаталог, если у вас многоязычный сайт. Название подкаталога стандартное и изменять его нельзя.
  • realfooter.php - файл. Это основной класс нашего модуля: инсталляция/деинсталляция и вся математика в т.ч. работа с БД. Создайте пока пустой файл.
  • realfooter.tpl - файл. Это представление (вьюха, отображение) - HTML код со вставками Smarty для вывода нашего модуля во фронт.. Создайте пока пустой файл. Что такое язык разметки Smarty и как им пользоваться - велкам ту соответствующий замечательный сайт (не забудьте там включить русский язык ;))
  • logo.gif - файл. Иконка, которая появится в админке и будет сопровождать наш новый модуль 
этого достаточно.

  2. Файл realfooter.php 

Как уже было сказано, это основной/главный файл нашего модуля.
<?php
if( !defined( '_PS_VERSION_' ) )
  exit;

class realfooter extends Module
{
  private $_html = '';
  private $_postErrors = array();

  public function __construct()
  {

    $this->name = 'realfooter';
    $this->tab = 'front_office_features';
    $this->version = '1.0';
    $this->author = 'HowKnowCoder';

    parent::__construct();

    $this->displayName = $this->l( 'Main page Footer module' );
    $this->description = $this->l( 'Main page Footer HowKnowCoder module' );
  }
В конструкторе мы указываем название нашего модуля, принадлежность к группе ('front_office_features'), версию и автора. Если Вы намерены делать несколько модулей, то рекомендую указвать автора, т.к. в админке есть фильтр по автору модуля, что очень удобно.
Ниже мы указываем название нашего модуля человеческим языком и краткое описание. Все это будет отображаться в админке
function install()
  {

    $this->_clearCache( 'realfooter.tpl' );
    if( Shop::isFeatureActive() )
      Shop::setContext( Shop::CONTEXT_ALL );

    return parent::install() &&
    $this->registerHook( 'footer' ) &&
    Configuration::updateValue( 'realfooter', 'settings' );
  }

  public function uninstall()
  {

    $this->_clearCache( 'realfooter.tpl' );
    if( !parent::uninstall() ||
        !Configuration::deleteByName( 'realfooter' )
    )
      return false;
    return true;
  }
Методы install и uninstall говорят сами за себя. Мы размещаемся в хуке Footer ($this->registerHook ...) и у нас будет админка  (Configuration::updateValue ...)
Этих действий вполне достаточно для инсталляции и деинсталляции нашего модуля. Если Вы собираетесь использовать собственную таблицу в БД - просто посмотрите как инициализируемый, создается и удаляется таблица на примере модулей, что в папке modules. Там ничего сложного.

Пару слов о страшилке под названием HOOK. На самом деле ничего страшного нет. В интернете полно описаний, но, честно говоря они не совсем понятны. Поэтому я скажу грубо, но метко: хук - это поле (место, позиция) в шаблоне, в которое помещается модуль, а точнее TPL -файл модуля. В Prestashop есть правда еще другие типы хуков, но нам сейчас они не важны.

Как вы уже догадались у нашего модуля будет админка (кто жил до этого момента в счастливом неведении - может "сделать глаза" и воскликнуть "Да ладно!" ;) ).
 Стандартом (если так можно выразиться) PrestaShop предусмотрено два метода: displayForm() и getContent(). Первый содержит код формы в админке, второй выводит форму и обрабатывает ее.

Создаем форму:

public function displayForm()
  {
    // Get default Language
    $languages = Language::getLanguages( false );
    $deflang_id = (int)( Configuration::get( 'PS_LANG_DEFAULT' ) );
    $lang_value = array();

//Создаем форму и выводим шапку

    $form = '<form action="' . Tools::safeOutput( $_SERVER[ 'REQUEST_URI' ] ) . '" 'method="post" 
             method="post">';
    $form .= '<fieldset><legend>';
    $form .= '<img src="' . $this->_path . 'logo.png" alt="" title="" />';
    $form .= $this->l( 'HowKnowCoder Footer Page Settings' ) . '</legend>';

// Цикл вывода панелей. По условиям у нас 4 панели
    // Panels
    for( $i = 1; $i <= 4; $i++ )
    {
      $form .= '<div style="clear: both;">';
      $form .= '<label style="font-weight: bold; border: 1px solid #AAA; padding: 10px; 
                background-color: #DDD;">';
      $form .= sprintf( $this->l( '%s panel' ), $i ) . '</label>';
      $form .= '<hr style="width: 80%; height: 1px; background-color: #AAA;" /></div>';
      $form .= '<div style="width: 100%; clear: both;"></div>';

// Выводим название блока. Название зависит от языка, поэтому выводим стандартную панель языков с флагами
      // Title
      $form .= '<label > ' . $this->l( 'Panel name:' ) . ' </label > ';
      $form .= '<div class="margin-form" > ';
      $form .= '<div class="translatable" > ';

      foreach( $languages as $language )
      {
        $lang_value[ $language[ 'id_lang' ] ][ 'hnc_panname_' . $i ] = Configuration::get( 'HNC_PANNAME_' . 
           $i . '_' . $language[ 'id_lang' ] );
        $form .= ' <div id = "hnc_panname_' . $i . '_' . (int)$language[ 'id_lang' ] . '" class="lang_' . 
           (int)$language[ 'id_lang' ] . '" style = "display: ' . ( $language[ 'id_lang' ] == $deflang_id ? 
           'block' : 'none' ) .'; float: left;">';
        $form .= ' <input size = "50" type = "text" name = "hnc_panname_' . $i . '_' . $language[ 'id_lang' ] . 
           '" value = "'. Tools::safeOutput( @$lang_value[ $language[ 'id_lang' ] ][ 'hnc_panname_' . $i ], 
           true ) . '" />';
        $form .= ' </div > ';
      }
      $form .= $this->displayFlags( $languages, $deflang_id, 'hnc_panname_' . $i, 'hnc_panname_' . $i, true );
      $form .= ' </div ></div ><div style="clear: both;padding-bottom: 10px;">
        </div>'; 

// Выводим 6 ссылок внутри блока. Ссылка состоит из поля Названия и поля собственно ссылки
      // Strings
      for( $s = 1; $s <= 6; $s++ )
      {
        // String Title
        $form .= '<label > ' . $this->l( 'String:' ) . $s . ' </label > ';
        $form .= '<div class="margin-form" > ';
        $form .= '<div class="translatable" > ';

        foreach( $languages as $language )
        {
          $lang_value[ $language[ 'id_lang' ] ][ 'hnc_strname_' . $i . '_' . $s ] =
             Configuration::get( 'HNC_STRNAME_' . $i . '_' . $s . '_' . $language[ 'id_lang' ] );
          $form .= ' <div id = "hnc_strname_' . $i . '_' . $s . '_' . 
            (int)$language[ 'id_lang' ] . '" class="lang_' . (int)$language[ 'id_lang' ] . '" 
             style = "display: ' . ( $language[ 'id_lang' ] == $deflang_id ? 
            'block' : 'none' ) .'; float: left;">';
          $form .= ' <input size = "50" type = "text" name = "hnc_strname_' . $i . '_' . $s . '_' . 
            $language[ 'id_lang' ] . '" value = "'. 
            Tools::safeOutput( @$lang_value[ $language[ 'id_lang' ] ][ 'gc_strname_' . 
            $i . '_' . $s ], true ) . '" />';
          $form .= ' </div>';
        }
        $form .= $this->displayFlags( $languages, $deflang_id, 'hnc_strname_' . $i . '_' . $s, 
           'hnc_strname_' . $i . '_' . $s, true );
        $form .= ' </div></div>';

// Ссылка не зависит от языка, поэтому код проще
        // Link
        $form .= '<label > ' . $this->l( 'Link:' ) . ' </label > ';
        $form .= '<div class="margin-form" > ';
        $form .= '<input type = "text" size = "50" name = "hnc_strlink_' . $i . '_' . $s . '" value = "' . 
           Configuration::get( 'HNC_STRLINK_' . $i . '_' . $s ) . '" />';
        $form .= '<p style = "clear: both" > '. sprintf( $this->l( 'URL like this: %s' ), 
           '/index.php?id_category=6&controller=category' ). ' </p ></div > ';
      }

// Здесь выводим пятую панель. Оба поля мультиязычны
    // Contacts Title
    $form .= '<div style="clear: both;">';
    $form .= '<label style="font-weight: bold; border: 1px solid #AAA; padding: 10px; 
       background-color: #DDD;"> '. sprintf( $this->l( 'Contact panel' ), $i ) . '</label>';
    $form .= '<hr style="width: 80%; height: 1px; background-color: #AAA;" /></div>';
    $form .= '<div style="width: 100%; clear: both;"></div>';
    $form .= '<label > ' . $this->l( 'Contact Title:' ) . ' </label > ';
    $form .= '<div class="margin-form" > ';
    $form .= '<div class="translatable" > ';

    foreach( $languages as $language )
    {
      $lang_value[ $language[ 'id_lang' ] ][ 'hnc_contname' ] = Configuration::get( 'HNC_CONTNAME_' . 
         $language[ 'id_lang' ] );
      $form .= ' <div id = "hnc_contname_' . (int)$language[ 'id_lang' ] . '" class="lang_' . 
         (int)$language[ 'id_lang' ] . '" style = "display: ' . ( $language[ 'id_lang' ] == $deflang_id ? 
         'block' : 'none' ).'; float: left;">';
      $form .= ' <input size = "50" type = "text" name = "hnc_contname_' . $language[ 'id_lang' ] . 
         '" value = "' .= Tools::safeOutput( @$lang_value[ $language[ 'id_lang' ] ][ 'mbf_contname' ], true ) . 
         '" />';
      $form .= ' </div > ';
    }
    $form .= $this->displayFlags( $languages, $deflang_id, 'hnc_contname', 'hnc_contname', true );
    $form .= ' </div ></div >';

    // Contacts
    $form .= '<label style = "clear:both;" > ' . $this->l( 'Contacts:' ) . ' </label > ';
    $form .= '<div class="margin-form" > ';
    $form .= '<div class="translatable" > ';

    foreach( $languages as $language )
    {
      $lang_value[ $language[ 'id_lang' ] ][ 'hnc_text' ] = Configuration::get( 'HNC_TEXT_' . 
         $language[ 'id_lang' ] );
      $form .= ' <div id = "hnc_text_' . (int)$language[ 'id_lang' ] . '" class="lang_' . 
         (int)$language[ 'id_lang' ] . '" style = "display: ' . ( $language[ 'id_lang' ] == $deflang_id ? 
         'block' : 'none' ).'; float: left;">';
      $form .= ' <textarea rows = "5" cols = "60" type = "text" name = "hnc_text_' . 
         $language[ 'id_lang' ] . 
         '">'. Tools::safeOutput( @$lang_value[ $language[ 'id_lang' ] ][ 'mbf_text' ], true ).
         '</textarea ></div > ';
    }
    $form .= $this->displayFlags( $languages, $deflang_id, 'hnc_text', 'hnc_text', true );
    $form .= ' </div ></div > '; 
    $form .= '<div style = "clear: both;width: 100%;" ></div > ';
    $form .= '<input type = "submit" name = "submitHNCPage" value = "' . $this->l( 'Save' ) . 
       '" class="button" />';
    
    $form .= '</fieldset ></form > ';

    return $form;
  } 
Страшно? Сам боюсь ... А если присмотреться - да все просто. Прежде всего надо определиться с названиями полей и названиями соответствующих переменных. Переменным очень желательно дать префикс, чтобы их можно было отличить от другого "хлама" и чтобы ваши переменные не переписали существующие!!! в таблице ps_configuration.

  • HNC_PANNAME_x_lang - название блока, где x: номер блока, lang: язык текста
  • HNC_STRNAME_x_s_lang - название ссылки, где x: номер блока, s: номер строки в блоке, lang: язык текста
  • HNC_STRLINK_x_s - ссылка, где x: номер блока, s: номер строки в блоке
  • HNC_CONTNAME_lang - название 5 панели, где lang: язык текста
  • HNC_TEXT_lang - текст 5 панели, где lang: язык текста
Чтобы не путаться соответствующие id и имена полей и div`ов называем также, только строчными буквами:
hnc_panname_x_lang, hnc_strname_x_s_lang, hnc_strlink_x_s, hnc_contname_lang, hnc_text_lang
Теперь разберемся с блоками формы. Для вывода обычного блока используем такую конструкцию:
<label >Название поля</label >
<div class="margin-form" > 
  <input type = "text" size = "50" name = "hnc_имя" value = "<?php echo Configuration::get( 'HNC_ИМЯ ) ?>" />
  <p style = "clear: both" > Подсказка если необходима </p >

Мультиязычный блок выводим так:
<label style = "clear:both;" >Название поля</label >
<div class="margin-form" >
  <div class="translatable" >
    <?php 
    foreach( $languages as $language )
    {
      $lang_value[ $language[ 'id_lang' ] ][ 'hnc_имя' ] = 
         Configuration::get( 'HNC_ИМЯ_' . $language[ 'id_lang' ] );
      $form .= ' <div id = "hnc_имя_' . (int)$language[ 'id_lang' ] . '" class="lang_' . 
         (int)$language[ 'id_lang' ] . '" style = "display: ' . ( $language[ 'id_lang' ] == $deflang_id ? 
         'block' : 'none' ).'; float: left;">';
      $form .= ' <textarea rows = "5" cols = "60" type = "text" name = "hnc_имя_' . 
         $language[ 'id_lang' ] . '">'. 
         Tools::safeOutput( @$lang_value[ $language[ 'id_lang' ] ][ 'hnc_имя' ], true ). '</textarea >
         </div>';
    }
    $form .= $this->displayFlags( $languages, $deflang_id, 'hnc_имя', 'hnc_имя', true );
    ?>
 </div >
</div > 

Вывод загруженной картинки и поля для загрузки:
<div class="over" >
  <div class="margin-form" >
    <img src = "<?php echo Tools::getProtocol() . Tools::getMediaServer( $this->name ) . _MODULE_DIR_ . 
    $this->name . '/files/' . Configuration::get( 'HNC_ИМЯ' ) ?>" 
    alt = "" style = "height:120px;margin-left: 100px;" />
  </div >
  <label> Название </label >
  <div class="margin-form" >
    <input id = "hnc_имя" type = "file" name = "hnc_имя" />
    <p style = "clear: both" >Подсказка если необходима </p >
  </div >
</div > 
Не забываем про enctype="multipart/form-data" при загрузке картинок

Кнопки:
<div style = "clear: both;width: 100%;" ></div >
<input type = "submit" name = "submitHNCPage" value = "Сохранить" class="button" />
    
Теперь вы можете создать любую форму

Обработчик формы

 Обработчиком служит метод getContent(). Он же и выводит форму.

  public function getContent()
  {
    $out_msg = '';
    $languages = Language::getLanguages( false );
    $output = '<h2>' . $this->displayName . '</h2>';

    if( Tools::isSubmit( 'submitHNCPage' ) )
    {
      $deflang_id = (int)( Configuration::get( 'PS_LANG_DEFAULT' ) );
      // Panels
      for( $i = 1; $i <= 4; $i++ )
      {
        foreach( $languages as $language )
        {
          if( Tools::getValue( 'hnc_panname_' . $i . '_' . $language[ 'id_lang' ] ) )
            Configuration::updateValue( 'HNC_PANNAME_' . $i . '_' . $language[ 'id_lang' ],
              Tools::getValue( 'hnc_panname_' . $i . '_' . $language[ 'id_lang' ] ) );
          else
            Configuration::updateValue( 'HNC_PANNAME_' . $i . '_' . $language[ 'id_lang' ], '' );
        }

        // Strings
        for( $s = 1; $s <= 6; $s++ )
        {
          foreach( $languages as $language )
          {
            if( Tools::getValue( 'hnc_strname_' . $i . '_' . $s . '_' . $language[ 'id_lang' ] ) )
              Configuration::updateValue( 'HNC_STRNAME_' . $i . '_' . $s . '_' . $language[ 'id_lang' ],
                Tools::getValue( 'hnc_strname_' . $i . '_' . $s . '_' . $language[ 'id_lang' ] ) );
            else
              Configuration::updateValue( 'HNC_STRNAME_' . $i . '_' . $s . '_' . $language[ 'id_lang' ], '' );
          }

          if( Tools::getValue( 'hnc_strlink_' . $i . '_' . $s ) )
            Configuration::updateValue( 'HNC_STRLINK_' . $i . '_' . $s,
              Tools::getValue( 'hnc_strlink_' . $i . '_' . $s ) );
          else
            Configuration::updateValue( 'HNC_STRLINK_' . $i . '_' . $s, '' );
        }
      }

      foreach( $languages as $language )
      {
        if( Tools::getValue( 'hnc_contname_' . $language[ 'id_lang' ] ) )
          Configuration::updateValue( 'HNC_CONTNAME_' . $language[ 'id_lang' ],
            Tools::getValue( 'hnc_contname_' . $language[ 'id_lang' ] ) );
        else
          Configuration::updateValue( 'HNC_CONTNAME_' . $language[ 'id_lang' ], '' );

        if( Tools::getValue( 'hnc_text_' . $language[ 'id_lang' ] ) )
          Configuration::updateValue( 'HNC_TEXT_' . $language[ 'id_lang' ],
            Tools::getValue( 'hnc_text_' . $language[ 'id_lang' ] ) );
        else
          Configuration::updateValue( 'HNC_TEXT_' . $language[ 'id_lang' ], '' );
      }
      $this->_clearCache( 'realfooter.tpl' );
      $out_msg .= '<div class="conf confirm">'.$this->l('Settings updated').'</div>';
    }
    return $out_msg . $this->displayForm();
  }
Ну здесь совсем все просто. Главное, чтобы name 'submitHNCPage' кнопки в форме:
<input type = "submit" name = "submitHNCPage" value = "Сохранить" class="button" /> 
совпал с
if( Tools::isSubmit( 'submitHNCPage' ) )

Метод Tools::isSubmit() проверяет, что кнопка нажималась.
Метод Tools::getValue() - выдает переменную пришедшую в $_POST
Метод Configuration::updateValue() - обновляет нашу переменную или создает новую, в случае ее отсутствия
В переменную  $out_msg собираем сообщения или ошибки
Да, для обработки картинок такой код:
 if( isset( $_FILES[ 'hnc_имя' ] ) &&
            isset( $_FILES[ 'hnc_имя' ][ 'tmp_name' ] ) &&
            !empty( $_FILES[ 'hnc_имя' ][ 'tmp_name' ] )
        )
        {
          if( $error = ImageManager::validateUpload( $_FILES[ 'hnc_имя' ], 4000000 ) )
            return $this->displayError( $this->l( 'Invalid image' ) );
          else
          {
            $ext = mb_substr( $_FILES[ 'hnc_имя' ][ 'name' ],
                mb_strrpos( $_FILES[ 'hnc_имя' ][ 'name' ], '.' ) + 1 );
            $file_name = md5( $_FILES[ 'hnc_имя' ][ 'name' ] ) . '.' . $ext;
            if( !move_uploaded_file( $_FILES[ 'hnc_имя'][ 'tmp_name' ],
                dirname( __FILE__ ) . '/files/' . $file_name )
            )
              return $this->displayError( $this->l( 'An error occurred while attempting to upload the file.' ) );
            else
            {
              if( Configuration::hasContext( 'HNC_ИМЯ', null, Shop::getContext() ) &&
                  Configuration::get( 'HNC_ИМЯ' ) != $file_name
              )
                @unlink( dirname( __FILE__ ) . '/files/' . Configuration::get( 'HNC_ИМЯ' ) );
              Configuration::updateValue( 'HNC_ИМЯ' . $i, $file_name );
              $this->_clearCache( 'realfooter.tpl' );
            }
          }
        }
Для файлов необходимо создать папку files в корне модуля

Подготовка и вывод данных в шаблон

В нашем примере данные выводятся в хук Footer.
  public function hookFooter( $params )
  {

    $deflang_id = (int)( Configuration::get( 'PS_LANG_DEFAULT' ) );
    $tmp = array();
    for( $i = 1; $i <= 4; $i++ )
    {
      $tmp[ $i ][ 'hnc_panname' ] = Configuration::get( 'HNC_PANNAME_' . $i . '_' . $this->context->language->id );


      for( $s = 1; $s <= 6; $s++ )
      {
        $tmp[ $i ][ $s ][ 'hnc_strname' ] =
            Configuration::get( 'HNC_STRNAME_' . $i . '_' . $s . '_' . $this->context->language->id );
        $tmp[ $i ][ $s ][ 'hnc_strlink' ] = Configuration::get( 'HNC_STRLINK_' . $i . '_' . $s );
      }
    }

    if( !$this->isCached( 'mbrosfooter.tpl', $this->getCacheId( 'realfooter' ) ) )
    {
      $this->smarty->assign( array(
        'hnc_contname' => Configuration::get( 'HNC_CONTNAME_' . $this->context->language->id ),
        'hnc_text' => Configuration::get( 'HNC_TEXT_' . $this->context->language->id ),
        'data' => $tmp
      ) );
    }
    $this->context->controller->addCSS( ( $this->_path ) . 'realfooter.css', 'all' );
    return $this->display( __FILE__, 'realfooter.tpl', $this->getCacheId( 'realfooter' ) );
  }
Здесь также ничего сложного. Мультиязычные данные мы выводим в зависимости от текущего языка $this->context->language->id. Все динамичесике данные запоминаем в массив $tmp. Передаем в шаблон как переменную $data.
Также подключаем таблицу стилей realfooter.css, которую расположим в корне папки модуля.
Если модуль предполагает расположение еще в каких-либо хуках, это надо явно указать:
function hookDisplayTop( $params )
  {
    return $this->hookDisplayHome( $params );
  }

Таким образом модулю будет разрешено располагаться в хуке Home в том же виде, что и в хуке Footer. Однако Вы можете указать здесь другой шаблон, вывести другие данные и т.п.

3. Шаблон 

Ну и наконец то, собственно ради чего все танцевалось: шаблон.

<!-- HowKnowCoder Footer module -->

<div id="foot-wrapper">
   {assign var="flag" value="false"}
   {foreach from=$data item=d name=frd}
      <div class="foot-panel">
         <h4>{$d.hnc_panname|upper}</h4>
         <ul>
           {section name="i" loop=$d}
              {if $d[i].hnc_strname|mb_strlen > 0 }
                 <li><a href="{$d[i].hnc_strlink}">{$d[i].hnc_strname}</a></li>
              {/if}
           {/section}
         </ul>
      </div>
    {/foreach} 
 
   <div class="foot-panel-contact">
       <h4>{$hnc_contname|upper}</h4>
          <p>{$hnc_text|nl2br}</p>
   </div>
</div>
Очень желательно указывать <!-- HowKnowCoder Footer module --> - так гораздо проще ориентироваться в коде. Кроме того, это чуть ли не стандарт PrestaShop.
Остальное все понятно без комментариев

4. Размещение

Тут все стандартно. Модуль инсталлируется и автоматом размещается в футере. Далее надо зайти в меню Модули и удалить из футера все модули кроме нашего.

На этом разрешите закончить. Надеюсь кому-то сэкономил время :)

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

Отправить комментарий

Есть что сказать - скажи