You are on page 1of 127

МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ

СУМСЬКИЙ ДЕРЖАВНИЙ УНІВЕРСИТЕТ


КАФЕДРА ІНФОРМАЦІЙНИХ ТЕХНОЛОГІЙ

Курсова робота
з дисципліни «Проектування веб-орієнтованих інформаційних систем»
на тему «Розроблення web-орієнтованої інформаційної системи у вигляді блогу на
базі архітектурного шаблону MVC з використанням фреймворку YII2»

Викладач Парфененко Ю.В.

Студенти Касьяненко Д.Р.


Сніжко А. Я.

Група ІТ.м-32
Суми 2023
ЗМІСТ

ВСТУП..............................................................................................................................3
1 ПОСТАНОВКА ЗАДАЧІ........................................................................................4
2 ПРАКТИЧНА РЕАЛІЗАЦІЯ ЗАДАЧІ...................................................................6
2.1 Архітектура додатку................................................................................................6
2.2 Реалізація бази даних..............................................................................................8
2.3 Програмні модулі інформаційної системи..........................................................12
2.4 Відображення результатів роботи у браузері.....................................................16
2.5 Відображення ходу роботи над розробкою додатку у GitHub..........................23
3 ТЕСТУВАННЯ...........................................................................................................25
ВИСНОВКИ...................................................................................................................42
СПИСОК ВИКОРИСТАНОЇ ЛІТЕРАТУРИ...............................................................43
ДОДАТОК А..................................................................................................................44
ДОДАТОК Б.................................................................................................................115

3
ВСТУП

Блог – це веб-сайт або електронний журнал, в якому автор, відомий як


блогер, публікує записи чи статті про різноманітні теми. Вміст блогу може бути
особистим, професійним, чи поєднувати обидва аспекти. Основна риса блогу –
публікація нового контенту в хронологічному порядку.
Блогери висловлюють свої думки, досліджують теми, діляться досвідом чи
надають інформацію своїм читачам. Ця форма ведення журналу може бути
засобом вираження творчості, або навіть способом заробітку, оскільки деякі
блогери залучають аудиторію та співпрацюють з рекламодавцями.
Блоги можуть включати різноманітні медіаформати, такі як текст,
зображення, відео та інші. Читачі зазвичай мають можливість залишати
коментарі, що дозволяє підтримувати взаємодію та обмін ідеями між блогером й
аудиторією. Однак існує проблема достовірності інформації, оскільки кожен може
створити блог, і якість контенту може варіюватися. Це може створювати виклики
для читачів. Не зважаючи на це, блог є популярним засобом. Його основними
функціями є:
 самовираження (шляхом публікації власних ідей та думок);
 спільнота та взаємодія («збір» однодумців навколо спільних інтересів
та ідеї);
 співпраця (блогери можуть вступати у співпрацю з брендами, іншими
блогерами та організаціям);
 підтримка трендів та нововведень (автори блогів можуть швидко
реагувати на новини, тренди та події у своїй області. Таким чином люди швидше
дізнаються про нову інформацію);
 інші.
Мета даної курсової роботи полягає в закріпленні теоретичних концепцій і
отриманні практичних навичок у розробці веб-орієнтованих інформаційних
систем у формі блогу. Робота базується на архітектурному шаблоні MVC з
використанням фреймворку YII2.

4
1 ПОСТАНОВКА ЗАДАЧІ

Необхідно розробити блог з використанням фреймворку Yii2, присвячений


порадам по здоровому харчуванню. Реалізація блогу повинна бути виконана за
архітектурним шаблоном Модель-Представлення-Контролер (MVC). Фреймворк
Yii2 використовує цей шаблон, який широко застосовується у сфері веб-
програмування.
В рамках блогу мають бути наступні функціональні можливості:
 система авторизації для автора статей та адміністратора.
 авторизація для автора.
 розподіл контенту за категоріями з відповідною тематикою.
 можливість перегляду інформації за категорією загалом та окремо
кожної статті.
 статті повинні містити назву, дату створення, опис, автора та мітки.
 функціонал для коментування записів читачами блогу.
 можливість відповідати на коментарі.
 пошук статей за їхніми мітками.
На рисунках 1.1 – 1.3 представлено функції користувачів.

Рисунок 1.1 – Функції читача

5
Рисунок 1.2 – Функції автора

Рисунок 1.3 – Функції адміністратора

Блог розроблятиметься групою студентів з двох осіб. Перелік основних


етапів робіт та виконавці наведено у таблиці 1.1:
Таблиця 1.1 Кроки створення проекту
№ Назва етапу Виконавець
1 Налаштування середовища Касьяненко Д.Р., Сніжко А. Я.
розробки локально
2 Налаштування командної Касьяненко Д.Р., Сніжко А. Я.
роботи
3 Створення бази даних Сніжко А. Я.
4. Наповнення бази даних Сніжко А. Я.
тестовими даними
5. Встановлення та Касьяненко Д.Р., Сніжко А. Я.
налаштування фреймворку
6. Написання контролерів Касьяненко Д.Р.
7. Написання моделей Касьяненко Д.Р.
6
8. Написання представлень Касьяненко Д.Р.
9 Написання стилів для Касьяненко Д.Р.
представлень
10 Проведення тестування Касьяненко Д.Р., Сніжко А. Я.
11 Усунення помилок Сніжко А. Я.
12 Написання пояснювальної Касьяненко Д.Р., Сніжко А. Я.
записки

7
2 ПРАКТИЧНА РЕАЛІЗАЦІЯ ЗАДАЧІ

2.1 Архітектура додатку

2.1.1 Архітектура ІС

Фреймворк Yii2 використовує архітектурний шаблон Модель-


Представлення-Контролер (MVC) [1], що дозволяє розділити структуру веб-
системи на три основні компоненти, кожен з яких виконує свою роль.
Модель (Model) в MVC виступає як ядро додатка, відповідаючи за
управління даними та логікою їх обробки. Модель дозволяє взаємодіяти з базою
даних, виконувати CRUD операції (створення, читання, оновлення, видалення) та
забезпечує проведення валідації даних.
Представлення (View) визначає спосіб, яким дані з моделі будуть
відображені користувачам. Це включає в себе вигляд та структуру веб-сторінок, а
також способи відображення інформації.
Контролер (Controller) виступає як посередник між моделлю та
представленням, і відповідає за обробку запитів користувача. Контролер
взаємодіє з моделлю для отримання та збереження даних, а також визначає, яке
представлення буде відображатися.

Рисунок 2.1 – Патерн MVC

Такий підхід дозволяє відокремити логіку додатку на рівні обробки даних,


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

8
Структуру фреймворку показано на рисунку 2.2. Основний корінь додатку
міститься в базовому (кореневому) каталозі. Внутрішні папки включають config
для конфігураційних файлів, controllers для контролерів, models для моделей,
views для представлень та web для веб-ресурсів [2]. Основні компоненти Yii2 – це
модель, що відповідає за управління даними та бізнес-логікою, представлення, яке
визначає структуру та вигляд веб-сторінок, та контролер, який обробляє HTTP-
запити та взаємодіє з моделлю та представленням.

Рисунок 2.2 – Структура фреймворку Yii2

Особливості фреймворку включають ліниве завантаження класів,


компонентну архітектуру, механізм вставки для управління залежностями, зручну
маршрутизацію та використання Active Record для роботи з базою даних та
моделями.
В Yii2 обробка запиту користувача відбувається наступним чином: коли
користувач виконує запит, механізм маршрутизації визначає, який контролер та
9
його дію слід викликати для обробки даного запиту. Обраний контролер потім
викликає відповідну дію, яка включає в себе весь необхідний код для обробки
запиту. За потреби контролер взаємодіє з моделями для отримання чи збереження
даних, наприклад, виконуючи операції з базою даних. Після цього контролер
формує дані, які повинні бути відображені, та викликає відповідне представлення.
Останнє відповідає за генерацію HTML-коду та виведення користувачу
сформованої відповіді.

2.2 Реалізація бази даних

Було створено базу даних «yii_blog» із наступними таблицями:


 article (стаття для блогу);
 topic (тема або категорія статті);
 comment (коментар);
 user (користувач блогу).
Таблиці до бази даних були додані за допомогою міграцій. На рисунку 2.3
представлено їх перелік.

Рисунок 2.3 – Перелік створених міграцій

Міграції дозволяють вносити та відміняти будь-які зміни в базі даних. У


даному випадку ці класи використовуються для створення таблиць із заданими
полями, встановлення зв’язків з іншими сутностями, додавання обмежень та
індексів. Клас міграції містить два основних методи: «saveUp» для внесення змін
до бази даних та «saveDown» – для відкоту цих змін. Нижче наведено фрагмент
програмного коду міграції для створення таблиці зі статтями:
public function safeUp()
{
$this->createTable('{{%user}}', [
'id' => $this->primaryKey(),
'name' => $this->string()->notNull(),
'login' => $this->string()->notNull(),
'password' => $this->string()->notNull(),
'image' => $this->string(),
'is_admin' => $this->boolean()->notNull(),
]);
}

10
Після запуску міграцій було створено базу даних, фізична модель якої
представлена на рисунку 2.4.

Рисунок 2.4 – Фізична модель бази даних «yii_blog»

На рисунках 2.5 – 2.12 представлено структуру та вміст кожної з таблиць.


Сутність article (стаття) зберігає в атрибутах свою назву (title), текст (description),
дату створення (date), назва та розширення файлу прикріпленого зображення
(image), теги для пошуку за ними (tag), кількість переглядів (viewed), посилання
на таблиці категорій (topic_id) та користувачів (user_id).

Рисунок 2.5 – Структура таблиці «article»

11
Рисунок 2.6 – Вміст таблиці «article»

Сутність topic (категорія) містить поле назви (name).

Рисунок 2.7 – Структура таблиці «topic»

Рисунок 2.8 – Вміст таблиці «topic»

Сутність comment (коментар) містить атрибути: текст коментаря (text),


посилання на таблиці користувачів (user_id) та статей (article_id), дату створення
(date) і відмітку, яка вказує на те, чи видалено даний коментар (delete). Блог
дозволяє надавати відповіді на коментарі (тобто, коментар до коментаря). Для
цього в даній таблиці створено поле comment_id, яке може містити посилання на
інший коментар.

12
Рисунок 2.9 – Структура таблиці «comment»

Рисунок 2.10 – Вміст таблиці «coment»

Сутність користувач (user) містить поля: ім’я користувача (name), логін


(login), який є електронною поштою, пароль (password), що зберігається у вигляді
геш-значення, назва та розширення зображення профілю користувача (image),
відмітка для позначення, чи є даний користувач адміністратором блогу (is_admin).

Рисунок 2.11 – Структура таблиці «user»

Рисунок 2.12 – Вміст таблиці «user»

13
2.3 Програмні модулі інформаційної системи

У проекті було створено моделі, перелік яких показано на рисунку 2.13.


В Yii2, якщо моделі працюють з певною таблицею в базі даних, то мають
наслідуватись від класу ActiveRecord [3]. Такими є, наприклад, класи Article та
User. Якщо модель не має зв’язку з базою даних, і лише зберігає і оброблює
інформацію в системі, вона має наслідуватись від класу Model. Прикладами є
CommentForm та SearchForm.

Рисунок 2.13 – Створені моделі

Розглянемо деякі з розроблених моделей, адже всі вони реалізовані за


однаковим принципом. Всі моделі мають метод rules, який задає правила
валідації. Для класу User:
public function rules()
{
return [
[['name', 'login', 'password'], 'required'],
['login', 'email'],
[['name', 'login', 'password'], 'string'],
[['name', 'login', 'password', 'image'], 'string', 'max'
=> 255],
];
}

Поля name, login і password є обов’язковими і не можуть бути пустими


(рядок масиву), логін є електронною поштою (2 рядок). Останні два рядки

14
говорять вказують вимоги до типу даних і максимального розміру для зазначених
полів.
Також модель користувача містить методи для отримання доступу до
атрибутів, пошуку запису в базі даних та інших перевірок. Наприклад, наступний
метод використовується при авторизації для перевірки правильності введеного
паролю:
public function validatePassword($password)
{
return Yii::$app
->getSecurity()
->validatePassword($password, $this->password) ? true :
false;
}
Клас UserSearch виконує пошук користувачів. Наслідується від описаної
вище моделі User і працює з таблицею user в базі даних.
Робота з формати також реалізована за допомогою моделей. Наприклад, за
авторизацію відповідає LoginForm. Цей клас містить в атрибутах посилання на
модель user і працює з нею.
В інформаційній системи було створено додаткові модулі для
адміністратора і користувача за допомогою графічного інтерфейсу «yii code
generator». Модуль представляє собою підсистему, яка сама по собі містить
елементи MVC, такі як моделі, представлення, контролери тощо [4].
Модулі адміністрування для звичайного користувача та адміністратора
мають схожий функціонал. Перший – більш обмежений за функціоналом. З
допомогою цього модулю користувач має можливість створювати, редагувати та
видаляти власні статті, а також змінити аби видалити свій обліковий запис.
Перелік компонентів модулю показано на рисунку 2.14.

