Создание простого плагина. Пошаговая инструкция для новичков. Часть 2

Это продолжение статьи для новичков о пошаговом создании простого плагина для Livestreet.
Часть 1.

7. Конфигурируем модуль
Модуль (Module) — класс, который удобно использовать как прослойку между контроллером (Экшином) и Маппером. Это удобно, если есть необходимость предварительной обработки получаемых/отправляемых данных. Так же, в Модуле удобно хранить какие-то свои функции, т.е., использовать этот класс, как библиотеку.

Откроем файл Модуля
livestreet/plugins/test/classes/modules/test/Test.class.php

Если вы ничего не изменяли, то класс будет пустым.

a) Создадим свойство класса $oMapper, в котором будет храниться объект Маппера:
добавим после объявления класса следующий код:
protected $oMapper;


b) Создадим метод инициализации класса:
public function Init() {

    $this->oMapper=Engine::GetMapper(__CLASS__);

}

Этот метод записывает в свойство класса $oMapper созданный объект Маппера.

c) Создадим методы createBook и getBooks
<?php

class PluginTest_ModuleTest extends Module {

    protected $oMapper;

    public function Init() {
        $this->oMapper=Engine::GetMapper(__CLASS__);
    }

    public function createBook($post){
        //Проходимся по массиву $post с данными
        //будущего объекта и очищаем от html-сущностей
        array_walk($post,function($field){
            htmlspecialchars($field);
        });

        //создаем объект сущности Book
        $book = Engine::GetEntity('PluginTest_ModuleTest_EntityBook');
        $book->setName($post['book_name']);
        $book->setAuthor($post['book_author']);
        $book->setDescription($post['book_description']);

        //Объект book готов, теперь можем передать его
        // Мапперу и записать в базу данных
        if ($iId=$this->oMapper->createBook($book)) {
            // если сохранение в БД прошло успешно,
            //возвращаем ID новосозданного объекта
            $book->setId($iId);
            return $iId;
        }
        // если что-то пошло не так, возвращаем false
        return false;
    }

    public function getBooks() {
        // Просто проксируем запрос от Экшина к Мапперу
        if($res=$this->oMapper->getBooks()){
            return $res;
        }
        return false;
    }
}
?>


8. Конфигурируем сущность (Entity)
Переходим в папку
livestreet/plugins/test/classes/modules/test/entity

и создаем файл класса Сущности(Entity)
Book.entity.class.php


Содержимое файла:
<?php

    class PluginTest_ModuleTest_EntityBook extends Entity
{

}

?>



Да, это пустой класс. Без единого метода. Нам этого достаточно :-)

9. Конфигурируем маппер (Mapper)
Переходим в папку
livestreet/plugins/test/classes/modules/test/mapper

и создаем файл класса Маппера
Test.mapper.class.php


Содержимое файла:
<?php


class PluginTest_ModuleTest_MapperTest extends Mapper
{

    /**
     * Записываем в базу данных новый объект Book или обновляем существующий

     * @param PluginTest_ModuleTest_EntityBook $oBook

     * @return array|bool

     */

    public function createBook(PluginTest_ModuleTest_EntityBook $oBook){

        $sql = "INSERT INTO ".Config::Get('plugin.test.table.test_table')."

			(

			name,

			author,

			description

			)

			VALUES(?, ?, ?)

		";

        if ($iId=$this->oDb->query(

            $sql,

            $oBook->getName(), $oBook->getAuthor(), $oBook->getDescription()

        ))

        {

            return $iId;

        }
        return false;

    }


    /**
     * Делаем выборку всех записей Book из таблицы

     * @return array

     */
 
   public function getBooks(){

        $sql = "SELECT * FROM ".Config::Get('plugin.test.table.test_table');

        $aResult=array();

        if ($aRows=$this->oDb->select($sql)) {

            // На основании каждой строки результата выборки создаем объект

            // класса Book. Это иногда удобно. Но можно и просто возвращать

            // именованный массив.

            foreach ($aRows as $aRow) {

                $aResult[]=Engine::GetEntity('PluginTest_ModuleTest_EntityBook',$aRow);

            }

        }

        return $aResult;

    }
}

?>


Мы создали простенький Маппер для сохранения объекта Book в БД и получения всех объектов Book из БД по запросу.

