П’ять патернів проєктування на прикладі Laravel: Основи

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Патерни проєктування — річ настільки абстрактна, що складно зрозуміти їх тільки за описом. Бувало, я перечитував опис патерну 6 разів на сайті Refactoring.Guru і не розумів, про що мова. Складно розуміти концепти, про які ти ніколи не чув, на прикладах, які не траплялися у твоїй роботі. Рішення для мене було очевидне: відкриваю laravel/framework і занотовую собі патерни проєктування, які використовував Taylor Otwell в розробці Laravel.

В цій статті я опишу 5 патернів проєктування, що зустрічаються в Laravel, та покажу, де саме вони використовуються.

💡 Помітив технічну помилку в статті? Я неправильно описав патерн чи надав неправильний приклад? Напевне, це дійсно помилка! Я вивчаю патерни й твій коментар про неточності в статті дуже допоможе мені та всім, хто навчається разом зі мною.

1. Singleton 🧍

Олександр Швець (автор Refactoring.Guru) описує Singleton як «породжувальний патерн проєктування, який гарантує, що клас має лише один екземпляр, та надає глобальну точку доступу до нього» (теорія).

Приклади

💡 Попереджаю. Більшість цієї статті я буду цитувати код із Laravel. Щоб його зрозуміти й не губитися, потрібно мати якісь знання про Laravel та Service Container. Без знання того, як працює Dependency Injection в Laravel, буде складно. Також тут буде мало теоретичного пояснення шаблонів.

Kernel

Файл

// Усе "ядро" додатку — Singleton
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

Cache

Файл

public function register()
{
    // Стандартний інструмент роботи з кешом — Singleton
    $this->app->singleton('cache', function ($app) {
        return new CacheManager($app);
    });

    /* ... КОД ... */
}

Session

Файл

protected function registerSessionManager()
{
    // стандартний інструмент роботи з сесією — Singleton
    $this->app->singleton('session', function ($app) {
        return new SessionManager($app);
    });
}

І багато інших...

Перераховувати всі приклади не має сенсу, тому що їх багато. Щоб переконатися в цьому, досить пошукати слово Singleton у GitHub репозиторії фреймворку. Кожен фасад в Laravel — це Singleton, до прикладу.

Імплементація

Як вищевказане реалізоване? Якщо говорити в загальному про PHP, то зазвичай рекомендують: зробити методи __construct та __clone приватними та додати статичний метод, який при створенні буде контролювати, щоб екземпляр цього класу вертався один і той самий. Проте конкретно Laravel створює Singleton за допомогою Service Container, тому імплементація відрізняється.

Прив’язка в контейнері

Якщо розробник хоче створити Singleton в Laravel, він використовує функцію app()->singleton(). Допустимо, він має клас CustomCache, що керує кешом в якийсь особливий спосіб. Екземпляр цього класу завжди має бути один і той самий, у якому б місці розробник його не викликав, тому що від цього залежить цільність даних, що він зберігає. Для цієї задачі в Service Provider потрібно прив’язати згаданий клас до інтерфейсу: app()->singleton(CustomCacheInterface::class, CustomCache::class) (також можна прив’язати до будь-якого string). Ланцюжок функцій, що викличеться, починається в src/Illuminate/Container/Container.php#L379:

public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}

Розробник прив’язує конкретний клас (CustomCache::class) до абстрактного (CustomCacheInterface::class). Функція позначає такий зв’язок як shared (це те, за що відповідає true в параметрах). Метод bind у коді зверху просто зберігає зв’язок між двома класами в асоціативному масиві (src/Illuminate/Container/Container.php#L239):

public function bind($abstract, $concrete = null, $shared = false)
{
    /* ... КОД ... */

    $this->bindings[$abstract] = compact('concrete', 'shared');

    /* ... КОД ... */
}

Пошук в контейнері

А коли розробник використає клас за допомогою app()->make(CustomCacheInterface::class) чи інших способів, викличеться функція Illuminate\Container\Container::resolve(). У цій функції буде відбуватися пошук конкретного класу (CustomCache::class), його «побудова», а також контроль над тим, щоб розробник щоразу отримував один і той самий екземпляр:

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    /* ... КОД ... */

    // вертає вже сформований екземпляр, якщо такий існує в пам'яті
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    /* ... КОД ... */

    // зберігає екземпляр у пам'ять, якщо це Singleton
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    /* ... КОД ... */
}

Коротко

Singleton у Laravel імплементовано за допомогою Service Container. Для використання патерну потрібно викликати функцію app->singleton() й потім отримувати необхідні екземпляри класу за допомогою app()->make() чи інших способів.

