Всем снова привет! Решил написать свой пост о модуле отзывов для Simpla CMS. Сейчас продают даже бесплатные решения, обидно за наших. Хотел бы поделиться скорее не модулем, а решением для реализации отзывов для интернет-магазина для Simpla.
Вам потребуется уйма времени и ровные руки. Я честно старался подготовить и максимально доходчиво изложить.

Поехали:

  1. Для начала создадим в базе данных таблицу для отзывов. Я назвал ее s_reviews (в зависимости от префикса у Вас имя таблицы может выглядеть по другому). Выполним для этого приведенный ниже sql запрос
--
-- Структура таблицы `s_reviews`
--

CREATE TABLE IF NOT EXISTS `s_reviews` (
  `id` bigint(20) NOT NULL,
  `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `ip` varchar(20) NOT NULL,
  `name` varchar(255) NOT NULL,
  `message` text NOT NULL,
  `approved` int(1) NOT NULL DEFAULT '0'
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
--
-- Индексы таблицы `s_reviews`
--
ALTER TABLE `s_reviews`
  ADD PRIMARY KEY (`id`);

--
-- AUTO_INCREMENT для сохранённых таблиц
--

--
-- AUTO_INCREMENT для таблицы `s_reviews`
--
ALTER TABLE `s_reviews`
  MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=0;

Получится такая вот структура: 

  1. Создадим файл в папке api/ с названием Reviews.php, после заполним его содержимым:
<?php 

/** 
 * Simpla CMS 
 * 
 * @copyright 2011 Denis Pikusov
 * @link http://simplacms.ru 
 * @author Denis Pikusov 
 * 
 */

require_once('Simpla.php');

class Reviews extends Simpla
{
    // Возвращает комментарий по id
    public function get_review($id)
    {
        $query = $this->db->placehold("SELECT r.id, r.name, r.ip, r.type, r.text, r.date, r.approved FROM __reviews r WHERE id=? LIMIT 1",
            intval($id));
        if ($this->db->query($query)) {
            return $this->db->result();
        } else {
            return false;
        }
    }

    public function get_reviews($filter = [], $new_on_top = false)
    {
        // По умолчанию
        $limit = 0;
        $page = 1;
        $keyword_filter = '';
        $approved_filter = '';
        if (isset($filter['limit'])) {
            $limit = max(1, intval($filter['limit']));
        }
        if (isset($filter['page'])) {
            $page = max(1, intval($filter['page']));
        }
        if (isset($filter['approved'])) {
            $approved_filter = $this->db->placehold("AND (r.approved=?)", intval($filter['approved']));
        }
        $sql_limit = $this->db->placehold(' LIMIT ?, ? ', ($page - 1) * $limit, $limit);
        if (!empty($filter['keyword'])) {
            $keywords = explode(' ', $filter['keyword']);
            foreach ($keywords as $keyword) {
                $keyword_filter .= $this->db->placehold('AND r.name LIKE "%' . $this->db->escape(trim($keyword)) . '%" OR r.message LIKE "%' . $this->db->escape(trim($keyword)) . '%" ');
            }
        }
        if ($new_on_top) {
            $sort = 'DESC';
        } else {
            $sort = 'ASC';
        }
        $query = $this->db->placehold("SELECT r.id, r.name, r.ip, r.message, r.date, r.approved FROM __reviews r WHERE 1 $keyword_filter $approved_filter ORDER BY r.id $sort $sql_limit");
        $this->db->query($query);
        return $this->db->results();
    }

    public function count_reviews($filter = [])
    {
        $keyword_filter = '';
        if (!empty($filter['keyword'])) {
            $keywords = explode(' ', $filter['keyword']);
            foreach ($keywords as $keyword) {
                $keyword_filter .= $this->db->placehold('AND r.name LIKE "%' . $this->db->escape(trim($keyword)) . '%" OR r.message LIKE "%' . $this->db->escape(trim($keyword)) . '%" ');
            }
        }
        $query = $this->db->placehold("SELECT count(distinct r.id) as count FROM __reviews r WHERE 1 $keyword_filter");
        $this->db->query($query);
        return $this->db->result('count');
    }

    public function add_reviews($review)
    {
        $query = $this->db->placehold('INSERT INTO __reviews SET ?%, date = NOW()', $review);
        if (!$this->db->query($query)) {
            return false;
        }
        $id = $this->db->insert_id();
        return $id;
    }

    public function update_review($id, $review)
    {
        $date_query = '';
        if (isset($review->date)) {
            $date = $review->date;
            unset($review->date);
            $date_query = $this->db->placehold(', date=STR_TO_DATE(?, ?)', $date, $this->settings->date_format);
        }
        $query = $this->db->placehold("UPDATE __reviews SET ?% $date_query WHERE id in(?@) LIMIT 1", $review,
            (array)$id);
        $this->db->query($query);
        return $id;
    }

    public function delete_review($id)
    {
        if (!empty($id)) {
            $query = $this->db->placehold("DELETE FROM __reviews WHERE id=? LIMIT 1", intval($id));
            $this->db->query($query);
        }
    }
}
  1. В файле api/Simpla.php добавляем после 
'comments' => 'Comments', 
'feedbacks' => 'Feedbacks',

Этот код

'reviews' => 'Reviews',
  1. В файле api/Managers.php после 
 'blog', 'comments', 'feedbacks',

добавляем

'reviews',
  1. В папке simpla создаем файл ReviewsAdmin.php со следующим содержимым:
<?php
require_once('api/Simpla.php');

########################################
class ReviewsAdmin extends Simpla
{
    function fetch()
    {
        // Поиск
        $keyword = $this->request->get('keyword', 'string');
        if (!empty($keyword)) {
            $filter['keyword'] = $keyword;
            $this->design->assign('keyword', $keyword);
        }
        // Обработка действий
        if ($this->request->method('post')) {
            // Действия с выбранными
            $ids = $this->request->post('check');
            if (!empty($ids)) switch ($this->request->post('action')) {
                case 'approve':
                {
                    foreach ($ids as $id) {
                        $this->reviews->update_review($id, ['approved' => 1]);
                    }
                    break;
                }
                case 'delete':
                {
                    foreach ($ids as $id) {
                        $this->reviews->delete_review($id);
                    }
                    break;
                }
            }
        }
        // Отображение
        $filter = [];
        $filter['page'] = max(1, $this->request->get('page', 'integer'));
        $filter['limit'] = 40;
        // Поиск
        $keyword = $this->request->get('keyword', 'string');
        if (!empty($keyword)) {
            $filter['keyword'] = $keyword;
            $this->design->assign('keyword', $keyword);
        }
        $reviews_count = $this->reviews->count_reviews($filter);
        // Показать все страницы сразу
        if ($this->request->get('page') == 'all') {
            $filter['limit'] = $reviews_count;
        }
        $reviews = $this->reviews->get_reviews($filter, true);
        $this->design->assign('pages_count', ceil($reviews_count / $filter['limit']));
        $this->design->assign('current_page', $filter['page']);
        $this->design->assign('reviews', $reviews);
        $this->design->assign('reviews_count', $reviews_count);
        return $this->design->fetch('reviews.tpl');
    }
}

?>
  1. В файл simpla/IndexAdmin.php после
'CommentsAdmin' => 'comments', 
'FeedbacksAdmin' => 'feedbacks',

добавляем

'ReviewsAdmin' => 'reviews',
  1. В файле simpla/ajax/update_object.php после
case 'comment': 
    if($simpla->managers->access('comments')) 
        $result = $simpla->comments->update_comment($id, $values); 
    break;

добавляем

case 'review':
    if($simpla->managers->access('reviews'))
        $result = $simpla->reviews->update_review($id, $values);
    break;
  1. Теперь переходим к выводу отзывов в админке, переходим и редактируем файлы в папке simpla/design/html
  • В файл feedbacks.tpl добавляем вывод отзывов:
{* Вкладки *}
{capture name=tabs}
    {if in_array('comments', $manager->permissions)}
    <li><a href="index.php?module=CommentsAdmin">Комментарии</a></li>
    {/if}
    <li class="active">
        <a href="index.php?module=FeedbacksAdmin">Обратная связь</a>
    </li>
    {if in_array('reviews', $manager->permissions)}
    <li>
        <a href="index.php?module=ReviewsAdmin">Отзывы</a>
    </li>
    {/if} 
{/capture}
  • В файл comments.tpl добавляем также вывод отзывов:
{* Вкладки *}
{capture name=tabs}
    {if in_array('comments', $manager->permissions)}
        <li><a href="index.php?module=CommentsAdmin">Комментарии</a></li>
    {/if}
    <li class="active"><a href="index.php?module=FeedbacksAdmin">Обратная связь</a></li>
    {if in_array('reviews', $manager->permissions)}
        <li><a href="index.php?module=ReviewsAdmin">Отзывы</a></li>
    {/if}
{/capture}
  • Соответственно создаем файл вывода отзывов reviews.tpl со следующим содержимым:
{* Вкладки *}
{capture name=tabs}
{if in_array('comments', $manager->permissions)}
<li><a href="index.php?module=CommentsAdmin">Комментарии</a></li>{/if}
{if in_array('feedbacks', $manager->permissions)}
<li><a href="index.php?module=FeedbacksAdmin">Обратная связь</a></li>{/if}
<li class="active"><a href="index.php?module=ReviewsAdmin">Отзывы</a></li>
{/capture}


{* Title *}
{$meta_title='Отзывы' scope=parent}

{* Поиск *}
{if $reviews || $keyword}
<form method="get">
    <div id="search">
        <input type="hidden" name="module" value='ReviewsAdmin'>
        <input class="search" type="text" name="keyword" value="{$keyword|escape}"/>
        <input class="search_button" type="submit" value=""/>
    </div>
</form>
{/if}


{* Заголовок *}
<div id="header">
    {if $keyword && $reviews_count}
    <h1>{$reviews_count|plural:'Нашелся':'Нашлось':'Нашлись'} {$reviews_count}
        {$reviews_count|plural:'отзыв':'отзывов':'отзыва'}</h1>
    {elseif !$type}
    <h1>{$reviews_count} {$reviews_count|plural:'отзыв':'отзывов':'отзыва'}</h1>
    {/if}
</div>


{if $reviews}
<div id="main_list">

    <!-- Листалка страниц -->
    {include file='pagination.tpl'}
    <!-- Листалка страниц (The End) -->

    <form id="list_form" method="post">
        <input type="hidden" name="session_id" value="{$smarty.session.id}">

        <div id="list" class="sortable">
            {foreach $reviews as $review}
            <div class="{if !$review->approved}unapproved{/if} row">
                <div class="checkbox cell">
                    <input type="checkbox" name="check[]" value="{$review->id}"/>
                </div>
                <div class="name cell">
                    <div class="review_name">
                        {$review->name|escape}
                        <a class="approve" href="#">Одобрить</a>
                    </div>
                    <div class="review_text">
                        {$review->message|escape|nl2br}
                    </div>
                    <div class="review_info">
                        Отзыв оставлен {$review->date|date} в {$review->date|time}
                    </div>
                </div>
                <div class="icons cell">
                    <a class="delete" title="Удалить" href="#"></a>
                </div>
                <div class="clear"></div>
            </div>
            {/foreach}
        </div>

        <div id="action">
            Выбрать <label id="check_all" class="dash_link">все</label> или <label id="check_unapproved"
                                                                                   class="dash_link">ожидающие</label>

            <span id="select">
		<select name="action">
			<option value="approve">Одобрить</option>
			<option value="delete">Удалить</option>
		</select>
		</span>

            <input id="apply_action" class="button_green" type="submit" value="Применить">

        </div>
    </form>

    <!-- Листалка страниц -->
    {include file='pagination.tpl'}
    <!-- Листалка страниц (The End) -->

</div>
{else}
Нет отзывов
{/if}

<!-- Меню -->
<div id="right_menu">

</div>
<!-- Меню  (The End) -->

{literal}
<script>
	$(function () {

		// Раскраска строк
		function colorize() {
			$('#list div.row:even').addClass('even');
			$('#list div.row:odd').removeClass('even');
		}

		// Раскрасить строки сразу
		colorize();

		// Выделить все
		$('#check_all').click(function () {
			$('#list input[type="checkbox"][name*="check"]').attr('checked', $('#list input[type="checkbox"][name*="check"]:not(:checked)').length > 0);
		});

		// Выделить ожидающие
		$('#check_unapproved').click(function () {
			$('#list input[type="checkbox"][name*="check"]').attr('checked', false);
			$('#list .unapproved input[type="checkbox"][name*="check"]').attr('checked', true);
		});

		// Удалить 
		$('a.delete').click(function () {
			$('#list input[type="checkbox"][name*="check"]').attr('checked', false);
			$(this).closest('.row').find('input[type="checkbox"][name*="check"]').attr('checked', true);
			$(this).closest('form').find('select[name="action"] option[value=delete]').attr('selected', true);
			$(this).closest('form').submit();
		});

		// Одобрить
		$('a.approve').click(function () {
			var line = $(this).closest('.row');
			var id = line.find('input[type="checkbox"][name*="check"]').val();
			$.ajax({
				type: 'POST',
				url: 'ajax/update_object.php',
				data: {
					'object': 'review',
					'id': id,
					'values': {'approved': 1},
					'session_id': '{/literal}{$smarty.session.id}{literal}'
				},
				success: function (data) {
					line.removeClass('unapproved');
				},
				dataType: 'json'
			});
			return false;
		});

		$('form#list_form').submit(function () {
			if ($('#list_form select[name="action"]').val() == 'delete' && !confirm('Подтвердите удаление')) {
				return false;
			}
		});

	});

</script>
{/literal}

В результате мы сделали админ-часть этой статьи 

1
  1. Админская часть у нас готова. Переходим к выводу отзывов и формы. Для начала создадим страницу с адресом reviews
3

Потом разрешим через .htaccess подключение этой страницы. В корне сайта находим .htaccess и добавляем строку чуть ниже feedback

# feedback
RewriteRule ^contact/?$	index.php?module=FeedbackView  [L,QSA]

после добавления получится так

# feedback RewriteRule ^contact/?$ index.php?module=FeedbackView [L,QSA] # reviews RewriteRule ^reviews/?$ index.php?module=ReviewsView [L,QSA]
  1. Теперь поворотим немного в view/, создаем по адресу view/ файл ReviewsView.php со следующим содержимым
<?PHP

/**
 * Simpla CMS
 *
 * @copyright  2011 Denis Pikusov
 * @link      http://simplacms.ru
 * @author        Denis Pikusov
 *
 * Этот класс использует шаблон reviews.tpl
 *
 */

require_once('View.php');


class ReviewsView extends View
{

    function fetch()
    {
        if ($this->request->method('post') && $this->request->post('review')) {
            $review = new stdClass;
            $review->name = $this->request->post('name');
            $review->message = $this->request->post('message');
            $captcha_code = $this->request->post('captcha_code', 'string');

            // Передадим отзыв обратно в шаблон - при ошибке нужно будет заполнить форму
            $this->design->assign('name', $review->name);
            $this->design->assign('message', $review->message);

            // Проверяем капчу и заполнение формы
            if ($_SESSION['captcha_code'] != $captcha_code || empty($captcha_code)) {
                $this->design->assign('error', 'captcha');
            } elseif (empty($review->name)) {
                $this->design->assign('error', 'empty_name');
            } elseif (empty($review->message)) {
                $this->design->assign('error', 'empty_message');
            } else {
                $this->design->assign('review_sent', true);
                // Создаем отзыв
                $review->ip = $_SERVER['REMOTE_ADDR'];

                // Если были одобренные отзывы от текущего ip, одобряем сразу
                $this->db->query("SELECT 1 FROM __reviews WHERE approved=1 AND ip=? LIMIT 1", $review->ip);
                if ($this->db->num_rows() > 0) {
                    $review->approved = 1;
                }

                // Добавляем отзыв в базу
                $review_id = $this->reviews->add_reviews($review);

                // Отправляем email
                // $this->notify->email_review_admin($review_id);

                // Приберем сохраненную капчу, иначе можно отключить загрузку рисунков и постить старую
                unset($_SESSION['captcha_code']);
                //header('location: '.$_SERVER['REQUEST_URI'].'#review_'.$review_id);


            }
        }


        // Отображение
        $filter = [];
        $filter['page'] = max(1, $this->request->get('page', 'integer'));
        $filter['limit'] = 40;
        $filter['approved'] = 1;

        // Поиск
        $keyword = $this->request->get('keyword', 'string');
        if (!empty($keyword)) {
            $filter['keyword'] = $keyword;
            $this->design->assign('keyword', $keyword);
        }

        $reviews_count = $this->reviews->count_reviews($filter);
        // Показать все страницы сразу
        if ($this->request->get('page') == 'all') {
            $filter['limit'] = $reviews_count;
        }


        $reviews = $this->reviews->get_reviews($filter, true);


        $this->design->assign('pages_count', ceil($reviews_count / $filter['limit']));
        $this->design->assign('current_page', $filter['page']);

        $this->design->assign('filter', $filter);

        $this->design->assign('reviews', $reviews);
        $this->design->assign('reviews_count', $reviews_count);


        $this->design->assign('meta_title', $review->meta_title);
        $this->design->assign('meta_keywords', $review->meta_keywords);
        $this->design->assign('meta_description', $review->meta_description);

        return $this->design->fetch('reviews.tpl');
    }

}
  1. Теперь осталось вывести отзывы на странице site.ru/reviews, для этого создадим в папке design/{THEME}/html файл reviews.tpl примерно с таким содержимым
{if $review_sent}
{$name|escape}, ваш отзыв добавлен на сайт.
{else}

<h1 data-page="{$page->id}">{$page->header|escape}</h1>


{$page->body}

<form class="form review_form" method="post">
    {if $error}
    <div class="message_error">
        {if $error=='captcha'}
        Неверно введена капча
        {elseif $error=='empty_name'}
        Введите имя
        {elseif $error=='empty_text'}
        Введите сообщение
        {/if}
    </div>
    {/if}
    <label>Имя</label>
    <input data-format=".+" data-notice="Введите имя" value="{$name|escape}" name="name" maxlength="255" type="text"/>

    <label>Отзыв</label>
    <textarea data-format=".+" data-notice="Введите сообщение" value="{$message|escape}" name="message">{$message|escape}</textarea>

    <input class="button" type="submit" name="review" value="Отправить" />

    <div class="captcha"><img src="captcha/image.php?{math equation='rand(10,10000)'}"/></div>
    <input class="input_captcha" id="comment_captcha" type="text" name="captcha_code" value="" data-format="\d\d\d\d" data-notice="Введите капчу"/>
</form>
{/if}
{if $reviews}
{include file='pagination_sec.tpl'}
<ul class="comment_list">
    {foreach $reviews as $review}

    <a name="review_{$review->id}"></a>
    <li>
        <!-- Имя и дата Отзыва-->
        <div class="comment_header">
            {$review->name|escape} <i>Отзыв оставлен {$review->date|date} в {$review->date|time}</i>
        </div>
        <!-- Имя и дата Отзыва (The End)-->

        <!-- Отзыв -->
        {$review->message|escape|nl2br}
        <!-- Отзыв (The End)-->
    </li>


    {/foreach}</ul>
{include file='pagination_sec.tpl'}

{else}
Нет отзывов
{/if}

Добавляем также пагинацию, design/{THEME}/html файл pagination_sec.tpl

{if $pages_count>1}

{* Скрипт для листания через ctrl → *}
{* Ссылки на соседние страницы должны иметь id PrevLink и NextLink *}
<script type="text/javascript" src="design/js/ctrlnavigate.js"></script>

<!-- Листалка страниц -->
<div id="pagination">

    {* Количество выводимых ссылок на страницы *}
    {$visible_pages = 11}

    {* По умолчанию начинаем вывод со страницы 1 *}
    {$page_from = 1}

    {* Если выбранная пользователем страница дальше середины "окна" - начинаем вывод уже не с первой *}
    {if $current_page > floor($visible_pages/2)}
    {$page_from = max(1, $current_page-floor($visible_pages/2)-1)}
    {/if}

    {* Если выбранная пользователем страница близка к концу навигации - начинаем с "конца-окно" *}
    {if $current_page > $pages_count-ceil($visible_pages/2)}
    {$page_from = max(1, $pages_count-$visible_pages-1)}
    {/if}

    {* До какой страницы выводить - выводим всё окно, но не более ощего количества страниц *}
    {$page_to = min($page_from+$visible_pages, $pages_count-1)}

    {* Ссылка на 1 страницу отображается всегда *}
    <a class="{if $current_page==1}selected{else}droppable{/if}" href="{url page=1}">1</a>

    {* Выводим страницы нашего "окна" *}
    {section name=pages loop=$page_to start=$page_from}
    {* Номер текущей выводимой страницы *}
    {$p = $smarty.section.pages.index+1}
    {* Для крайних страниц "окна" выводим троеточие, если окно не возле границы навигации *}
    {if ($p == $page_from+1 && $p!=2) || ($p == $page_to && $p != $pages_count-1)}
    <a class="{if $p==$current_page}selected{/if}" href="{url page=$p}">...</a>
    {else}
    <a class="{if $p==$current_page}selected{else}droppable{/if}" href="{url page=$p}">{$p}</a>
    {/if}
    {/section}

    {* Ссылка на последнююю страницу отображается всегда *}
    <a class="{if $current_page==$pages_count}selected{else}droppable{/if}"  href="{url page=$pages_count}">{$pages_count}</a>

    <a href="{url page=all}">все сразу</a>
    {if $current_page>1}<a id="PrevLink" href="{url page=$current_page-1}">←назад</a>{/if}
    {if $current_page<$pages_count}<a id="NextLink" href="{url page=$current_page+1}">вперед→</a>{/if}

</div>
<!-- Листалка страниц (The End) -->
{/if}

Верстка может быть изменена.

  1. Ну и сам вывод я решил реализовать через логику, правим файл design/{THEME}/html/page.tpl , изменяем и делаем примерно таким
{* Канонический адрес страницы *}
{if $page->url == reviews }
    {$canonical="/{$page->url}" scope=parent}

    {include file='reviews.tpl'}
{else}
{$canonical="/{$page->url}" scope=parent}

<!-- Заголовок страницы -->
<h1 data-page="{$page->id}">{$page->header|escape}</h1>

<!-- Тело страницы -->
{$page->body}
{/if}

В результате получим модерируемые через админ-панель отзывы

На этом пожалуй все. Пишите вопросы. Может что дополнить надо, я как раз протестирую на одном проекте и внесу правки.

Спасибо за внимание!

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

p.s. Если статья была полезной и вас переполняет чувство благодарности, можете поддержать меня долларом на патреоне

Об авторе

Web Developer. I have expirience in FrontEnd, Backend, Devops. PHP, Python, Javascript (Vue.js, React.js)

Смотреть посты