10. Конфигурируем шаблон (Template)
Перейдем в папку
livestreet/plugins/page/templates

и создадим следующую иерархию папок:


В папке
ActionTest
создадим два файла:

index.tpl и books.tpl

index.tpl — отвечает за отображение данных, обработанных Экшином index, books.tpl отвечает за отображение данных, обработанных экшином books. Эти файлы автоматически подключаются движком LS при обработке соответствующих Экшинов.

Переменные, переданные из Экшина в Темплейт будут доступны через специальные теги шаблонизатора Smarty. Их можно обрабатывать и желаемым образом выводить клиенту.
Также доступны языковые переменные.

Содержание файла index.tpl:

{include file='header.tpl'}


<h1>{$aLang.plugin.test.form_title}</h1>

{* Форма ввода данных книги *}

<form action="" method="POST">

	<input type="hidden" name="security_ls_key" value="{$LIVESTREET_SECURITY_KEY}" />


	<p><label for="book_name">{$aLang.plugin.test.book_name}:</label>

		<input type="text" id="book_name" class="input-text input-width-full" name="book_name" value="" class="input-wide" />
	</p>


	<p><label for="book_author">{$aLang.plugin.test.book_author}:</label>

		<input type="text" id="book_author" class="input-text input-width-full" name="book_author" value="" class="input-wide" />
	</p>


	<label for="book_description">{$aLang.plugin.test.book_description}:</label>

	<textarea name="book_description" id="book_description" rows="20" class="input-width-full"></textarea><br />


	<p>

		<button type="submit" class="button button-primary" name="submit_book_save">{$aLang.plugin.test.bt_save_book}</button>

	</p>
</form>


{* Парсинг массива объектов $aBooks, переданных в шаблон в Экшине.
	Проходимся по каждому элемну массива объектов $aBooks и выводим свойства в таблицу
*}

<table cellspacing="0" class="table">

	<thead>
	
        <tr>

		<th width="180px">{$aLang.plugin.test.table_title}</th>

		<th align="center">{$aLang.plugin.test.table_id}</th>

		<th align="center">{$aLang.plugin.test.table_name}</th>

		<th align="center">{$aLang.plugin.test.table_author}</th>

		<th align="center">{$aLang.plugin.test.table_description}</th>

	</tr>

	</thead>
	<tbody>

	{foreach from=$aBooks item=oBook}

		<tr>

			<td>{$oBook->getId()}</td>

			<td>{$oBook->getName()}</td>

			<td>{$oBook->getAuthor()}</td>

			<td>{$oBook->getDescription()}</td>

		</tr>

	{/foreach}

	</tbody>

</table>

{* Подключаем темплейт футера *}
{include file='footer.tpl'}


Содержание файла index.tpl:

{include file='header.tpl'}


<h1>{$aLang.plugin.test.books_page_title}</h1>

{* Парсинг массива объектов $aBooks, переданных в шаблон в Экшине.

	Проходимся по каждому элемну массива объектов $aBooks и выводим свойства в таблицу
*}

<table cellspacing="0" class="table">

	<thead>

	<tr>

		<th width="180px">{$aLang.plugin.test.table_title}</th>

		<th align="center">{$aLang.plugin.test.table_id}</th>

		<th align="center">{$aLang.plugin.test.table_name}</th>

		<th align="center">{$aLang.plugin.test.table_author}</th>

		<th align="center">{$aLang.plugin.test.table_description}</th>

	</tr>

	</thead>

	<tbody>

	{foreach from=$aBooks item=oBook}
            <tr>

		<td>{$oBook->getId()}</td>

		<td>{$oBook->getName()}</td>

		<td>{$oBook->getAuthor()}</td>

		<td>{$oBook->getDescription()}</td>

		</tr>

	{/foreach}

	</tbody>

</table>


{* Подключаем темплейт футера *}

{include file='footer.tpl'}


Структура папок и размещение файлов шаблона:


11. Конфигурируем языковой файл
Откроем языковой файл
livestreet/plugins/test/templates/language/russian.php

Языковой файл состоит из именованного массива, где ключем выступает название переменной, выводимой в шаблоне, а значением — соответствующий текст.
Нам необходимо пройтись по файлам шаблона, и определить все языковые переменные.
Содержание языкового файла:
<?php
/**
 * Русский языковой файл плагина
 */