2. Facade 🏛️

У Laravel є цілий список фасадів, що розробники використовують у кожному проєкті. Це — шаблон проєктування. Описують його як «структурний патерн проєктування, який надає простий інтерфейс до складної системи класів, бібліотеки або фреймворку» (теорія).

Приклади

Cache

Фасад
Складна система класів

/* 
	Приклад того, як ти можеш використати фасад.
	Під капотом Laravel вибирає "драйвер" для роботи з кешом 
	та використовує, наприклад, клас `Illuminate/Cache/DatabaseStore`.
*/ 
Illuminate\Support\Facades\Cache::put('admin', Role::admin());

Queue

Фасад
Складна система класів

/* 
	Приклад того, як ти можеш використати фасад.
	Під капотом Laravel вибирає "драйвер" для роботи з чергами 
	та використовує, наприклад, клас `Illuminate/Queue/DatabaseQueue`.
*/ 
Illuminate\Support\Facades\Queue::push(new ExampleJob());

І багато інших...

Список фасадів можна знайти в документації.

Імплементація

Багато разів я намагався розібратися, як працює конкретний фасад. Наприклад, хотів знайти, як працює метод Log::info(). І щоразу я натикався на схожий патерн (src/Illuminate/Support/Facades/Log.php):

class Log extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'log';
    }
}

Лише один метод у цілому класі. Що це означає? Де метод info(), імплементацію якого я намагався знайти? Яким чином це працює? Декілька годин у відкритому коді Laravel — і я знайшов відповіді на ці питання.

💡 Імплементація фасаду в Laravel не є обов’язковим аспектом імплементації фасаду в іншому фреймворку чи іншому проєкті. Проте це важливо для того, щоб розуміти, як працює Laravel.

Прив’язка в контейнері

Рядок log — це ідентифікатор класу, який відповідає за вищезгаданий метод. Laravel використовує Service Container, щоб у файлі src/Illuminate/Log/LogServiceProvider.php прив’язати строку до потрібного класу:

class LogServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('log', function ($app) {
            return new LogManager($app);
        });
    }
}

Статичний метод

Так, розробник викликає статичний метод info(). Проте перший метод, який викликається насправді, це магічний метод __callStatic у файлі src/Illuminate/Support/Facades/Facade.php. Він відповідальний за те, щоб знайти екземпляр класу та делегувати йому виконання викликаного методу:

public static function __callStatic($method, $args)
{
    // знаходження екземляра класу
    $instance = static::getFacadeRoot();

    /* ... КОД ... */

    // делегація виконання функції щойно створеному екземпляру
    return $instance->$method(...$args);
}

Пошук в контейнері

В методі getFacadeRoot відбувається пошук за ідентифікатором. Функція за своєю суттю дуже схожа на app()->make() (src/Illuminate/Support/Facades/Facade.php#L186):

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

Делегація

Після того, як LogManager був знайдений, виконується його метод (src/Illuminate/Log/LogManager.php):

public function info($message, array $context = []): void
{
    $this->driver()->info($message, $context);
}

Коротко

Facade в Laravel побудовано за допомогою Service Container та магічного методу __callStatic().

3. Builder 🧱

Builder — це «породжувальний патерн проєктування, що дає змогу створювати складні об’єкти крок за кроком. Будівельник дає можливість використовувати один і той самий код будівництва для отримання різних відображень об’єктів» (теорія).

Приклади

Eloquent Builder

Навіть з назви нескладно здогадатися, що це — приклад вищезгаданого шаблону проєктування. Візьмемо звичайний запит, який може написати будь-який розробник:

User::where('active', true)
    ->with('payments')
    ->get();

У коді вище розробник вказує, який конкретно запит до бази даних він хоче.

💡 Чому я говорю про Eloquent Builder, коли звертаюся до моделі User? Тому, що Laravel, користуючись гнучкістю магічних методів __call та __callStatic, перенаправляє такі запити до моделі до Illuminate\Database\Eloquent\Builder. Подивитися на те, як це відбувається, можна у відкритому коді.

Кожен метод змінює запит, який розробник отримає на виході. Я наведу використані функції нижче, проте я не очікую, що ти маєш їх зрозуміти (я далеко не все зрозумів). Ці приклади важливі лише для того, щоб зрозуміти суть, чому саме виникає такий синтаксис, та як приблизно модифікується запит.

where()

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
    if ($column instanceof Closure && is_null($operator)) {
        $column($query = $this->model->newQueryWithoutRelationships());

        $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
    } else {
        $this->query->where(...func_get_args());
    }

    return $this;
}

