Пример плагина «Отзывы»

Рабочий плагин к руководству «Плагины». Что в нём показано:

Что Где
описание плагина, автозагрузка классов plugin.json
подписка на события из класса plugin.php, src/Reviews.php → register()
свой тип записей с меню в админке registerTypes() → register_post_type('review', …)
свой блок для редактора registerTypes() → register_block_type('otzyvy-list', …), шаблон views/blocks/list.blade.php
поля в редакторе записи (метабокс) и их сохранение metaBoxes(), views/meta-box.blade.php, save()
стили на сайте styles() → plugin_asset(), assets/otzyvy.css
действие при включении register_activation_hook() → activate()
уборка при удалении uninstall.php

Как попробовать

  1. Скопируйте папку otzyvy в plugins/ (или запакуйте её в zip и загрузите в «Плагины → Добавить плагин»).
  2. Включите плагин «Отзывы» — в меню появится раздел «Отзывы».
  3. Добавьте пару отзывов с оценкой, затем в любой странице добавьте блок «Отзывы».

Файлы примера

plugin.json
{
    "name": "Отзывы",
    "description": "Пример плагина из документации: свой тип записей «Отзывы» с оценкой и блок «Отзывы» для страниц.",
    "version": "1.0.0",
    "author": "CMS ARGELIT",
    "uri": "https://cms.argelit.com",
    "requires_cms": "0.13",
    "requires_php": "8.3",
    "autoload": {
        "Examples\\Otzyvy\\": "src"
    }
}
plugin.php
<?php

/*
 * «Отзывы» — пример плагина из документации CMS ARGELIT (docs/developers/plugins.md).
 *
 * Этот файл подключается на каждом запросе, пока плагин включён. Вся работа — в классе src/Reviews.php
 * (он подключается сам, см. «autoload» в plugin.json). Переменная $plugin — описание плагина из plugin.json.
 */

use Examples\Otzyvy\Reviews;

(new Reviews($plugin))->register();
assets/otzyvy.css
/* Блок «Отзывы». Цвета и скругления берутся из переменных темы, если они есть. */
.otzyvy {
    margin: 2em 0;
}

.otzyvy__title {
    margin-bottom: 0.75em;
}

.otzyvy__list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 16px;
}