return array(
    'form_title' => 'Добавление новой книги',
    'book_name' => 'Название книги',
    'book_author' => 'Автор',
    'book_description' => 'Описание',
    'bt_save_book' => 'Сохранить книгу',
    'table_title' => 'Список книг',
    'table_name' => 'Название',
    'table_author' => 'Автор',
    'table_description' => 'Описание',

    'books_page_title' => 'Список сохраненных книг',
);

?>


Заключение
Плагин готов. Теперь осталось проверить его работоспособность,

a) Активируем плагин.
— Для этого перейдем в админку установленных плагинов: ваш_сайт.tlt/admin/plugins/
— И активируем наш плагин найдя его в списке плагинов.

b) Проверим успешность создания таблицы плагина.
— Откроем нашу БД, например и убедимся, что таблица prefix__test_tablle была создана.

с) Перейдем на стнаницу плагина ваш_сайт_ls.tld/test и введя данные, сохраним книгу.

d) Перейдем на страницу вывода списка книг ваш_сайт_ls.tld/test/books



UPD1:
Участник сообщества PSNet указал на 2 коварные ошибки/оплошности в данной части руководства:
1. Файлы шаблонов *.tpl
Ни в коем случае не нужно добавлять комментарии в шаблонах перед подключением шаблона header.tpl:

Неправильно:
{* какой-то комментарий *}
{include file='header.tpl'}




Правильно:
{include file='header.tpl'}



Перед подключением шаблона header.tpl не должно быть никаких символов, включая enter, пробел и т.п.

2. Проверка ключа LIVESTREET_SECURITY_KEY для отправляемых форм.
Это — защита от CSRF атак. Это необходимо делать всегда.
Для этого необходимо:
a) Добавить в форму скрытое поле
<input type="hidden" name="security_ls_key" value="{$LIVESTREET_SECURITY_KEY}" />

b) Добавить в соответствующий контроллер (Экшин) проверку формы (данная операция делается в ч.1 данной статьи):
$this->Security_ValidateSendForm();

13 комментариев

avatar
{* Подключаем темплейт хедера *}
{include file='header.tpl'}

перед подключением хедера не должно быть комментариев, т.к. переводы строк попадают перед доктайпом чего делать нельзя

добавьте в экшен при добавлении формы проверку ключа security_ls_key и напишите что так нужно делать всегда
avatar
По первому да, согласен, комментарий добавлял уже тут, в редакторе. Моя оплошность.
Про ключ — тоже согласен.

Отредактирую утром уже. Большое спасибо за уточнения!
avatar
почему то когда нет книг то вот так
Fatal error: Call to a member function getId() on a non-object in D:\*****\dc39eca987ad1eaac2deab28163c98ce210d1a65.file.books.tpl.php on line 55
avatar
поправка, если нет вообще книг то такая ошибка
avatar
Надо проверить что лежит в массиве $aBooks.
avatar
ну я думаю что это связано с тем что выводить нечего т.к не создан объект, то получается нужно ранее как то завести объект который будет с нулевыми переменными чтоле, поправьте если не прав
avatar
в book.tpl перед foreach надо сделать проверку
{if $aBooks}
{foreach from=$aBooks item=oBook}
<tr>

	<td>{$oBook->getId()}</td>
	<td>{$oBook->getName()}</td>

	<td>{$oBook->getAuthor()}</td>
	<td>{$oBook->getDescription()}</td>
</tr>
{/foreach}
{else}
<tr>

	<td colspan="4">Список книг пуст.</td>
</tr>
{/if}
avatar
точно, спасиб
avatar
Спасибо за подробный мануал. А как сделать плагин который будет расширять методы классов движка, модули, может в шаблоне будет что дополнять? Подозреваю что нужно сделать функцию, которая будет по адресу находить нужную строчку и оборачивать или может вставлять свою строчку с кодом.
avatar
Самый простой вариант — скачайте пару бесплатных плагинов из каталога, и посмотрите, как они устроены )
avatar
Тоже так хотел сделать, просто хотел узнать good practices)
avatar
я, честно говоря, сам изучаю двожок методом проб и ошибок :-)
avatar
В п.10 очепятка: «Содержание файла index.tpl:» указано дважды )
Спасибо автору за труд — отличная статья!
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.