with()

public function with($relations, $callback = null)
{
    if ($callback instanceof Closure) {
        $eagerLoad = $this->parseWithRelations([$relations => $callback]);
    } else {
        $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
    }

    $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);

    return $this;
}

get()

Останній метод у ланцюжку, який вертає моделі по збудованому запиту.

public function get($columns = ['*'])
{
    $builder = $this->applyScopes();

    if (count($models = $builder->getModels($columns)) > 0) {
        $models = $builder->eagerLoadRelations($models);
    }

    return $builder->getModel()->newCollection($models);
}

Будівельні функції (where та with) зберігають дані всередині класу та вертають самого себе (return $this), щоб утворити типовий патерн виклику методів fn()->fn()->fn(). У той час як метод get() опрацьовує всі збережені дані та вертає потрібний результат.

Query Builder

Eloquent Builder часто звертається до Query Builder. Суть Query Builder схожа, проте цей будівельник працює не з моделями, а напряму з базою даних, коли викликається через DB::select(), наприклад. Я не буду навантажувати деталями, його імплементацію можна знайти у відкритому коді.

4. Observer 👓

Observer — це «поведінковий патерн проєктування, який створює механізм підписки, що дає змогу одним об’єктам стежити й реагувати на події, які відбуваються в інших об’єктах» (теорія).

Приклади

Event Registered -> Listener SendEmailVerificationNotification

При встановленні свіжої версії Laravel, в EventServiceProvider є Event та Listener за замовчуванням:

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];

    /* ... КОД ... */
}

Цей код відповідає за те, щоб при події Registered відбулася команда SendEmailVerificationNotification. Простіше кажучи, даний код надає інструкції: коли користувач зареєструється, надішли йому сповіщення про верифікацію емейлу. Проте ніщо з цього не вказує на те, коли саме виникне подія «користувач зареєструвався».

Подія може відбуватися в будь-якому контролері, сервісі, чи моделі додатка, а команда все одно спрацює (src/Illuminate/Auth/Listeners/SendEmailVerificationNotification.php):

class SendEmailVerificationNotification
{
    public function handle(Registered $event)
    {
        if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
            $event->user->sendEmailVerificationNotification();
        }
    }
}

Коли зазвичай виникає подія Registered? Залежить. Якщо розробник хоче написати авторизацію сам, то це його справа сповістити про подію. Якщо розробник використовує Fortify, то це роблять за нього у файлі RegisteredUserController:

public function store(Request $request, CreatesNewUsers $creator): RegisterResponse
{
    event(new Registered($user = $creator->create($request->all())));

    $this->guard->login($user);

    return app(RegisterResponse::class);
}

Імплементація

У Laravel є цілий розділ, який описує цю систему: laravel.com/docs/9.x/events. Зараз я спробую заглибитися трішки більше, ніж розповідає документація. Спойлер: це можливо завдяки Service Container і асоціативному масиву.

Зберігання Event

Зазвичай зберігання всіх подій відбувається в App\Providers\EventServiceProvider. Провайдер викликає метод Illuminate\Events\Dispatcher::listen() до всіх подій у списку. Саме цей метод відповідальний за збереження Event та відповідного йому Listener у пам’яті:

public function listen($events, $listener = null)
{
    /* ... КОД ... */

    $this->listeners[$event][] = $listener;

    /* ... КОД ... */
}

Запуск Listener

Коли розробник сповіщає про подію через event(new CustomEvent()), виконується ланцюжок функцій, в кінці якого виконується метод Illuminate\Events\Dispatcher::dispatch(). Метод виконує всі слухачі цієї події, які є на цю мить у пам’яті:

public function dispatch($event, $payload = [], $halt = false)
{
    /* ... КОД ... */

    foreach ($this->getListeners($event) as $listener) {
        $response = $listener($event, $payload);
    }

    /* ... КОД ... */
}

💡 Що за ланцюжок функцій з event() до Illuminate\Events\Dispatcher::dispatch()? Викликається допоміжна функція event, яка за допомогою Service Container знаходить клас Illuminate\Events\Dispatcher. У цьому класі вже й виконується метод dispatch().

Коротко

Шаблон Observer в Laravel зроблено за допомогою вбудованих Events і Listeners. Усі події та їхні слухачі зберігаються в асоціативному масиві. Синтаксис виклику й збереження можливий завдяки Service Container.

5. Chain of Responsibility ⛓️