15
Рисунок 2.14 – Компоненти модулю користувача
Модуль для адміністратора, окрім описаних можливостей, дозволяє
створювати нових користувачів, видаляти будь-які коментарі, а також
створювати, видаляти й редагувати статті й категорії. Перелік компонентів даного
модулю показано на рисунку 2.15.

16
Рисунок 2.15 – Компоненти модулю адміністратора

Правила доступу до модулю описуються в класі Module. У даному


випадку, до описаного вище модулю повинен мати доступ тільки адміністратор
блогу. Для цього використовується правило:
'rules' => [
[
'allow' => true,
'matchCallback' => function ($rule, $action) {
return !Yii::$app->user->isGuest &&
!Yii::$app->user->identity->isAdmin();
}
]
]
Фрагмент коду вказує, що користувач має доступ лише тоді, коли він
авторизований як адміністратор.
Контролери взаємодіють з представленнями та описаними вище моделями
для реалізації CRUD (Create, Read, Update, Delete) операцій. Наприклад, метод
actionIndex класу UserController в модулі адміністратора призначений для
відображення переліку всіх користувачів. Контролер звертається до моделі
пошуку (UserSearch), отримує всі необхідні дані і передає їх відповідному
представленню. Реалізація даного методу наступна:
public function actionIndex()
17
{
$searchModel = new UserSearch();
$dataProvider = $searchModel->search($this->request-
>queryParams);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
Представлення index.php відображає отримані від контролера дані
користувачеві у вигляді таблиці. Фрагмент програмного коду наведено нижче.
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],

'id',
'text',
'user_id',
'comment_id',
'article_id',
//'date',
//'delete',
[
'class' => ActionColumn::class,
'template' => '{view} {delete}',
'urlCreator' => function ($action, Comment $model,
$key, $index, $column) {
return Url::toRoute([$action, 'id' => $model-
>id]);
}
],
],
]); ?>
Програмний код у повному обсязі міститься в додатку А.

18
2.4 Відображення результатів роботи у браузері

У блозі передбачено три категорії користувачів: гість, автор й


адміністратор. Перший має доступ до головної сторінки та web-сторінки
перегляду статті. На рисунках 2.16 – 2.17 показана головна сторінка блогу, на якій
знаходиться хедер із головним меню. Основний простій займають статті з
пагінацією. У правій частині розташоване поле для пошуку за тегом, три
найпопулярніші та три останні опубліковані статті. У нижній частині знаходиться
підвал веб-сторінки.

Рисунок 2.16 – Головна сторінка (частина 1)

19
Рисунок 2.17 – Головна сторінка (частина 2)
На рисунках 2.18 – 2.19 представлена сторінка перегляду статті з
коментарями до неї. Також зазначено автора, категорію, теги, і дату публікації.
Користувач має можливість поділитися постом у соціальних мережах, залишити
власний коментар та відповісти на коментарі інших.

Рисунок 2.18 – Перегляд статті (частина 1)

Рисунок 2.19 – Перегляд статті (частина 2)

Користувач може скористатися полем для пошуку статті за тегом,


розташованим у верхній правій частині сторінки.

20
Автор, окрім вже описаного функціоналу, має можливість зареєструватися
та / або авторизуватися. Для цього можна скористатися командою в головному
меню. Форма реєстрації показана на рисунку 2.20.

Рисунок 2.20 – Форма реєстрації

У разі успішної реєстрації користувач переспрямовується на сторінку


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

Рисунок 2.21 – Форма авторизації

21
Після проходження даного етапу користувач отримує доступ до панелі
управління. Перейти до неї можна за допомогою головного меню. Головна
сторінка даної панелі зображена на рисунку 2.22.

Рисунок 2.22 – Панель управління для ролі автора

За допомогою функціоналу керування обліковим записом автор може


переглянути та змінити дані свого профілю, встановити зображення або видалити
свій обліковий запис (рис. 2.23).

Рисунок 2.23 – Панель управління обліковим записом автора

Іншою частиною функціоналу модулю управління є керування власними


статтями. Авторизований користувач ще не має жодної статті, тому було
виконано вхід з облікового запису іншого користувача. На рисунку 2.24
представлено сторінку з переліком статей автора, а на рисунку 2.25 – форму
редагування статті.

22
Рисунок 2.24 – Статті автора

Рисунок 2.25 – Редагування статті

Виконано вхід з облікового запису адміністратора. Даний користувач має


найширші повноваження. На рисунку 2.26 показано основний функціонал адмін.
панелі.

23
Рисунок 2.26 – Функціонал панелі адміністратора

Даний користувач може переглядати, редагувати та видаляти будь-які


облікові записи користувачів блогу (рис. 2.27 – 2.28).

Рисунок 2.27 – Дані всіх користувачів блогу

Рисунок 2.28 – Форма редагування профілю автора на адмін. панелі

24
Це саме стосується статей і категорій. На рисунках 2.29 – 2.31
представлено функціонал створення і видалення категорії.

Рисунок 2.29 – Створення категорії

Рисунок 2.30 – Видалення категорії

Рисунок 2.31 – Результат видалення

Додатково адміністратор має можливість переглядати та видаляти


коментарі до статей.

25
2.5 Відображення ходу роботи над розробкою додатку у GitHub

Для роботи над проектом було використано системи контролю версій Git.
Відкритий репозиторій створено на GitHub, використано профіль kasianenkod
(рис. 2.32). Графік здійснення комітів продемонстровано на рисунку 2.33.

Рисунок 2.32 – Учасник проекту

Рисунок 2.33 – Здійснення комітів

26
На рисунку 2.34 зображена аналітика репозиторію, що демонструє роботу
над проектом.

Рисунок 2.34 – Аналітика репозиторію

27
3 ТЕСТУВАННЯ

Перш за все було перевірено працездатність посилань у головному меню,


тобто переходи між сторінками – усе працює коректно. Наступним кроком було
проведено тестування форм, адже за їх допомогою реалізовано основний
функціонал додатку. Спершу протестовано авторизацію те реєстрацію. Результати
представлені у таблиці 3.1.

Таблиця 3.1 – Тестування форм авторизації та реєстрації


№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
1 Відправка Повідом- Пройде-
форми лення, що ний
реєстрації із логін вже
логіном, який зайнятий
вже існує в
базі даних
2 Спроба Повідом- Пройде-
відправити лення про ний
форму помилки біля
реєстрації з відповідних
незаповненим полів
и полями

3 Відправка Створення Пройде-


форми користувача ний
реєстрації із в базі даних,
коректними переспря-
даними мування на
сторінку
авторизації

28
Продовження таблиці 3.1
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
4 Спроба Повідом- Пройде-
відправити не лення про ний
заповнену помилки біля
фурму відповідних
авторизації полів

5 Відправка Повідом- Пройде-


форми лення про ний
авторизації з помилку
некоректним
логіном
6 Відправка Повідом- Пройде-
форми лення про ний
авторизації з помилку
логіном, якого
не існує
7 Відправка Повідом- Пройде-
форми лення про ний
авторизації з помилку
некоректним
паролем
8 Відправка Переспря- Пройде-
форми мування на ний
авторизації із головну
коректними сторінку
даними блогу

Даний функціонал працює як і очікувалося. Наступним кроком виконано


тестування форми пошуку статей за тегами. Результати представлено в
таблиці 3.2.

29
Таблиця 3.2 – Тестування пошуку статей за тегами
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
1 Спроба Повідом- Пройде-
відправити не лення про те, ний
заповнену що текст для
форму пошуку не
введено
2 Відправка Престав- Пройде-
форми з лення статей, ний
існуючим як містять
тегом введений тег

3 Відправка Повідом- Пройде-


форми з не лення, що ний
існуючим нічого не
тегом знайдено

Результати відповідають очікуваним. Далі виконано тестування


функціоналу створення коментарів та відповідей на них (табл. 3.3). Коментар
створено під обліковим записом автора, а відповідь – як гість.

Таблиця 3.3 – Тестування функціоналу коментування статей


№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
1 Спроба Повідом- Пройде-
відправити не лення про те, ний
заповнену що форму не
форму заповнено

2 Відправка Успішно Пройде-


форми з доданий ний
текстом коментар
коментаря

30
Продовження таблиці 3.3
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
3 Здійснено Успішно Пройде-
вихід. Додано доданий ний
коментар як коментар
гість

4 Додано Успішно Пройде-


відповідь на додана ний
коментар відповідь
іншого
користувача

5 Здійснено Відмічено Пройде-


вихід з коментар як ний
облікового видалений
запису автора
першого
створеного
коментаря.
Виконано
видалення
власного
коментаря

31
Продовження таблиці 3.3
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
6 Створено Успішно Пройде-
новий видалений ний
коментар і коментар
видалено його

Протестовано модуль управління для автора статей. Результати


представлені у таблиці 3.4.

Таблиця 3.4 – Тестування модулю управління


№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
1 Перехід до Доступ Пройде-
модулю з дозволено, ний
облікового завантажена
запису автора головна
сторінка
2 Спроба Помилка 404 Пройде-
доступу до ний
модулю з
облікового
запису
адміністра-
тора
3 Спроба Помилка 404 Пройде-
доступу до ний
модулю як
гість

32
Продовження таблиці 3.4
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
4 Виконано вхід Помилка про Пройде-
під тестовим некорект- ний
обліковим ність даних
записом.
Відкрито
форму
оновлення
користувача.
Вказано
некоректний
логін
5 Спроба Повідомлен- Пройде-
надіслати не ня про ний
заповнену помилки біля
форму відповідних
полів

6 Відправка Оновлений Пройде-


форми з профіль ний
оновленими
даними

7 Зміна Повідом- Пройде-


зображення. лення про ний
Спроба помилку
відправити
пусту форму

33
Продовження таблиці 3.4
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
8 Зміна Повідом- Пройде-
зображення. лення про ний
Вибрано файл помилку
не
дозволеного
розширення
9 Відправка Додано Пройде-
форми зміни зображення ний
зображення.
Обрано
дозволений
формат файлу

10 Повторна Змінено Пройде-


відправка зображення ний
форми з
іншим
зображенням

11 Відправка Видалено Пройде-


форми для профіль. ний
видалення Переспря-
власного мування на
профілю головну
сторінку

34
Продовження таблиці 3.4
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
12 Виконано вхід Повідом- Пройде-
під обліковим лення про ний
записом помилки
автора.
Створення
статі. Спроба
відправити
пусту форму

13 Відправка Створена Пройде-


коректно стаття ний
заповненої
форми
створення
статті

14 Редагування Оновлено Пройде-


статті. статтю ний
Відправка
коректно
заповненої
форми

35
Продовження таблиці 3.4
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
15 Відправка Додано Пройде-
форми зображення ний
додавання
зображення

16 Відправка Видалено Пройде-


форми для статтю. ний
видалення Переспря-
статті мування на
сторінку з
переліком
статей

Останнім кроком є тестування панелі адміністратора. Для того, щоб


перевірити працездатність функціоналу керування користувачами, було знову
створено тестовий обліковий запис автора статей. Результати тестування
адміністративної панелі представлені в таблиці 3.5.

Таблиця 3.5 – Тестування модулю адміністрування


№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
1 Перехід до Доступ Пройде-
модулю з дозволено, ний
облікового завантажена
запису адміна головна
сторінка
2 Спроба Помилка 404 Пройде-
доступу до ний
модулю з
облікового
запису автора

36
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
3 Спроба Помилка 404 Пройде-
доступу до ний
модулю як
гість
4 Спроба Повідом- Пройде-
надіслати лення про ний
пусту форму помилки біля
для створення відповідних
користувача. полів форми

5 Спроба Повідом- Пройде-


надіслати лення про ний
форму для помилку
створення
користувача із
логіном, який
вже існує в
базі даних
6 Надсилання Створено Пройде-
форми користувача ний
створення
користувача з
коректними
даними

37
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
7 Спроба Повідом- Пройде-
надіслати не лення про ний
заповнену помилки біля
форму для відповідних
оновлення полів форми
користувача

8 Спроба Повідом- Пройде-


надіслати лення про ний
форму для помилку
оновлення
користувача із
логіном
іншого
користувача,
який вже
існує в базі
даних
9 Надсилання Оновлено Пройде-
форми користувача ний
оновлення
користувача з
коректними
даними