.otzyvy__item {
    margin: 0;
    padding: 20px;
    border-radius: var(--radius, 12px);
    background: var(--surface, #F2F4F7);
}

.otzyvy__stars {
    margin: 0 0 8px;
    color: #f5a623;
    letter-spacing: 2px;
}

.otzyvy__text {
    margin: 0;
    padding: 0;
    border: 0;
    font-style: normal;
}

.otzyvy__text > :first-child {
    margin-top: 0;
}

.otzyvy__author {
    margin-top: 12px;
    color: var(--muted, #5e6670);
    font-size: 0.9em;
}
src/Reviews.php
<?php

namespace Examples\Otzyvy;

use Cms\Admin\MetaBoxes;
use Cms\Content\PostType;
use Cms\Models\Post;
use Cms\Plugins\Plugin;
use Illuminate\Http\Request;

/**
 * Плагин «Отзывы»: тип записей «review», поля «Оценка», «Имя клиента», «Город» в редакторе
 * и блок «Отзывы», который выводит последние отзывы на любой странице.
 */
class Reviews
{
    /** Метаполя отзыва. Подчёркивание в начале — служебное поле: его не видно в блоке «Произвольные поля». */
    public const META = ['rating' => '_otzyvy_rating', 'author' => '_otzyvy_author', 'city' => '_otzyvy_city'];

    public function __construct(protected Plugin $plugin) {}

    /**
     * Подписаться на события CMS. Обработчики — методы этого класса.
     */
    public function register(): void
    {
        register_activation_hook($this->plugin, [$this, 'activate']);

        add_action('init', [$this, 'registerTypes']);                 // тип записей и блок — в «init»
        add_action('add_meta_boxes', [$this, 'metaBoxes'], 10, 3);    // поля в редакторе отзыва
        add_action('save_post_review', [$this, 'save'], 10, 2);       // сохранить их вместе с отзывом
        add_action('site_head', [$this, 'styles']);                   // стили блока на сайте
    }

    /**
     * При включении плагина: запомнить версию (пригодится, если в следующих версиях понадобится что-то перенести).
     */
    public function activate(): void
    {
        update_option('otzyvy_version', $this->plugin->version);
    }

    public function registerTypes(): void
    {
        register_post_type('review', [
            'labels' => [
                'name' => 'Отзывы',
                'singular_name' => 'Отзыв',
                'all_items' => 'Все отзывы',
                'add_new' => 'Добавить отзыв',
                'add_new_item' => 'Новый отзыв',
                'edit_item' => 'Редактировать отзыв',
                'count' => 'отзыв|отзыва|отзывов',
            ],
            'public' => false,            // отдельных страниц у отзывов нет — их показывает блок
            'supports' => ['title', 'editor'],
            'menu_icon' => 'star',
            'menu_position' => 27,
            'admin_slug' => 'reviews',    // адрес в админке: /admin/reviews
        ]);

        register_block_type('otzyvy-list', [
            'title' => 'Отзывы',
            'icon' => 'star',
            'category' => 'design',
            'description' => 'Последние отзывы клиентов с оценкой.',
            'keywords' => ['отзывы', 'рейтинг', 'клиенты'],
            'fields' => [
                'heading' => ['type' => 'text', 'label' => 'Заголовок', 'default' => 'Отзывы наших клиентов'],
                'number' => ['type' => 'number', 'label' => 'Сколько показать', 'min' => 1, 'max' => 12, 'default' => 3],
                'show_rating' => ['type' => 'checkbox', 'label' => 'Показывать оценку', 'default' => true],
            ],
            'view' => $this->plugin->slug.'::blocks.list',
        ]);
    }

    /**
     * Событие «add_meta_boxes» срабатывает для каждой записи — блок добавляем только отзывам.
     */
    public function metaBoxes(PostType $postType, Post $post, MetaBoxes $boxes): void
    {
        if ($postType->name === 'review') {
            add_meta_box('otzyvy-details', 'Оценка и автор', $this->plugin->slug.'::meta-box', 'review', 'side', 5);
        }
    }

    /**
     * Сохранить поля из блока «Оценка и автор». Права на редактирование отзыва CMS уже проверила.
     */
    public function save(Post $post, Request $request): void
    {
        if (! $request->has('otzyvy')) {
            return; // блок полей убрали — не трогаем
        }

        $data = $request->validate([
            'otzyvy.rating' => ['required', 'integer', 'between:1,5'],
            'otzyvy.author' => ['nullable', 'string', 'max:100'],
            'otzyvy.city' => ['nullable', 'string', 'max:100'],
        ])['otzyvy'];

        update_post_meta($post, self::META['rating'], (int) $data['rating']);
        update_post_meta($post, self::META['author'], trim((string) ($data['author'] ?? '')));
        update_post_meta($post, self::META['city'], trim((string) ($data['city'] ?? '')));
    }

    public function styles(): void
    {
        echo '<link rel="stylesheet" href="'.e(plugin_asset($this->plugin->slug, 'otzyvy.css')).'">'.PHP_EOL;
    }

    /**
     * Звёзды оценки для шаблона: «★★★★☆».
     */
    public static function stars(int $rating): string
    {
        $rating = max(1, min(5, $rating));

        return str_repeat('★', $rating).str_repeat('☆', 5 - $rating);
    }
}
uninstall.php
<?php

/*
 * Выполняется, когда плагин удаляют в разделе «Плагины» (при отключении — нет).
 * Убираем всё, что плагин создал: отзывы (вместе с их полями) и настройку.
 */

use Cms\Models\Post;

Post::withTrashed()->where('type', 'review')->get()->each(fn (Post $review) => $review->forceDelete());

delete_option('otzyvy_version');
views/blocks/list.blade.php
{{-- Блок «Отзывы». Переменные: $block (поля блока), $post (запись, в которой стоит блок). --}}
@php
    $reviews = get_posts(['type' => 'review', 'number' => (int) $block['number']]);
    $meta = \Examples\Otzyvy\Reviews::META;
@endphp

@if ($reviews->isNotEmpty())
    <section class="block otzyvy">
        @if ($block['heading'] !== '')
            <h2 class="otzyvy__title">{{ $block['heading'] }}</h2>
        @endif

        <div class="otzyvy__list">
            @foreach ($reviews as $review)
                <figure class="otzyvy__item">
                    @if ($block['show_rating'])
                        <p class="otzyvy__stars" aria-label="Оценка {{ (int) $review->getMeta($meta['rating'], 5) }} из 5">{{ \Examples\Otzyvy\Reviews::stars((int) $review->getMeta($meta['rating'], 5)) }}</p>
                    @endif
                    <blockquote class="otzyvy__text">{!! $review->contentHtml() !!}</blockquote>
                    <figcaption class="otzyvy__author">
                        {{ $review->getMeta($meta['author']) ?: $review->displayTitle() }}@if ($review->getMeta($meta['city'])), {{ $review->getMeta($meta['city']) }}@endif
                    </figcaption>
                </figure>
            @endforeach
        </div>
    </section>
@endif
views/meta-box.blade.php
{{-- Блок «Оценка и автор» в редакторе отзыва. Переменные: $post, $postType. --}}
@php
    $meta = \Examples\Otzyvy\Reviews::META;
    $rating = (int) old('otzyvy.rating', $post->getMeta($meta['rating'], 5));
@endphp

<p>
    <label for="otzyvy-rating">Оценка</label><br>
    <select id="otzyvy-rating" name="otzyvy[rating]">
        @foreach ([5, 4, 3, 2, 1] as $value)
            <option value="{{ $value }}" @selected($rating === $value)>{{ \Examples\Otzyvy\Reviews::stars($value) }} — {{ $value }}</option>
        @endforeach
    </select>
</p>
<p>
    <label for="otzyvy-author">Имя клиента</label>
    <input type="text" id="otzyvy-author" name="otzyvy[author]" class="widefat" maxlength="100"
           value="{{ old('otzyvy.author', $post->getMeta($meta['author'])) }}">
</p>
<p>
    <label for="otzyvy-city">Город</label>
    <input type="text" id="otzyvy-city" name="otzyvy[city]" class="widefat" maxlength="100"
           value="{{ old('otzyvy.city', $post->getMeta($meta['city'])) }}">
</p>