Chain of Responsibility — це «поведінковий патерн проєктування, що дає змогу передавати запити послідовно ланцюжком обробників. Кожен наступний обробник вирішує, чи може він обробити запит сам і чи варто передавати запит далі ланцюжком» (теорія).

Приклади

Middleware

Коли користувач надсилає запит до додатка, App\Http\Kernel його обробляє. У цьому файлі можна знайти список $middleware:

class Kernel extends HttpKernel
{
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /* ... CODE ... */
}

Один за одним вказані Middleware обробляють запит і передають наступному в черзі. Перевіряють, чи все гаразд з CORS, забирають пробіли в параметрів, перетворюють '' в null і т.п.

У цій поведінці і є суть патерну «Chain of Responsibilty». Запускати окремі між собою не пов’язані Middleware. При цьому типовий Middleware буде виглядати доволі просто, як, наприклад, App\Http\Middleware\RedirectIfAuthenticated:

/**
 * Приклад того, як виглядає стандартний обробник запиту.
 * Метод handle() відподвідає за обробку.
 * Якщо потрібно продовжити обробку, вертається $next($request).
 * Якщо потрібно зупинити обробку, вертається інша відповідь.
 */
class RedirectIfAuthenticated
{
    public function handle(Request $request, Closure $next, ...$guards)
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
}

Імплементація

Як саме відбувається проходження запиту ланцюжком? У сухому залишку відповідальний за це метод Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter():

protected function sendRequestThroughRouter($request)
{
    /* ... КОД ... */

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

Головний клас, що відповідальний за це: Illuminate\Pipeline\Pipeline. Так, це той самий клас, який за відчуттям займає 90% логів. Це Builder (патерн, про який я розповідав вище), суть якого в створенні «конвеєра», по якому йде якийсь об’єкт. Метод send() визначає, який об’єкт має змінюватися. through() зберігає масив класів, що будуть змінювати об’єкт.

І, нарешті, функція then() означає, що рух по «конвеєру» потрібно почати. Також там зазначають, що робити після того, як всі модифікації були задіяні. Якщо піти навіть глибше, то ось так виглядає функція, що відповідає за рух конвеєра:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

Так, розумію, функція зверху не дуже очевидна. Якщо дуже просто, метод об’єднує всі функції в одну. «Збірна» функція потім обробляє $this->passable (у нашому випадку $request).

Коротко

Laravel використовує клас Illuminate\Pipeline\Pipeline, щоб імплементувати патерн Chain of Responsibility.

Наостанок

💡 Я ще не закінчив вивчати патерни проєктування. Олександр Швець описав аж 22 патерни і далеко не всі з них були зрозумілі для мене. Тому я маю намір знайти приклади до всіх. Я навіть створив англомовний репозиторій, у якому буду це робити. Якщо тобі цікаво, можеш приєднатися до мене.

Дякую, що прочитав. Сподіваюсь, стаття була корисна. Якщо є уточнення чи запитання, буду радий обговорити.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

дебильный фреймворк с упоротыми авторами, которые закрывают любые issues. Несколько лет назад, когда еще на с пыхой работал, использовали этот фреймворк на проекте, и нашли в ядре несколько факапов в реализации ConsoleKernel и CacheSchedulingMutex, посоздавали ишью на гитхабе, их авторы тупо все позакрывали не разобравшись. Хороший комбайн для базовой конструкции сайта, но может подложить очень жирную свинью или проблему, то что некоторые классы нельзя переопределить вообще

Навіщо ви написали цю статтю? Якщо розказати про дизайн патерни, то навіщо тут фреймворк? У фреймворках більшість цих речей робиться «під капотом». З академічної точки зору краще розбирати патерни на голому PHP. В інтернеті таких статей тисячі.

Смисл статті в тому, щоб:

1) Використати інтерес до інструменту, щоб на його прикладі показати практичні використання шаблонів проєктування. Коли в Laravel використовується Singleton? В яких умовах використовується Chain of Responsibility? Є питання, на які я особисто хочу знайти відповіді: коли практично слід використовувати Abstract Factory? Чи використовується в Laravel Factory Method? На прикладах із фреймворку мені було би легше зрозуміти різницю між патернами, тому що я прямо працюю з ними.

2) Поглибити знання розробників, що використовують Laravel. Протягом роботи з фреймворком у розробників виникають питання, на які документація відповідає не настільки детально, а моя стаття частково дає на це відповіді. Чому у фасадах може бути тільки один метод, за що він відповідає? Як створити свій фасад? Як працюють допоміжні методи типу event чи dispatch? Чому в логах стільки класів Pipeline, за що вони відповідають? Як у самому фреймворку використовують Service Providers?