10 Додавання Повідом- Пройде-


зображення: лення про ний
спроба помилку
надіслати не
заповнену
форму
38
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
11 Спроба Повідом- Пройде-
надіслати лення про ний
форму помилку
додавання
зображення із
вибраним
файлом не
дозволеного
розширення
12 Надсилання Додано Пройде-
форми із зображення ний
зображенням користувача
коректного
розширення

13 Повторне Змінено Пройде-


надсилання зображення ний
форми з користувача
іншим
зображенням

39
40
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
14 Надсилання Видалено Пройде-
форми для користувача ний
видалення (und_test)
користувача

15 Надсилання Повідом- Пройде-


не заповненої лення про ний
форми для помилки біля
створення відповідних
статті полів форми

16 Надсилання Створено Пройде-


коректно статтю ний
заповненої
форми для
створення
статті

41
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
17 Відправка Додано Пройде-
форми зображення ний
додавання
зображення з
файлом
коректного
розширення

18 Надсилання Видалено Пройде-


форми статтю ний
видалення
статті

19 Редагування Змінені дані Пройде-


статті іншого про статтю ний
користувача.
Спершу
створено
статтю під
іншим
обліковим
записом.
Відправка
форми
редагування
статті на
адмін. панелі

42
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
20 Надсилання Видалена Пройде-
форми стаття ний
видалення
статті іншого
користувача

21 Спроба Повідом- Пройде-


надіслати не лення про ний
заповнену помилку
форму
створення
категорії
22 Надсилання Створена Пройде-
заповненої категорія ний
форми
створення
категорії

23 Спроба Повідом- Пройде-


надіслати не лення про ний
заповнену помилку
форму
оновлення
категорії
24 Надсилання Оновлена Пройде-
заповненої категорія ний
форми
оновлення
категорії

43
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
25 Надсилання Видалена Пройде-
форми категорія ний
видалення
категорії

26 Спроба Повідом- Пройде-


надіслати не лення про ний
заповнену помилку
форму
створення
коментаря

27 Надсилання Створений Пройде-


заповненої коментар ний
форми
створення
коментаря

44
Продовження таблиці 3.5
№ Тестування Очікуваний Отриманий результат Резуль-
кей результат тат
-су кейсу
28 Надсилання Видалено Пройде-
форми коментар ний
видалення
коментаря

29 Надсилання Видалено Пройде-


форми коментар з ний
видалення ідентифіка-
коментаря тором 12
іншого автора
(id = 12)

45
ВИСНОВКИ

Під час виконання даного проекту було розроблено блог, який є


платформою для розміщення статей з порадами по здоровому харчуванню з
використанням фреймворку Yii2 та архітектурного шаблону MVC. Приведено
опис створеної бази даних та основних розроблених програмних компонентів.
Було реалізовано визначений функціонал, який включає авторизований
доступ, категоризацію матеріалів, виведення інформації за категоріями та
окремими статтями, а також коментування записів та взаємодію з ними шляхом
відповідей на коментарі та пошуку за мітками до статей. Виконано тестування
розробленої інформаційної системи. Програмний код міститься в додатку А, дамп
створеної бази даних – у додатку Б.
В результаті виконання курсового проекту отримано значний досвід у
роботі з базами даних та мовою структурованих запитів SQL. Набуті знання
охоплюють об’єктну модель PHP 8, структуру веб-орієнтованих інформаційних
систем на базі архітектурного шаблону MVC, принципи організації PHP-
фреймворків (з підтримкою MVC) та принципи роботи зі системою контролю
версій Git.

46
СПИСОК ВИКОРИСТАНОЇ ЛІТЕРАТУРИ

1 Introduction: About Yii [Електронний ресурс] – Режим доступу до


ресурсу: https://www.yiiframework.com/doc/guide/2.0/en/intro-yii (дата звернення
30.12.2023).
2 Application Structure [Електронний ресурс] – Режим доступу до
ресурсу: https://www.yiiframework.com/doc/guide/2.0/en/structure-overview (дата
звернення 04.01.2024).
3 ActiveRecord [Електронний ресурс] – Режим доступу до ресурсу:
https://www.yiiframework.com/doc/api/2.0/yii-db-activerecord (дата звернення
05.01.2024).
4 Module [Електронний ресурс] – Режим доступу до ресурсу:
https://www.yiiframework.com/doc/api/2.0/yii-base-module (дата звернення
07.01.2024).

47
ДОДАТОК А

Програмний код

Основні налаштування і конфігурація

AppAsset.php
<?php

/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/

namespace app\assets;

use yii\web\AssetBundle;

/**
* Main application asset bundle.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
'css/style.css',
'css/custom.css',

'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-
awesome.min.css',

'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css',
];
public $js = [
'https://cdn.jsdelivr.net/npm/flatpickr',
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap5\BootstrapAsset'
];
}

config/web.php
<?php
48
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';

$config = [
'id' => 'basic',
'name' => 'Блог',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'components' => [
'request' => [
'cookieValidationKey' => 'key4732rh378yh4',
'baseUrl' => '',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
'loginUrl' => ['auth/login']
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,

'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'' => 'site/index',
'<action>' => 'site/<action>',
/* admin and user modules */
49
'admin/index' => 'admin/default/index',
'user/index' => 'user/default/index',
'<action>' => 'default/<action>',
'/login' => 'site/login',
'<action>' => 'site/<action>',
'<controller:(post|comment)>/<id:\d+>/<action:
(create|update|delete)>' => '<controller>/<action>',
'<controller:(post|comment)>/<id:\d+>' =>
'<controller>/view',
'<controller:(post|comment)>s' =>
'<controller>/index',
],
],

],

'modules' => [
'admin' => [
'class' => 'app\modules\admin\Module',
],
'user' => [
'class' => 'app\modules\user\Module',
],
],

'params' => $params,


];

if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not
connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];

$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not
connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}

return $config;

Основні контролери

50
AuthController.php
<?php

namespace app\controllers;

use app\models\LoginForm;
use app\models\SignupForm;
use app\models\User;
use Yii;
use yii\web\Controller;
use yii\web\Response;

class AuthController extends Controller


{
/**
* Login action.
*
* @return Response|string
*/
public function actionLogin()
{
if (!Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model-
>login()) {
return $this->goBack();
}
$model->password = '';
return $this->render('/site/login.php', [
'model' => $model,
]);
}

/**
* Logout action.
*
* @return Response
*/
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}

public function actionSignup()


{
$model = new SignupForm();
if (Yii::$app->request->isPost) {
$model->load(Yii::$app->request->post());
if ($model->signup()) {

51
return $this->redirect(['auth/login']);
}
}
return $this->render('/site/signup', ['model' => $model]);
}
}

SiteController.php
<?php

namespace app\controllers;

use Yii;
use yii\data\Pagination;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;

use app\models\Article;
use app\models\Comment;
use app\models\CommentForm;
use app\models\ContactForm;
use app\models\LoginForm;
use app\models\Topic;
use app\models\SearchForm;

class SiteController extends Controller


{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'only' => ['logout'],
'rules' => [
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'logout' => ['post'],
],
],

52
];
}

/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}

/**
* Displays homepage.
*
* @return string
*/
public function actionIndex()
{

$popular = Article::find()->orderBy('viewed desc')-


>limit(3)->all();
$recent = Article::find()->orderBy('date desc')->limit(3)-
>all();
$topics = Topic::find()->all();

// build a DB query to get all articles


$query = Article::find();
// get the total number of articles (but do not fetch the
article data yet)
$count = $query->count();
// create a pagination object with the total count
$pagination = new Pagination(['totalCount' => $count,
'pageSize' => 3]);
// limit the query using the pagination and retrieve the
articles
$articles = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
return $this->render('index', [
'articles' => $articles,
'pagination' => $pagination,
'popular' => $popular,
'recent' => $recent,
'topics' => $topics
]);
53
}

/**
* Displays blog page
*/
public function actionView($id)
{
$article = Article::findOne($id);

// increase view count


$article->viewedCounter();

$popular = Article::find()->orderBy('viewed desc')-


>limit(3)->all();
$recent = Article::find()->orderBy('date desc')->limit(3)-
>all();

$topics = Topic::find()->all();

$comments = $article->comments;
$commentsParent = array_filter($comments, function ($k) {
return $k['comment_id'] == null;
});
$commentsChild = array_filter($comments, function ($k) {
return ($k['comment_id'] != null && !$k['delete']);
});
$commentForm = new CommentForm();

return $this->render('single', [
'article' => $article,
'popular' => $popular,
'recent' => $recent,
'topics' => $topics,
'commentsParent' => $commentsParent,
'commentsChild' => $commentsChild,
'commentForm' => $commentForm,
]);
}

/**
* Search article
*/
public function actionSearch()
{
$model = new SearchForm();

if (Yii::$app->request->isGet) {

$model->load(Yii::$app->request->get());

$data = $model->SearchArticle(3);

54
$popular = Article::find()->orderBy('viewed desc')-
>limit(3)->all();

$recent = Article::find()->orderBy('date desc')-


>limit(3)->all();

$topics = Topic::find()->all();

return $this->render('search', [

'articles' => $data['articles'],

'pagination' => $data['pagination'],

'popular' => $popular,

'recent' => $recent,

'topics' => $topics,

'search' => $model->text

]);
}
}

/**
* Displays topic page
*/
public function actionTopic($id)
{
$query = Article::find()->where(['topic_id' => $id]);
$count = $query->count();

// create a pagination object with the total count


$pagination = new Pagination(['totalCount' => $count,
'pageSize' => 2]);

// limit the query using the pagination and retrieve the


articles
$articles = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();

$popular = Article::find()->orderBy('viewed desc')-


>limit(3)->all();
$recent = Article::find()->orderBy('date desc')->limit(3)-
>all();
$topics = Topic::find()->all();

return $this->render('topic', [
'articles' => $articles,
'pagination' => $pagination,
55
'popular' => $popular,
'recent' => $recent,
'topics' => $topics,
]);
}

/**
* Displays comment
*/
public function actionComment($id, $id_comment = null)
{
$model = new CommentForm();
if (Yii::$app->request->isPost) {
$model->load(Yii::$app->request->post());
if ($model->saveComment($id, $id_comment)) {
return $this->redirect(['site/view', 'id' => $id]);
}
}
}

/**
* Deletes comment
*/
public function actionCommentDelete($id, $id_comment)
{
if (Yii::$app->request->isPost) {
$data = Comment::findOne($id_comment);
if ($data->user_id == Yii::$app->user->id) {
$data->delete = true;
$data->save(false);
}
return $this->redirect(['site/view', 'id' => $id]);
}
}

/**
* Displays contact page.
*
* @return Response|string
*/
public function actionContact()
{
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model-
>contact(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('contactFormSubmitted');

return $this->refresh();
}
return $this->render('contact', [
'model' => $model,
]);
}
56
/**
* Displays about page.
*
* @return string
*/
public function actionAbout()
{
return $this->render('about');
}
}

Моделі

Article.php
<?php

namespace app\models;

use Yii;

/**
* This is the model class for table "article".
*
* @property int $id
* @property string|null $title
* @property string|null $description
* @property string|null $date
* @property string|null $image
* @property string|null $tag
* @property int|null $viewed
* @property int|null $topic_id
* @property int|null $user_id
*
* @property Comment[] $comments
* @property Topic $topic
* @property User $user
*/
class Article extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'article';
}

/**
* {@inheritdoc}
*/
public function rules()

57
{
return [
[['title', 'description', 'date', 'user_id',
'topic_id'], 'required'],
[['description'], 'string'],
[['date'], 'safe'],
[['viewed', 'topic_id', 'user_id'], 'integer'],
[['title', 'image', 'tag'], 'string', 'max' => 255],
[['user_id'], 'exist', 'skipOnError' => true,
'targetClass' => User::class, 'targetAttribute' => ['user_id' =>
'id']],
[['topic_id'], 'exist', 'skipOnError' => true,
'targetClass' => Topic::class, 'targetAttribute' => ['topic_id' =>
'id']],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'title' => 'Title',
'description' => 'Description',
'date' => 'Date',
'image' => 'Image',
'tag' => 'Tag',
'viewed' => 'Viewed',
'topic_id' => 'Topic ID',
'user_id' => 'User ID',
];
}

/**
* Gets query for [[Comments]].
*
* @return \yii\db\ActiveQuery
*/
public function getComments()
{
return $this->hasMany(Comment::class, ['article_id' =>
'id']);
}

/**
* Gets query for [[Topic]].
*
* @return \yii\db\ActiveQuery
*/
public function getTopic()
{
58
return $this->hasOne(Topic::class, ['id' => 'topic_id']);
}

/**
* Gets query for [[User]].
*
* @return \yii\db\ActiveQuery
*/
public function getUser()
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}

public function getImage()


{
if ($this->image) {
return '/uploads/' . $this->image;
}
return '/no-image-available.jpg';
}

public function saveArticle()


{
$this->user_id = Yii::$app->user->id;

return $this->save();
}

public function saveImage($filename)


{
$this->image = $filename;
return $this->save(false);
}

public function deleteImage()


{
$imageUploadModel = new ImageUpload();
$imageUploadModel->deleteCurrentImage($this->image);
}

public function beforeDelete()


{
$this->deleteImage();
return parent::beforeDelete(); // TODO: Change the
autogenerated stub
}

public function getDate()


{
return Yii::$app->formatter->asDate($this->date);
}

/**
59
* View counter for article
*/

public function viewedCounter()


{
$this->viewed += 1;
return $this->save(false);
}
}

ArticleSearch.php
<?php

namespace app\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\Article;

/**
* ArticleSearch represents the model behind the search form of
`app\models\Article`.
*/
class ArticleSearch extends Article
{
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'viewed', 'topic_id', 'user_id'], 'integer'],
[['title', 'description', 'date', 'image', 'tag'],
'safe'],
];
}

/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}

/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider

60
*/
public function search($params)
{
$query = Article::find();

// add conditions that should always apply here

$dataProvider = new ActiveDataProvider([


'query' => $query,
]);

$this->load($params);

if (!$this->validate()) {
// uncomment the following line if you do not want to
return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}

// grid filtering conditions


$query->andFilterWhere([
'id' => $this->id,
'date' => $this->date,
'viewed' => $this->viewed,
'topic_id' => $this->topic_id,
'user_id' => $this->user_id,
]);

$query->andFilterWhere(['like', 'title', $this->title])


->andFilterWhere(['like', 'description', $this-
>description])
->andFilterWhere(['like', 'image', $this->image])
->andFilterWhere(['like', 'tag', $this->tag]);

return $dataProvider;
}
}

Comment.php
<?php

namespace app\models;

use Yii;

/**
* This is the model class for table "comment".
*
* @property int $id
* @property string|null $text
* @property int|null $user_id

61
* @property int|null $comment_id
* @property int|null $article_id
* @property string|null $date
* @property int|null $delete
*
* @property Article $article
* @property Comment $comment
* @property Comment[] $comments
* @property User $user
*/
class Comment extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'comment';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['date', 'text', 'article_id'], 'required'],
[['user_id', 'comment_id', 'article_id', 'delete'],
'integer'],
[['date'], 'safe'],
[['text'], 'string', 'max' => 255],
[['article_id'], 'exist', 'skipOnError' => true,
'targetClass' => Article::class, 'targetAttribute' => ['article_id'
=> 'id']],
[['user_id'], 'exist', 'skipOnError' => true,
'targetClass' => User::class, 'targetAttribute' => ['user_id' =>
'id']],
[['comment_id'], 'exist', 'skipOnError' => true,
'targetClass' => Comment::class, 'targetAttribute' => ['comment_id'
=> 'id']],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'text' => 'Text',
'user_id' => 'User ID',
'comment_id' => 'Comment ID',
62
'article_id' => 'Article ID',
'date' => 'Date',
'delete' => 'Delete',
];
}

/**
* Gets query for [[Article]].
*
* @return \yii\db\ActiveQuery
*/
public function getArticle()
{
return $this->hasOne(Article::class, ['id' =>
'article_id']);
}

/**
* Gets query for [[Comment]].
*
* @return \yii\db\ActiveQuery
*/
public function getComment()
{
return $this->hasOne(Comment::class, ['id' =>
'comment_id']);
}

/**
* Gets query for [[Comments]].
*
* @return \yii\db\ActiveQuery
*/
public function getComments()
{
return $this->hasMany(Comment::class, ['comment_id' =>
'id']);
}

/**
* Gets query for [[User]].
*
* @return \yii\db\ActiveQuery
*/
public function getUser()
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}

public function getDate()


{
return Yii::$app->formatter->asDate($this->date);
}
63
}

CommentForm.php
<?php

namespace app\models;

use Yii;
use yii\base\Model;

class CommentForm extends Model


{
public $comment;

public function rules()


{
return [
[['comment'], 'required']
];
}
public function saveComment($article_id, $comment_id)
{
$comment = new Comment;
$comment->text = $this->comment;
if (Yii::$app->user) {
$comment->user_id = Yii::$app->user->id;
} else {
$comment->user_id = NULL;
}

$comment->article_id = $article_id;
if ($comment_id != null) {
$comment->comment_id = $comment_id;
}
$comment->date = date('Y-m-d');

return $comment->save();
}
}

CommentSearch.php
<?php

namespace app\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\Comment;

/**

64
* CommentSearch represents the model behind the search form of
`app\models\Comment`.
*/
class CommentSearch extends Comment
{
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'user_id', 'comment_id', 'article_id',
'delete'], 'integer'],
[['text', 'date'], 'safe'],
];
}

/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}

/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Comment::find();

// add conditions that should always apply here

$dataProvider = new ActiveDataProvider([


'query' => $query,
]);

$this->load($params);

if (!$this->validate()) {
// uncomment the following line if you do not want to
return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}

// grid filtering conditions


65
$query->andFilterWhere([
'id' => $this->id,
'user_id' => $this->user_id,
'comment_id' => $this->comment_id,
'article_id' => $this->article_id,
'date' => $this->date,
'delete' => $this->delete,
]);

$query->andFilterWhere(['like', 'text', $this->text]);

return $dataProvider;
}
}

ImageUpload.php
<?php

namespace app\models;

use Yii;
use yii\base\Model;
use yii\web\UploadedFile;

class ImageUpload extends Model


{
public $image;

public function rules()


{
return [
[['image'], 'required'],
[['image'], 'file', 'extensions' => 'jpg,png']
];
}

public function uploadFile(UploadedFile $file, $currentImage)


{
$this->image = $file;

if ($this->validate()) {
$this->deleteCurrentImage($currentImage);

$filename = strtolower(md5(uniqid($file->baseName)) .
'.' . $file->extension);
$file->saveAs(Yii::getAlias('@web') . 'uploads/' .
$filename);

return $filename;
}
}

66
public function deleteCurrentImage($currentImage)
{
if (
file_exists(Yii::getAlias('@web') . 'uploads/' .
$currentImage) &&
is_file(Yii::getAlias('@web') . 'uploads/' .
$currentImage)
) {
unlink(Yii::getAlias('@web') . 'uploads/' .
$currentImage);
}
}
}
LoginForm.php
<?php

namespace app\models;

use Yii;
use yii\base\Model;

/**
* LoginForm is the model behind the login form.
*
* @property-read User|null $user
*
*/
class LoginForm extends Model
{
public $username;
public $password;
public $rememberMe = true;

private $_user = false;

/**
* @return array the validation rules.
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}

/**
* Validates the password.

67
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being
validated
* @param array $params the additional name-value pairs given in
the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();

if (!$user || !$user->validatePassword($this->password))
{
$this->addError($attribute, 'Неправильний логін або
пароль.');
}
}
}

/**
* Logs in a user using the provided username and password.
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this-
>rememberMe ? 3600*24*30 : 0);
}
return false;
}

/**
* Finds user by [[username]]
*
* @return User|null
*/
public function getUser()
{
if ($this->_user === false) {
$this->_user = User::findByUsername($this->username);
}

return $this->_user;
}
}

SearchForm.php
<?php

namespace app\models;

68
use Yii;
use yii\base\Model;
use yii\data\Pagination;

class SearchForm extends Model


{
public $text;
public function rules()
{
return [
[['text'], 'required']
];
}
public function SearchArticle($pageSize = 5)
{
$query = Article::find()->andWhere(['like', 'tag', '%' .
$this->text . '%', false]);
$count = $query->count();
// create a pagination object with the total count
$pagination = new Pagination(['totalCount' => $count,
'pageSize' => $pageSize]);
// limit the query using the pagination and retrieve the
articles
$articles = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
$data['articles'] = $articles;
$data['pagination'] = $pagination;
return $data;
}
}

SignupForm.php
<?php

namespace app\models;

use yii\base\Model;

class SignupForm extends Model


{
public $name;
public $login;
public $password;

public function rules()


{
return [
[['name', 'login', 'password'], 'required'],
['login', 'email'],
[['name', 'login', 'password'], 'string'],

69
[['login'], 'unique', 'targetClass' => 'app\models\
User', 'targetAttribute' => 'login'],
];
}

public function signup()


{
if ($this->validate()) {
$user = new User();
$user->attributes = $this->attributes;
return $user->create();
}
}
}

Topic.php
<?php

namespace app\models;

use Yii;

/**
* This is the model class for table "topic".
*
* @property int $id
* @property string|null $name
*
* @property Article[] $articles
*/
class Topic extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'topic';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['name'], 'required'],
[['name'], 'string', 'max' => 255],
];
}

/**

70
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'name' => 'Name',
];
}

/**
* Gets query for [[Articles]].
*
* @return \yii\db\ActiveQuery
*/
public function getArticles()
{
return $this->hasMany(Article::class, ['topic_id' => 'id']);
}
}

TopicSearch.php
<?php

namespace app\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\Topic;

/**
* TopicSearch represents the model behind the search form of `app\
models\Topic`.
*/
class TopicSearch extends Topic
{
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id'], 'integer'],
[['name'], 'safe'],
];
}

/**
* {@inheritdoc}
*/
public function scenarios()
{

71
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}

/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Topic::find();

// add conditions that should always apply here

$dataProvider = new ActiveDataProvider([


'query' => $query,
]);

$this->load($params);

if (!$this->validate()) {
// uncomment the following line if you do not want to
return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}

// grid filtering conditions


$query->andFilterWhere([
'id' => $this->id,
]);

$query->andFilterWhere(['like', 'name', $this->name]);

return $dataProvider;
}
}

User.php
<?php

namespace app\models;

use Yii;
use yii\web\IdentityInterface;

/**
* This is the model class for table "user".
*

72
* @property int $id
* @property string|null $name
* @property string|null $login
* @property string|null $password
* @property string|null $image
*
* @property Article[] $articles
* @property Comment[] $comments
*/
class User extends \yii\db\ActiveRecord implements IdentityInterface
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'user';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['name', 'login', 'password'], 'required'],
['login', 'email'],
[
['login'], 'unique',
'message' => 'Цей логін вже використовується.'
],
[['name', 'login', 'password'], 'string'],
[['name', 'login', 'password', 'image'], 'string', 'max'
=> 255],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'name' => 'Name',
'login' => 'Login',
'password' => 'Password',
'image' => 'Image',
];
}

/**
* Gets query for [[Articles]].
73
*
* @return \yii\db\ActiveQuery
*/
public function getArticles()
{
return $this->hasMany(Article::class, ['user_id' => 'id']);
}

/**
* Gets query for [[Comments]].
*
* @return \yii\db\ActiveQuery
*/
public function getComments()
{
return $this->hasMany(Comment::class, ['user_id' => 'id']);
}

public function saveImage($filename)


{
$this->image = $filename;
return $this->save(false);
}

public function getImage()


{
if ($this->image) {
return '/uploads/' . $this->image;
}
return '/no-image-available.jpg';
}

public function deleteImage()


{
$imageUploadModel = new ImageUpload();
$imageUploadModel->deleteCurrentImage($this->image);
}

public function beforeDelete()


{
$this->deleteImage();
return parent::beforeDelete(); // TODO: Change the
autogenerated stub
}

/**
* @inheritDoc
*/
public static function findIdentity($id)
{
return User::findOne($id);
}
/**
74
* @inheritDoc
*/
public static function findIdentityByAccessToken($token, $type =
null)
{
// TODO: Implement findIdentityByAccessToken() method.
}
/**
* @inheritDoc
*/
public function getId()
{
return $this->id;
}
/**
* @inheritDoc
*/
public function getAuthKey()
{
// TODO: Implement getAuthKey() method.
}
/**
* @inheritDoc
*/
public function validateAuthKey($authKey)
{
// TODO: Implement validateAuthKey() method.
}

public static function findByUsername($username)


{
return User::find()->where(['login' => $username])->one();
}

public function validatePassword($password)


{
//return ($this->password == $password) ? true : false;
return Yii::$app
->getSecurity()
->validatePassword($password, $this->password) ? true :
false;
}

public function getUsername()


{
return $this->name;
}

public function getDate()


{
return Yii::$app->formatter->asDate($this->date);
}