Якщо коротко, тому що для когось так може бути легше й цікавіше дізнаватися про шаблони проєктування. Наприклад, мені. Я таку статтю не знайшов, тому написав її.

Чи використовується в Laravel Factory Method?

Ви маєте наувазі чи використовують його автори фреймворку? А яка різниця, що вам заважає його застосовувати там де це вам потрібно?

Приклад дозволяє краще зрозуміти принцип роботи шаблону. Приклад в Laravel дозволяє зробити додаткові асоціативні зв’язки. Можливо, я вже використовував цей шаблон, проте не знаю про це. Можливо, цей шаблон використовується у тому фасаді, який я використовую щодня. Це допомагає відчувати себе впевненішим у використанні патерна.

Мені нічого не заважає застосовувати його самому. Приклад в фреймворку важливий мені для кращого засвоєння інформації, це все.

У мене поки є складнощі з розумінням того, де і як якісь шаблони використовувати. Якби була стаття «Як автори Laravel використовують патерн Factory Method», мені було би легше зрозуміти матеріал. Такої статті я не знайшов, тому вирішив написати сам)

refactoring.guru/uk/design-patterns

Ось тут все що треба знати про патерни. Приклади на різних мовах програмування. Написано у чому їх суть, як реалізовуються і де їх використовувати. А головне — які проблеми вони вирішують.

тому написав її

Добре зробив.
Дякую!

Я можу помилятись, але фасади у ларавел це не ті самі фасади, що описують у книжках по паттернах проектування.

У ларавел фасад обирає який саме сервіс буде вікликаний, а фасад «з паттернів» просто дає простіший інтерфейс для складних операций.

Якщо в ларавел ми визиваємо умовний MailerFacade::mail($mailText); то під капотом ларавел обирає який саме мейлер буде займатись відправкою пошти.
Звичайний же фасад — це просто сервіс який в собі інкапсулює складну логику відправки пошти та на публіку віддає лише метод mail().

Тобто, як на мене фасади в ларавелі то щось ближще до паттерну стратегія, бо від капотом можуть бути визвані різні сервіси, але через один фасад.

Ось стаття в підтримку — medium.com/...​венный-фасад-cc5e19554120

Перш за все, дійсно суджу по назві. Як би там не було, Taylor Otwell ймовірно так назвав цю секцію, тому що його ціль була в спрощені доступу до фреймворку. Якщо ні, то не зрозуміло, чому саме така назва. Він часто використовує дизайн патерни у фреймворку і він навряд міг не знати про те, який смисл вкладає в цю частину Laravel.

Refactoring.Guru пише:
«Фасад — це простий інтерфейс для роботи зі складною підсистемою, яка містить безліч класів. Фасад може бути спрощеним відображенням системи, що не має 100% тієї функціональності, якої можна було б досягти, використовуючи складну підсистему безпосередньо. Разом з тим, він надає саме ті „фічі“, які потрібні клієнтові, і приховує все інше.»

По факту, дуже рідко користувачі фреймворку взаємодіють з кешем чи базою даних без фасаду. Тобто фасад виконує саме цю функцію: один клас використовується для того, щоб мати доступ до цілої папки з класами.

У ларавел фасад обирає який саме сервіс буде вікликаний, а фасад «з паттернів» просто дає простіший інтерфейс для складних операций.

Я теж про це задумався. Відповідь для себе в мене була така:

Мені здається, в Laravel фасад не відповідає за те, який саме сервіс буде викликаний. У більшості, фасад дає доступ через Service Container до якогось класу. На цьому його функції завершуються. Проте цей клас, до якого надається доступ, уже відповідає за подальшу логіку.

Дуже часто фасад надає доступ до менеджера. Наприклад, фасад Cache приводить до CacheManager, який уже використовує патерн Стратегія.

Тобто момент виклику функції Facades\Cache::forget() — це патерн Фасад. Ми спрощено отримуємо доступ до багатьох класів усередині фреймворку. Проте те, що відбувається в CacheManager, — це вже патерн Стратегія. Вибираються різні сервіси, тощо.

Але мы ж можемо самотушки обирати який саме сервіс повернеться з фасаду. getFacadeAccessor може мати внутрішню логіку яка саме і обирає що само видати після виклику фасада.

Як на мене фасади в ларавеле це обгортка навколо імплементації. Типу завжди викликати Cache та не перейматись який саме кєш буде використовуватись. А фасад спрямовує виклик на необхідний сервіс. Тож це схоже, але не зовсім те, що мається наувазі в паттерні фасад.

Підписатись на коментарі