75
public function create()
{
$hash = Yii::$app->getSecurity()-
>generatePasswordHash($this->password);
$this->password = $hash;
// dd($this);
return $this->save(false);
}

public function myUpdate()


{
$user = static::findOne($this->id);
if ($user->password != $this->password) {
// $hash = Yii::$app->getSecurity()-
>generatePasswordHash($this->password);
// $this->password = $hash;
return $this->create();
}

return $this->save(false);
}

public function isAdmin()


{
return $this->is_admin == 1 ? true : false;
}
}

UserSearch.php
<?php

namespace app\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\User;

/**
* UserSearch represents the model behind the search form of `app\
models\User`.
*/
class UserSearch extends User
{
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id'], 'integer'],
[['name', 'login', 'password', 'image'], 'safe'],
];

76
}

/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}

/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = User::find();

// add conditions that should always apply here

$dataProvider = new ActiveDataProvider([


'query' => $query,
]);

$this->load($params);

if (!$this->validate()) {
// uncomment the following line if you do not want to
return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}

// grid filtering conditions


$query->andFilterWhere([
'id' => $this->id,
]);

$query->andFilterWhere(['like', 'name', $this->name])


->andFilterWhere(['like', 'login', $this->login])
->andFilterWhere(['like', 'password', $this->password])
->andFilterWhere(['like', 'image', $this->image]);

return $dataProvider;
}
}

Міграції

77
<?php

use yii\db\Migration;

/**
* Handles the creation of table `{{%topic}}`.
*/
class m240106_201024_create_topic_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable('{{%topic}}', [
'id' => $this->primaryKey(),
'name' => $this->string()->notNull(),
]);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable('{{%topic}}');
}
}

<?php

use yii\db\Migration;

/**
* Handles the creation of table `{{%user}}`.
*/
class m240106_201056_create_user_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable('{{%user}}', [
'id' => $this->primaryKey(),
'name' => $this->string()->notNull(),
'login' => $this->string()->notNull(),
'password' => $this->string()->notNull(),
'image' => $this->string(),
'is_admin' => $this->boolean()->notNull(),
]);
}
78
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable('{{%user}}');
}
}

<?php

use yii\db\Migration;

/**
* Handles the creation of table `{{%article}}`.
*/
class m240106_201103_create_article_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable('{{%article}}', [
'id' => $this->primaryKey()->notNull(),
'title' => $this->string()->notNull(),
'description' => $this->text()->notNull(),
'date' => $this->date()->notNull(),
'image' => $this->string(),
'tag' => $this->string(),
'viewed' => $this->integer()->notNull()->default(0),
'topic_id' => $this->integer()->notNull(),
'user_id' => $this->integer()->notNull(),
]);
// create index for column `topic_id`
$this->createIndex(
'idx-topic_id',
'article',
'topic_id'
);
// add foreign key for table `topic`
$this->addForeignKey(
'fk-topic_id',
'article',
'topic_id',
'topic',
'id',
'CASCADE'
);
// create index for column `user_id`
$this->createIndex(
'idx-post-user_id',
79
'article',
'user_id'
);
// add foreign key for table `user`
$this->addForeignKey(
'fk-post-user_id',
'article',
'user_id',
'user',
'id',
'CASCADE'
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable('{{%article}}');
}
}

<?php

use yii\db\Migration;

/**
* Handles the creation of table `{{%comment}}`.
*/
class m240106_201117_create_comment_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable('{{%comment}}', [
'id' => $this->primaryKey(),
'text' => $this->string()->notNull(),
'user_id' => $this->integer(),
'comment_id' => $this->integer(),
'article_id' => $this->integer()->notNull(),
'date' => $this->date()->notNull(),
'delete' => $this->boolean(),
]);
// create index for column `user_id`
$this->createIndex(
'idx-post-user_id',
'comment',
'user_id'
);
80
// add foreign key for table `user`
$this->addForeignKey(
'fk-comment-post-user_id',
'comment',
'user_id',
'user',
'id',
'CASCADE'
);
// create index for column `article_id`
$this->createIndex(
'idx-article_id',
'comment',
'article_id'
);
// add foreign key for table `article`
$this->addForeignKey(
'fk-article_id',
'comment',
'article_id',
'article',
'id',
'CASCADE'
);
// create index for column `comment_id`
$this->createIndex(
'idx-comment_id',
'comment',
'comment_id'
);
// add foreign key
$this->addForeignKey(
'fk-comment_id',
'comment',
'comment_id',
'comment',
'id',
'CASCADE'
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable('{{%comment}}');
}
}

Стилі

81
style.css
@import url(https://fonts.googleapis.com/css?
family=Open+Sans:400,400italic,700,600);
@import url(https://fonts.googleapis.com/css?
family=Merriweather:400,700,400italic);

body {
font-family: 'Open Sans', sans-serif;
background: #ccccc4;
}

.main-content {
margin-top: 100px;
}

.row {
margin-right: -15px;
margin-left: -15px;
}

.post {
background: #fff;
margin-bottom: 50px;
}

article {
border: 1px solid #d6d8d0;
}

.post-content {
padding: 0 43px;
overflow: hidden;
}

.post-thumb img {
width: 100%;
}

.post-thumb {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;

82
flex-direction: column;
}

.entry-header {
margin: 40px 0 23px;
}

.entry-header h1 {
margin-top: 0;
font-size: 24px;
}

span,
p,
input {
font-family: 'Merriweather', serif;
line-height: 26px;
}

.post .social-share {
border-top: 1px solid #EEE;
padding: 15px 0 75px;
}

.social-share-title {
padding: 9px 0;
font-style: italic;
font-size: 13px;
}

/* .pagination {
display: inline-block;
padding-left: 0;
margin: 0 0 50px;
border-radius: 4px;
} */

/* Основний контейнер для пагінації */


.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 20px 0;
}

/* Стилі для окремого елемента пагінації */


.pagination li {
margin-right: 5px;
}

/* Задаємо стилі для посилань */


.pagination a {
display: block;
83
padding: 8px 12px;
text-decoration: none;
color: #333;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
transition: background-color 0.3s ease;
}

/* Задаємо стилі для активного елемента */


.pagination li.active a {
background-color: #686945;
color: #fff;
border: 1px solid #686945;
}

/* Змінюємо стилі при наведенні курсора на посилання */


.pagination a:hover {
background-color: #ddd;
}

.entry-header h1 a:hover {
color: #7b7c58;
}

.entry-header h1 a {
font-weight: 600;
color: #333333;
-webkit-transition: all 0.5s;
transition: all 0.5s;
font-size: 24px;
line-height: 34px;
}

.social-share ul li {
list-style: none;
display: inline-block;
}

.social-share ul li a i {
height: 30px;
width: 30px;
line-height: 30px;
font-size: 15px;
border-radius: 50%;
margin: 3px;
border: 1px solid #e5e5e5;
-webkit-transition: all 0.4s;
transition: all 0.4s;
color: #C2C2C2;
}

.social-share span a {
84
color: #333;
}

.pagination>li>a,
.pagination>li>span {
position: relative;
float: left;
padding: 6px 12px;
margin-left: 5px;
line-height: 1.42857143;
color: #333;
text-decoration: none;
background-color: #fff;
border: 0;
-webkit-transition: all 0.3s;
transition: all 0.3s;
font-weight: 600;
}

.widget {
padding: 37px 30px;
margin-bottom: 35px;
background-color: #fff;
border: 1px solid #eee;
}

.widget-title {
font-size: 18px;
margin: 0 0 25px;
letter-spacing: 0.5px;
color: #333333;
}

.widget .popular-post:first-of-type {
padding-top: 0;
}

.popular-post {
border-bottom: 1px solid #eee;
padding: 25px 0 20px;
}

.popular-img {
position: relative;
width: 100%;
margin-bottom: 15px;
}

.popular-img img {
width: 100%;
}

85
img {
max-width: 100%;
}

.p-overlay {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
-webkit-transition: all 0.44s;
transition: all 0.44s;
}

.p-content a {
font-size: 12px;
color: #333333;
text-decoration: none;
display: block;
font-weight: 600;
transition: all 0.44s;
}

.p-content span {
color: #666666;
font-size: 12px;
font-style: italic;
}

/* comments */
.leave-comment {
background-color: #fff;
margin: 30px 0;
padding: 20px;
color: #212121;
}

.send-btn {
background: #333;
color: #fff;
text-transform: uppercase;
-webkit-transition: all .33s;
transition: all .33s;
border-radius: 0;
}

.leave-comment h4 {
font-size: 14px;
text-transform: uppercase;
font-weight: 700;
}
86
.leave-comment input,
.leave-comment textarea {
background-color: #FAFAFA;
color: #999999;
border-radius: 0;
font-size: 14px;
line-height: 28px;
padding: 20px;
border-color: #eee;
-webkit-box-shadow: inset 0 0 0 rgba(0, 0, 0, .075);
box-shadow: inset 0 0 0 rgba(0, 0, 0, .075);
}

body .tab-content .comment {


background: #fff;
padding: 10px;
}

body .comment {
position: relative;
border-bottom: 1px solid #c7b8b8;
padding: 10px;
min-height: 120px;
background-color: white;
}

body .comment-img {
float: left;
margin-right: 10px;
width: 100px;
height: 100px;
overflow: hidden;
position: relative;
}

body .comment-body {
margin-left: 110px;
}

body .comment-top {
display: flex;
justify-content: space-between;
}

body .comment-date {
font-size: 12px;
}

body .comment-text {
word-break: break-word;
padding-right: 10px;
}
87
body .comment-childs-container {
transition: max-height .4s;
}

body .comment-more-button {
text-align: center;
padding: 8px 8px 8px 30px;
border: 1px solid #e2e2e2;
border-top: none;
cursor: pointer;
}

body .comment-childs {
padding: 10px 0 10px 50px;
}

a.replay.btn.pull-right {
padding: 8px 7px;
display: inline-block;
font-size: 12px;
cursor: pointer;
margin-left: 10px;
color: #333;
background-color: white;
border: 1px solid #e2e2e2;
}

img.img-round {
border-radius: 50%;
height: 100px;
width: 100px;
}

i.fa.fa-trash {
font-size: 2.5em;
}

.comment-delete {
float: right;
margin-top: -35px;
}

.leave-comment-child {
background-color: white;
padding: 10px;
}

.post-list .post-content {
padding: 0 20px 0 0;
overflow: hidden;
}

88
.post-list .entry-header {
margin: 22px 0;
}

.entry-header h6 a {
color: #686945;
font-weight: 700;
font-size: 12px;
}
.entry-header h6 a:hover {
color: #7b7c58;
}

.entry-header h1 a,
.entry-header h6 a {
text-decoration: none;
letter-spacing: 0.5px;
}

.post-grid .entry-header h1,


.post-list .entry-header h1 {
margin: 0;
line-height: 24px;
}

.post-grid .entry-header h1 a,
.post-list .entry-header h1 a {
font-size: 18px;
line-height: 24px;
}

.search-form {
height: 90px;
}

.form-group.field-searchform-text.required {
font-size: 17px;
float: left;
width: 80%;
}

.btn-search {
float: left;
width: 20%;
padding: 4px;
background: #686945;
color: white;
font-size: 17px;
border: 1px solid grey;
border-radius: 5px;
border-left: none;
cursor: pointer;
height: 38px;
89
margin-top: 1px;
}

.btn-search:hover {
background: #7b7c58;
}

custom.css
a {
text-decoration: none;
display: inline-block;
color: #686945;
}

a:hover {
color: #73770f;
}

/* .form-control, .form-control:focus {
border-color: #686945;
}

form label {
border-color: #686945;
} */

form .btn {
background-color: #686945;
border: 1px solid #686945;
font-size: 15px;
}

.form-check-label::before{
color: #686945;
}

select {
background-color: #fff;
border: 1px solid #686945;
color: #686945;
cursor: pointer;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="%23686945" width="18px" height="18px"><path d="M7 10l5 5 5-5z"
/></svg>');
background-repeat: no-repeat;
background-position: right center;
padding-right: 20px;
}

select:hover {

90
border-color: #A29E71;
}

form .my-btn-success {
background-color: #198754;
border-color: #198754;
}

form .my-btn-success:hover, form .my-btn-success:active {


background-color: #A29E71;
border-color: #A29E71;
}

form .my-btn-primary {
background-color: #0a58ca;
border-color: #0a58ca;
}

form .my-btn-primary:hover,
form .my-btn-primary:active {
background-color: #A29E71;
border-color: #A29E71;
}

/* адмінка */

.module-default-index {
display: block;
background-color: #f5f5f5;
margin: 50px 30px;
padding: 15px;
}

.module-default-index > p {
margin-top: 20px;
}

.module-default-index .features {
margin: 0;
padding: 15px 10px;
display: flex;
justify-content: center;
height: auto;
}

.module-default-index .features .feature {


max-width: 300px;
margin: 0 10px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
91
}

.module-default-index .features .feature h1 {


color: #333;
}

.module-default-index .features .feature p {


color: #555;
margin-bottom: 15px;
}

.module-default-index .features .feature {


margin-bottom: 10px;
}

.module-default-index .features .feature a {


text-decoration: none;
color: #686945;
}

.module-default-index .features .feature a:hover {


color: #A29E71;
}

Layout

admin.php
<?php

/** @var yii\web\View $this */


/** @var string $content */

use app\assets\AppAsset;
use app\widgets\Alert;
use yii\bootstrap5\Breadcrumbs;
use yii\bootstrap5\Html;
use yii\bootstrap5\Nav;
use yii\bootstrap5\NavBar;

AppAsset::register($this);

$this->registerCsrfMetaTags();
$this->registerMetaTag(['charset' => Yii::$app->charset],
'charset');
$this->registerMetaTag(['name' => 'viewport', 'content' =>
'width=device-width, initial-scale=1, shrink-to-fit=no']);
$this->registerMetaTag(['name' => 'description', 'content' => $this-
>params['meta_description'] ?? '']);
$this->registerMetaTag(['name' => 'keywords', 'content' => $this-
>params['meta_keywords'] ?? '']);
$this->registerLinkTag(['rel' => 'icon', 'type' => 'image/x-icon',
'href' => Yii::getAlias('@web/favicon.ico')]);

92
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>" class="h-100">

<head>
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>

<body class="d-flex flex-column h-100">


<?php $this->beginBody() ?>

<header id="header">
<?php
NavBar::begin([
'brandLabel' => Yii::$app->name,
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-md navbar-dark
bg-dark fixed-top']
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' => 'Головна', 'url' =>
['/admin/default/index']],
['label' => 'Користувачі', 'url' =>
['/admin/user/index']],
['label' => 'Статті', 'url' =>
['/admin/article/index']],
['label' => 'Коментарі', 'url' =>
['/admin/comment/index']],
['label' => 'Категорії', 'url' =>
['/admin/topic/index']],
'<li class="nav-item">'
. Html::beginForm(['/auth/logout'])
. Html::submitButton(
'Вийти (' . Yii::$app->user->identity-
>username . ')',
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>',
],
]);
NavBar::end();
?>
</header>

<main id="main" class="flex-shrink-0" role="main">


<div class="container">
<?php if (!empty($this->params['breadcrumbs'])) : ?>

93
<?= Breadcrumbs::widget(['links' => $this-
>params['breadcrumbs']]) ?>
<?php endif ?>
<?= Alert::widget() ?>
<?= $content ?>
</div>
</main>

<footer id="footer" class="mt-auto py-3 bg-light">


<div class="container">
<div class="row text-muted">
<div class="col-md-6 text-center text-md-
start">&copy; Kasianenko D, <?= date('Y') ?></div>
<div class="col-md-6 text-center text-md-end"><?=
Yii::powered() ?></div>
</div>
</div>
</footer>

<?php $this->endBody() ?>


</body>

</html>
<?php $this->endPage() ?>

user.php
<?php

/** @var yii\web\View $this */


/** @var string $content */

use app\assets\AppAsset;
use app\widgets\Alert;
use yii\bootstrap5\Breadcrumbs;
use yii\bootstrap5\Html;
use yii\bootstrap5\Nav;
use yii\bootstrap5\NavBar;

AppAsset::register($this);

$this->registerCsrfMetaTags();
$this->registerMetaTag(['charset' => Yii::$app->charset],
'charset');
$this->registerMetaTag(['name' => 'viewport', 'content' =>
'width=device-width, initial-scale=1, shrink-to-fit=no']);
$this->registerMetaTag(['name' => 'description', 'content' => $this-
>params['meta_description'] ?? '']);
$this->registerMetaTag(['name' => 'keywords', 'content' => $this-
>params['meta_keywords'] ?? '']);
$this->registerLinkTag(['rel' => 'icon', 'type' => 'image/x-icon',
'href' => Yii::getAlias('@web/favicon.ico')]);
?>

94
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>" class="h-100">

<head>
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>

<body class="d-flex flex-column h-100">


<?php $this->beginBody() ?>

<header id="header">
<?php
NavBar::begin([
'brandLabel' => Yii::$app->name,
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-md navbar-dark
bg-dark fixed-top']
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' => 'Головна', 'url' =>
['/user/default/index']],
['label' => 'Обліковий запис', 'url' =>
['/user/user/index']],
['label' => 'Статті', 'url' =>
['/user/article/index']],
'<li class="nav-item">'
. Html::beginForm(['/auth/logout'])
. Html::submitButton(
'Вийти (' . Yii::$app->user->identity-
>username . ')',
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>',
],
]);
NavBar::end();
?>
</header>

<main id="main" class="flex-shrink-0" role="main">


<div class="container">
<?php if (!empty($this->params['breadcrumbs'])) : ?>
<?= Breadcrumbs::widget(['links' => $this-
>params['breadcrumbs']]) ?>
<?php endif ?>
<?= Alert::widget() ?>
<?= $content ?>
</div>
95
</main>

<footer id="footer" class="mt-auto py-3 bg-light">


<div class="container">
<div class="row text-muted">
<div class="col-md-6 text-center text-md-
start">&copy; Kasianenko D, <?= date('Y') ?></div>
<div class="col-md-6 text-center text-md-end"><?=
Yii::powered() ?></div>
</div>
</div>
</footer>

<?php $this->endBody() ?>


</body>

</html>
<?php $this->endPage() ?>

Модуль адміністратора

Module.php
<?php

namespace app\modules\admin;

use yii\filters\AccessControl;

/**
* admin module definition class
*/
class Module extends \yii\base\Module
{
/**
* {@inheritdoc}
*/
public $layout = '/admin';
public $controllerNamespace = 'app\modules\admin\controllers';

/**
* {@inheritdoc}
*/
public function init()
{
parent::init();

// custom initialization code goes here


}

public function behaviors()


{
return [

96
'access' => [
'class' => AccessControl::class,
'denyCallback' => function ($rule, $action) {
throw new \yii\web\NotFoundHttpException();
},
'rules' => [
[
'allow' => true,
'matchCallback' => function ($rule, $action)
{
if (isset(\Yii::$app->user->identity)) {
return (\Yii::$app->user->identity-
>isAdmin());
} else {
return false;
}
}
]
]
]
];
}
}

ArticleController.php
<?php

namespace app\modules\admin\controllers;

use app\models\Article;
use app\models\ArticleSearch;
use app\models\ImageUpload;
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\UploadedFile;

/**
* ArticleController implements the CRUD actions for Article model.
*/
class ArticleController extends Controller
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [

97
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
]
);
}

/**
* Lists all Article models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new ArticleSearch();
$dataProvider = $searchModel->search($this->request-
>queryParams);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Article model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Article model.
* If creation is successful, the browser will be redirected to
the 'view' page.
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new Article();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>saveArticle()) {

98
return $this->redirect(['view', 'id' => $model-
>id]);
}
} else {
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

/**
* Updates an existing Article model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);

if ($this->request->isPost && $model->load($this->request-


>post()) && $model->saveArticle()) {
return $this->redirect(['view', 'id' => $model->id]);
}

return $this->render('update', [
'model' => $model,
]);
}

/**
* Set image for article
*/
public function actionSetImage($id)
{
$model = new ImageUpload();

if (Yii::$app->request->isPost) {

$article = $this->findModel($id);

$file = UploadedFile::getInstance($model, 'image');


if ($article->saveImage($model->uploadFile($file,
$article->image))) {
return $this->redirect(['view', 'id' => $article-
>id]);
}
}

99
return $this->render('image', ['model' => $model]);
}

/**
* Deletes an existing Article model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Article model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return Article the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Article::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}
}

CommentController.php
<?php

namespace app\modules\admin\controllers;

use app\models\Comment;
use app\models\CommentSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* CommentController implements the CRUD actions for Comment model.
*/
class CommentController extends Controller

100
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
]
);
}

/**
* Lists all Comment models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new CommentSearch();
$dataProvider = $searchModel->search($this->request-
>queryParams);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Comment model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Comment model.
* If creation is successful, the browser will be redirected to
the 'view' page.
101
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new Comment();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>save()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
} else {
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

/**
* Updates an existing Comment model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
// public function actionUpdate($id)
// {
// $model = $this->findModel($id);

// if ($this->request->isPost && $model->load($this-


>request->post()) && $model->save()) {
// return $this->redirect(['view', 'id' => $model->id]);
// }

// return $this->render('update', [
// 'model' => $model,
// ]);
// }

/**
* Deletes an existing Comment model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
102
$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Comment model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return Comment the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Comment::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}
}

TopicController.php
<?php

namespace app\modules\admin\controllers;

use app\models\Topic;
use app\models\TopicSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* TopicController implements the CRUD actions for Topic model.
*/
class TopicController extends Controller
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],

103
],
],
]
);
}

/**
* Lists all Topic models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new TopicSearch();
$dataProvider = $searchModel->search($this->request-
>queryParams);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Topic model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Topic model.
* If creation is successful, the browser will be redirected to
the 'view' page.
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new Topic();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>save()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
} else {
104
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

/**
* Updates an existing Topic model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);

if ($this->request->isPost && $model->load($this->request-


>post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}

return $this->render('update', [
'model' => $model,
]);
}

/**
* Deletes an existing Topic model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Topic model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return Topic the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
105
protected function findModel($id)
{
if (($model = Topic::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}
}

UserController.php
<?php

namespace app\modules\admin\controllers;

use Yii;
use app\models\ImageUpload;
use app\models\User;
use app\models\UserSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\web\UploadedFile;
use yii\filters\VerbFilter;

/**
* UserController implements the CRUD actions for User model.
*/
class UserController extends Controller
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
]
);
}

/**
* Lists all User models.
*
* @return string

106
*/
public function actionIndex()
{
$searchModel = new UserSearch();
$dataProvider = $searchModel->search($this->request-
>queryParams);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single User model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new User model.
* If creation is successful, the browser will be redirected to
the 'view' page.
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new User();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>validate()) {
if ($model->create()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
}
} else {
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

107
/**
* Updates an existing User model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);

if ($this->request->isPost && $model->load($this->request-


>post())&& $model->validate()) {
if ($model->myUpdate()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
}

return $this->render('update', [
'model' => $model,
]);
}

/**
* Update user image
*/
public function actionSetImage($id)
{
$modelUser = new ImageUpload;
if (Yii::$app->request->isPost) {
$user = $this->findModel($id);
$file = UploadedFile::getInstance($modelUser, 'image');
if ($user->saveImage($modelUser->uploadFile($file,
$user->image))) {
return $this->redirect(['view', 'id' => $user->id]);
}
}
return $this->render('image', ['model' => $modelUser]);
}

/**
* Deletes an existing User model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();
108
return $this->redirect(['index']);
}

/**
* Finds the User model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return User the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = User::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}
}

views/default/index.php
<?php

use yii\helpers\Url;
?>

<div class="module-default-index">
<h1>Вітаємо на панелі адміністратора!</h1>
<p>Даний модуль дозволяє вам:</p>

<div class="features">
<div class="feature">
<p>Управляти користувачами блогу</p>
<a href="<?= Url::toRoute('/admin/user/') ?>">
Управління користувачами
</a>
</div>

<div class="feature">
<p>Переглядати та редагувати статті</p>
<a href="<?= Url::toRoute('/admin/article/') ?>">
Перегляд та редагування статей
</a>
</div>

<div class="feature">
<p>Працювати з категоріями</p>
<a href="<?= Url::toRoute('/admin/topic/') ?>">
Категорії статей

109
</a>
</div>

<div class="feature">
<p>Та коментарями до статей</p>
<a href="<?= Url::toRoute('/admin/comment') ?>">
Коментарі до статей
</a>
</div>
</div>

</div>

Модуль користувача

Module.php
<?php

namespace app\modules\user;

use Yii;
use yii\filters\AccessControl;

/**
* user module definition class
*/
class Module extends \yii\base\Module
{
/**
* {@inheritdoc}
*/
public $layout = '/user';
public $controllerNamespace = 'app\modules\user\controllers';

/**
* {@inheritdoc}
*/
public function init()
{
parent::init();

// custom initialization code goes here


}

public function behaviors()


{
return [
'access' => [
'class' => AccessControl::class,
'denyCallback' => function ($rule, $action) {
throw new \yii\web\NotFoundHttpException();
110
},
'rules' => [
[
'allow' => true,
'matchCallback' => function ($rule, $action)
{
return !Yii::$app->user->isGuest &&
!Yii::$app->user->identity-
>isAdmin();
}
]
]
]
];
}
}

ArticleController.php
<?php

namespace app\modules\user\controllers;

use app\models\Article;
use app\models\ArticleSearch;
use app\models\ImageUpload;
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\UploadedFile;

/**
* ArticleController implements the CRUD actions for Article model.
*/
class ArticleController extends Controller
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
]
);

111
}

/**
* Lists all Article models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new ArticleSearch();

// $dataProvider = $searchModel->search($this->request-
>queryParams);
$params['ArticleSearch']['user_id'] = Yii::$app->user->id;
$dataProvider = $searchModel->search($params);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Article model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
$this->checkUserAccess($id);

return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Article model.
* If creation is successful, the browser will be redirected to
the 'view' page.
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new Article();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>save()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
112
} else {
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

/**
* Updates an existing Article model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$this->checkUserAccess($id);

$model = $this->findModel($id);

if ($this->request->isPost && $model->load($this->request-


>post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}

return $this->render('update', [
'model' => $model,
]);
}

/**
* Set image for article
*/
public function actionSetImage($id)
{
$model = new ImageUpload();

if (Yii::$app->request->isPost) {

$article = $this->findModel($id);

$file = UploadedFile::getInstance($model, 'image');


if ($article->saveImage($model->uploadFile($file,
$article->image))) {
return $this->redirect(['view', 'id' => $article-
>id]);
}
}

return $this->render('image', ['model' => $model]);


113
}

/**
* Deletes an existing Article model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->checkUserAccess($id);

$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Article model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return Article the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Article::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}

/**
* check if user has access
*/
public function checkUserAccess($id)
{
$model1 = $this->findModel($id);
if ($model1->user_id != Yii::$app->user->id) {
throw new \yii\web\NotFoundHttpException();
}
return true;
}
}

UserController.php
<?php

114
namespace app\modules\user\controllers;

use app\models\ImageUpload;
use app\models\User;
use app\models\UserSearch;
use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\web\UploadedFile;
use yii\filters\VerbFilter;

/**
* UserController implements the CRUD actions for User model.
*/
class UserController extends Controller
{
/**
* @inheritDoc
*/
public function behaviors()
{
return array_merge(
parent::behaviors(),
[
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
]
);
}

/**
* Lists all User models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new UserSearch();

// $dataProvider = $searchModel->search($this->request-
>queryParams);
$params['UserSearch']['id'] = Yii::$app->user->id;
$dataProvider = $searchModel->search($params);

return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
115
}

/**
* Displays a single User model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
$this->checkUserAccess($id);

return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new User model.
* If creation is successful, the browser will be redirected to
the 'view' page.
* @return string|\yii\web\Response
*/
public function actionCreate()
{
$model = new User();

if ($this->request->isPost) {
if ($model->load($this->request->post()) && $model-
>create()) {
return $this->redirect(['view', 'id' => $model-
>id]);
}
} else {
$model->loadDefaultValues();
}

return $this->render('create', [
'model' => $model,
]);
}

/**
* Updates an existing User model.
* If update is successful, the browser will be redirected to
the 'view' page.
* @param int $id ID
* @return string|\yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$this->checkUserAccess($id);
116
$model = $this->findModel($id);

if ($this->request->isPost && $model->load($this->request-


>post()) && $model->myUpdate()) {
return $this->redirect(['view', 'id' => $model->id]);
}

return $this->render('update', [
'model' => $model,
]);
}

/**
* Update user image
*/
public function actionSetImage($id)
{
$modelUser = new ImageUpload;
if (Yii::$app->request->isPost) {
$user = $this->findModel($id);
$file = UploadedFile::getInstance($modelUser, 'image');
if ($user->saveImage($modelUser->uploadFile($file,
$user->image))) {
return $this->redirect(['view', 'id' => $user->id]);
}
}
return $this->render('image', ['model' => $modelUser]);
}

/**
* Deletes an existing User model.
* If deletion is successful, the browser will be redirected to
the 'index' page.
* @param int $id ID
* @return \yii\web\Response
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->checkUserAccess($id);

$this->findModel($id)->delete();

return $this->redirect(['/site/index']);
}

/**
* Finds the User model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be
thrown.
* @param int $id ID
* @return User the loaded model
117
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = User::findOne(['id' => $id])) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not


exist.');
}

public function checkUserAccess($id)


{
$model1 = $this->findModel($id);
if ($model1->id != Yii::$app->user->id) {
throw new \yii\web\NotFoundHttpException();
}
return true;
}
}

views/default/index.php
<?php

use yii\helpers\Url;
?>

<div class="module-default-index">
<h1>Вітаємо на панелі управління!</h1>
<p>Даний модуль дозволяє вам:</p>

<div class="features">
<div class="feature">
<p>Управляти своїм обліковим записом</p>
<a href="<?= Url::toRoute('/user/user/') ?>">
Управління обліковим записом
</a>
</div>

<div class="feature">
<p>Переглядати та редагувати власні статті</p>
<a href="<?= Url::toRoute('/user/article/') ?>">
Перегляд та редагування статей
</a>
</div>
</div>

</div>

118
ДОДАТОК Б

Дамп бази даних

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";


START TRANSACTION;
SET time_zone = "+00:00";

-
-- База даних: `yii_blog`
--

-- --------------------------------------------------------

--
-- Структура таблиці `article`
--

CREATE TABLE `article` (


`id` int NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`date` date NOT NULL,
`image` varchar(255) DEFAULT NULL,
`tag` varchar(255) DEFAULT NULL,
`viewed` int NOT NULL DEFAULT '0',
`topic_id` int NOT NULL,
`user_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

--
-- Дамп даних таблиці `article`
--

INSERT INTO `article` (`id`, `title`, `description`, `date`,


`image`, `tag`, `viewed`, `topic_id`, `user_id`) VALUES
(8, 'Ранок здоров\'я: як приготувати смачний та корисний сніданок',
'Здоровий сніданок - це ключ до вдалого початку дня. Він не лише
надає організму необхідну енергію, але й створює основу для
правильного харчування протягом усього дня. Ось кілька порад, які
допоможуть вам приготувати смачний та корисний сніданок, який
зарядить вас позитивною енергією:\r\n\r\n1. Вживайте зернові
культури:\r\nПочніть свій день з високоволокнистих зерен, таких як
овес або гречка. Вони надають довготривалу енергію та сприяють
насиченню.\r\n\r\n2. Додайте фрукти:\r\nПідсиліть смак і корисність
свого сніданку, додаючи свіжі фрукти. Банани, ягоди чи кілька
шматочків манго чудово поєднуються з гранолою чи йогуртом.\r\n\r\n3.
Включіть білок:\r\nЗбалансуйте ваш сніданок, додаючи білкові
складники. Яєчні білки, грецький йогурт чи лляна насіння — це
119
відмінний вибір для забезпечення організму необхідними білками.\r\n\
r\n4. Пийте чай чи каву:\r\nВиберіть натуральний чай або каву без
додавання великої кількості цукру. Це не лише розслабляє, але й
надає антиоксидантів.\r\n\r\n5. Спробуйте нові рецепти:\r\
nЕкспериментуйте з різноманітністю рецептів. Наприклад, смачний
омлет з овочами або бататні панкейки можуть стати приємним сюрпризом
для вашого сніданку.\r\n\r\nНе забувайте, що важливо не лише те, що
ви їсте, але і як ви це робите. Розслабтеся, насолоджуйтеся моментом
та налаштуйте себе на позитивний день!', '2023-11-01',
'3ed7014cde870fad0f962ee8fe938b7f.jpg', 'Сніданки', 3, 1, 3),
(9, 'Коктейлі для здорового сніданку: смачно та корисно',
'Розпочніть свій ранок правильно, насолоджуючись смачними та
енергетичними коктейлями, які видають вам натхнення для активного
дня. Ось декілька неперевершених рецептів для коктейлів, які
прокинуть вас і додадуть заряду енергії:\r\n\r\n1. \"Зелений
Енергетичний Вибух\"\r\nІнгредієнти:\r\nСпанація\r\nБанан\r\
nАпельсиновий сік\r\nЛляне насіння\r\nОпис:\r\nЗбалансуйте своє тіло
вітамінами та мінералами за допомогою цього свіжого зеленого
коктейлю.\r\n2. \"Ягідне Завороження\"\r\nІнгредієнти:\r\nЗаморожені
ягоди (суміш малини, синьої ягоди, полуниці)\r\nГрецький йогурт\r\
nМед\r\nМ\'ятний лист\r\nОпис:\r\nСолодкий та освіжаючий коктейль з
додаванням грецького йогурту для більшого білкового ефекту.\r\
n3. \"Будь-який Фруктовий Рай\"\r\nІнгредієнти:\r\nАнанас\r\nМанго\
r\nБілий чай\r\nБанан\r\nОпис:\r\nСтворіть свій фруктовий рай з
екзотичними смаками та ароматами цього коктейлю.\r\n4. \"Шоколадний
Буст\"\r\nІнгредієнти:\r\nБанан\r\nКакао порошок\r\nМед\r\nМигдальне
молоко\r\nОпис:\r\nДодайте смак шоколаду до свого ранкового
коктейлю, отримуючи важливі поживні речовини.\r\nЦі коктейлі — це не
лише смачний спосіб розпочати день, а й чудовий спосіб подарувати
вашому організму необхідний заряд життєвої енергії та вітамінів.
Вони також можуть слугувати чудовим доповненням до вашого здорового
сніданку.', '2023-11-03', '94c6914b75bd519946671b7bd3bfc3f8.jpg',
'Сніданки коктейлі', 6, 1, 3),
(10, 'Сніданок за 5 хвилин: швидкі та здорові ідеї', 'Ранковий
поспіх не є виправданням для знехтування здоровим сніданком. Якщо ви
шукаєте швидкі та здорові ідеї для ранкового приготування, то ця
стаття для вас. Вам не потрібно витрачати години на кулінарію, щоб
насолоджуватися смачним та поживним сніданком.\r\n\r\nОднією з
найшвидших ідей є готовність суперфудів, таких як хрустка гранола та
свіжі ягоди. Просто заллєте гранолу в миску, додасте улюблені ягоди,
влити трошки меду та додати грецький йогурт. Цей сніданок не тільки
швидкий, але і надасть вам енергії на цілий день.\r\n\r\nЩе однією
варіацією є омлет з овочами. Просто розбийте яйця, додайте нарізані
томати, шпинат та кубики фети, і все це обсмажте на сковороді. Ви
отримаєте ситний та смачний сніданок за кілька хвилин.\r\n\r\nДля
тих, хто любить солодке, фруктовий салат — це чудовий вибір. Просто
нарізте свої улюблені фрукти, додайте трошки м\'яти та полийте
натуральним медом. Це освіжаюче та швидке рішення для тих, хто хоче
солодкувати без зайвого часу на приготування.\r\n\r\nНехай ваш ранок
буде ефективним і ситним, завдяки цим швидким та здоровим ідеям для
сніданку.', '2023-11-23', '25a26d80b94607b4e63f3a3921340d1a.jpg',
'Швидкі сніданки', 4, 1, 2),

120
(11, 'Овочеві сніданки: здорові та поживні', 'Здорові сніданки
можуть бути не лише смачними, але і насиченими вітамінами та
мінералами, особливо якщо ви включаєте овочі у своє ранкове меню.
Овочі додають свіжості та смаку, а також роблять ваш сніданок більш
ситним та корисним.\r\n\r\nОднією з простих та смачних ідей є омлет
з овочами. Розбийте яйця, додайте нарізані помідори, шпинат та тонко
нарізані паперчіки. Після цього приготуйте на сковороді, і ви
отримаєте насичений омлет, який наповнить вас енергією та
вітамінами.\r\n\r\nЩе однією ідеєю є овочевий тост. Використовуйте
цільнозерновий хліб та обсмажте його на грилі. Покладіть на тост шар
гуакамоле або нарізаного авокадо, та додайте свіжі томати та огірки.
Цей сніданок не лише смачний, але і повний корисних речовин.\r\n\r\
nДля тих, хто віддає перевагу холодним сніданкам, овочевий смузі -
чудовий вибір. Змішайте шпинат, огірок, ягоди та банан у блендері з
невеликою кількістю грецького йогурту. Ви отримаєте смачний та
освіжаючий напій, який надасть вам енергії на весь день.\r\n\r\
nНехай ваш ранок буде насиченим та здоровим завдяки цим овочевим
сніданковим ідеям.', '2023-10-10',
'a76f38e51ce706d06bfae24d2cc6dc8a.jpg', 'Овочі сніданки', 3, 1, 3),
(12, 'Кавові інновації: смачні сніданки з кавою', 'Ранкова кава може
бути не лише засобом прокинутися, але й частиною смачного та
насиченого сніданку. Сполучивши смак кави з іншими інгредієнтами, ви
можете створити справжні інновації у своїй ранковій кулінарії.\r\n\
r\nОднією з цікавих ідей є кавовий смузі. Змішайте свіжозаварену
каву з бананом, мигдальним маслом та льодом у блендері. Результат -
освіжаючий напій з чарівним поєднанням кавового аромату та солодкого
смаку банана.\r\n\r\nКавові вівсяні котлети - ще одна інноваційна
ідея для сніданку. Додайте свіжозаварену каву до готових овсяних
відерець, покладіть на верх начинку з ягід та горіхів. Це не лише
ситно, але й додасть вашому сніданку особливий кавовий шарм.\r\n\r\
nДля тих, хто любить випічку, кавовий банановий хліб - ідеальний
вибір. Додайте до тіста свіжозавареної кави та кубики вареного
банана. Після випічки ви отримаєте ароматний та смачний хліб, який
відмінно поєднується з кавою.\r\n\r\nНехай ваші ранкові сніданки
стануть справжніми кавовими інноваціями, даруючи вам насолоду та
енергію на весь день.', '2023-11-05',
'39a6c80eff767e06b13a9dfdfb5daca0.jpg', 'Сніданки кава', 21, 1, 3),
(13, 'Ідеї сніданків для активного ранку', 'Розпочати день з енергії
та життєвого натхнення - ось що дозволить ця колекція ідей для
сніданків. Відповідно до вашого стилю життя та вподобань, ці
сніданки створені для тих, хто бажає активного та продуктивного
ранку.\r\n\r\n\"Суперфуд сенсація\"\r\nІнтегруйте суперфуди у свій
сніданок, отримуючи всі необхідні вітаміни та мінерали для підтримки
енергії та здоров\'я.\r\n\r\n\"Страви для спортсменів\"\r\nДля тих,
хто займається фізичною активністю, ця тема пропонує сніданки, які
нададуть вам силу та витривалість на тренування та весь день.\r\n\r\
n\"Ароматні ранкові ритуали\"\r\nЗбалансуйте смакові відчуття своєї
чашки чаю з смачним та здоровим сніданком, створюючи ароматні
комбінації.\r\n\r\nЦі ідеї дозволяють вам насолоджуватися смачними
сніданками, що заряджають вас позитивною енергією та готують до
активного ранку.', '2023-11-18',
'68c2775703e3a4db42e50912026f066d.jpg', 'Сніданки', 1, 1, 3),

121
(14, 'Сніданки для смаку та здоров\'я', 'Ця тема спрямована на тих,
хто цінує гастрономічний насолодження, але при цьому прагне
зберігати баланс та здоров\'я у своєму харчуванні. Ви знайдете
сніданкові ідеї, які поєднують у собі вишуканий смак і корисність.\
r\n\r\n\"Колорит смаку: екзотичні сніданки для палати\"\r\nРозширте
свій кулінарний світ за допомогою страв з різних кухонь світу,
додаючи екзотичні інгредієнти до свого ранкового меню.\r\n\r\
n\"Трошки солодкого, трошки солоного: комбіновані сніданки\"\r\
nДодайте різноманіття до свого ранкового харчування, комбінувавши
солодкі та солоні компоненти у ваших сніданкових стравах.\r\n\r\
n\"Сніданок-подорож: смакові враження з різних куточків світу\"\r\
nВирушайте у кулінарну подорож, спробовуючи сніданки, що
інспіруються традиціями різних культур.\r\n\r\nЦя тема розкриє перед
вами світ гастрономічних можливостей, даруючи якісні смакові
враження та стимулюючи здоровий підхід до харчування.', '2023-12-
03', '025f78771eb97dc8537b48067e936212.jpg', 'Сніданки', 2, 1, 2),
(15, '5 ключових принципів збалансованої дієти', 'Збалансована дієта
визначається раціоном, який забезпечує всі необхідні поживні
речовини для нормального функціонування організму. При виборі
продуктів харчування та готуванні страв важливо дотримуватися певних
ключових принципів, щоб забезпечити необхідний рівновагу.\r\n\r\
nОдин з ключових принципів - різноманітність. Важливо включати до
раціону різні типи продуктів, щоб отримати широкий спектр поживних
речовин. Кожна група харчових продуктів має свої унікальні корисні
речовини, які важливі для здоров\'я.\r\n\r\nЩе одним ключовим
аспектом є баланс між білками, жирами та вуглеводами. Правильне
співвідношення цих складових допомагає забезпечити ефективне
функціонування організму. Наприклад, збільшений вміст білків
корисний для м\'язів, але важливо не забувати про рослинні джерела
цих елементів.\r\n\r\nНе менш важливим принципом є вибір цільових
продуктів. Важливо враховувати вміст корисних речовин та вибирати ті
продукти, які допоможуть досягти вашого конкретного харчового
плану.\r\n\r\nКонтроль розмірів порцій - ще один аспект. Великі
порції можуть привести до перевищення калорій, тоді як малі можуть
не забезпечити достатньої кількості поживних речовин. Тому важливо
слідкувати за розмірами порцій та слухати власний організм.\r\n\r\
nНе останнім, але дуже важливим принципом є гідна питома кількість
води. Правильне забезпечення організму вологою допомагає
підтримувати ефективне травлення, регулювати температуру та виводити
токсини.\r\n\r\nЦі принципи допоможуть вам створити збалансований
підхід до харчування та забезпечити вашому організму необхідні
ресурси для активного та здорового життя.', '2023-10-10',
'3beabd71dad5cd43e3af2abb2d9b79a6.png', 'Збалансована дієта', 2, 2,
2),
(16, 'Білки, вуглеводи, жири: як правильно балансувати елементи
живлення', 'Ваше здоров\'я і енергія залежать від правильного
балансу білків, вуглеводів та жирів в вашому харчуванні. Білки - це
будівельні блоки для тіла, які важливі для росту та відновлення
тканин. Вуглеводи - основне джерело енергії, яке живить ваш мозок і
м\'язи. Жири - необхідні для підтримки здоров\'я шкіри та волосся, а
також для правильного функціонування органів.\r\n\r\nВажливо
розуміти, що кожен з цих елементів виконує унікальні функції, і їх
правильне співвідношення забезпечує гармонійне функціонування
122
організму. Більше не завжди означає краще, тому важливо
дотримуватися збалансованого підходу.\r\n\r\nБілки мають різні
джерела, включаючи м\'ясо, рибу, яйця, молоко та рослинні продукти,
такі як боби та горох. Вуглеводи можна отримати зі злаків, фруктів
та овочів, а також зі складників, які містять велику кількість
волокон. Жири також повинні бути різноманітними, включаючи
поліненасичені жири з риби та оріхів, а також ненасичені жири з
рослинних олій та авокадо.\r\n\r\nПам\'ятайте, що ваші потреби в цих
елементах можуть змінюватися залежно від стилю життя, віку та
фізичної активності. Слухайте свій організм і дбайте про те, щоб
забезпечити йому всі необхідні складові для здоров\'я та життєвої
сили.', '2023-12-08', '5734b5d4b1a6e1c20fccc6bf69e84880.jpg',
'Збалансована дієта', 20, 2, 3);

-- --------------------------------------------------------

--
-- Структура таблиці `comment`
--

CREATE TABLE `comment` (


`id` int NOT NULL,
`text` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`user_id` int DEFAULT NULL,
`comment_id` int DEFAULT NULL,
`article_id` int NOT NULL,
`date` date NOT NULL,
`delete` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

--
-- Дамп даних таблиці `comment`
--

INSERT INTO `comment` (`id`, `text`, `user_id`, `comment_id`,


`article_id`, `date`, `delete`) VALUES
(1, 'Чудовий рецепт!', 2, NULL, 8, '2023-11-02', NULL),
(2, 'Треба буде спробувати.', 2, NULL, 12, '2023-11-07', NULL),
(3, 'Вже спробував - чудовий рецепт.', 3, 2, 12, '2023-11-08',
NULL);

-- --------------------------------------------------------

--
-- Структура таблиці `migration`
--

CREATE TABLE `migration` (


`version` varchar(180) NOT NULL,
`apply_time` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

123
--
-- Дамп даних таблиці `migration`
--

INSERT INTO `migration` (`version`, `apply_time`) VALUES


('m000000_000000_base', 1704572431),
('m240106_201024_create_topic_table', 1704572437),
('m240106_201056_create_user_table', 1704572438),
('m240106_201103_create_article_table', 1704572442),
('m240106_201117_create_comment_table', 1704572458);

-- --------------------------------------------------------

--
-- Структура таблиці `topic`
--

CREATE TABLE `topic` (


`id` int NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

--
-- Дамп даних таблиці `topic`
--

INSERT INTO `topic` (`id`, `name`) VALUES


(1, 'Здорові сніданки'),
(2, 'Збалансована дієта');

-- --------------------------------------------------------

--
-- Структура таблиці `user`
--

CREATE TABLE `user` (


`id` int NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`login` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_0900_ai_ci NOT NULL,
`image` varchar(255) DEFAULT NULL,
`is_admin` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

--
-- Дамп даних таблиці `user`
--

124
INSERT INTO `user` (`id`, `name`, `login`, `password`, `image`,
`is_admin`) VALUES
(1, 'admin', 'admin@gmail.com',
'$2y$13$7DzIXWRWK4etK9MyHBYXteB554YpIqvUWWpJqCVEwtX7.K8pvsmFG',
'd00a5c831d5a8cecef4c7f732e5c5a4b.png', 1),
(2, 'user1', 'user1@gmail.com',
'$2y$13$1Tkqj9qW05i71u/couVgqOCn/7ry2BjkPGoDt22L./zv.6AZ46Ho2',
'84cc959d2024be2f2878e4049c12f1d9.png', 0),
(3, 'user2', 'user2@gmail.com',
'$2y$13$N090AywE0XtOjaP5OU24..bFRd/7Sxt1U.85uLtjCFD8yaYY/9q2m',
'3704c8730e00223c6e24dd3efe5888ff.png', 0),
(17, 'test', 'test@gmail.com',
'$2y$13$3DoI/EOLl1r9p3jK0iHhH.3MFIbGo1ZAku1omXlT32R0CphM/Lm5G',
NULL, 0);

--
-- Індекси збережених таблиць
--

--
-- Індекси таблиці `article`
--
ALTER TABLE `article`
ADD PRIMARY KEY (`id`),
ADD KEY `idx-topic_id` (`topic_id`),
ADD KEY `idx-post-user_id` (`user_id`);

--
-- Індекси таблиці `comment`
--
ALTER TABLE `comment`
ADD PRIMARY KEY (`id`),
ADD KEY `idx-post-user_id` (`user_id`),
ADD KEY `idx-article_id` (`article_id`),
ADD KEY `idx-comment_id` (`comment_id`);

--
-- Індекси таблиці `migration`
--
ALTER TABLE `migration`
ADD PRIMARY KEY (`version`);

--
-- Індекси таблиці `topic`
--
ALTER TABLE `topic`
ADD PRIMARY KEY (`id`);

--
-- Індекси таблиці `user`
--
ALTER TABLE `user`
ADD PRIMARY KEY (`id`);
125
--
-- AUTO_INCREMENT для збережених таблиць
--

--
-- AUTO_INCREMENT для таблиці `article`
--
ALTER TABLE `article`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=30;

--
-- AUTO_INCREMENT для таблиці `comment`
--
ALTER TABLE `comment`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=14;

--
-- AUTO_INCREMENT для таблиці `topic`
--
ALTER TABLE `topic`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;

--
-- AUTO_INCREMENT для таблиці `user`
--
ALTER TABLE `user`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=23;

--
-- Обмеження зовнішнього ключа збережених таблиць
--

--
-- Обмеження зовнішнього ключа таблиці `article`
--
ALTER TABLE `article`
ADD CONSTRAINT `fk-post-user_id` FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk-topic_id` FOREIGN KEY (`topic_id`) REFERENCES
`topic` (`id`) ON DELETE CASCADE;

--
-- Обмеження зовнішнього ключа таблиці `comment`
--
ALTER TABLE `comment`
ADD CONSTRAINT `fk-article_id` FOREIGN KEY (`article_id`)
REFERENCES `article` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk-comment-post-user_id` FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk-comment_id` FOREIGN KEY (`comment_id`)
REFERENCES `comment` (`id`) ON DELETE CASCADE;
COMMIT;
126
127

You might also like