Этот урок набрал набрал достаточно большое количество комментариев и дальнейшее его комментирование отключено. Если вы хотели убедиться в правильности выполнения ДЗ или у вас возник вопрос по уроку, посмотрите ранее добавленные комментарии, кликнув по кнопке ниже. Скорее всего вы найдете там то, что искали. Если это не помогло - задайте вопрос в чате в телеграме - https://t.me/php_zone
prognoz 31.08.2019 в 15:18

У меня вопрос, как сайт с подобной архитектурой загрузить на сервер провайдера? Допустим, тот же beget. Ничего не получается.
Там все грузится в папку public_html.
Вопрос, как переадресовать запрос в папку www.
Все что пишу в public_html/.htaccess не работает.
Варианты такие:
DirectoryIndex /web/index.php
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.+) $1 [L]
RewriteCond %{DOCUMENT_ROOT}/web%{REQUEST_URI} -f
RewriteRule ^(.+) /web/$1 [L]

Не работает. Что делать?

ivashkevich 31.08.2019 в 21:03

Просто переименовать www в public_html. Вся остальная структура не изменится.

prognoz 01.09.2019 в 00:10

Если на OpenServer поменять название папки www на public_html, то все работает.

На хостинге ничего не работает.
Структура там такая prognoex.beget.tech и в нем папка public_html
Грузим наш проект в папку prognoex.beget.tech
Наша структура папок:
src
templates
public_html (бывший www)

В итоге ошибка 500.

Служба поддержки пишет следующее:
Здравствуйте!
Насколько я вижу, проблема возникает из-за некорректной работы php-скрипта. Прикладываю фрагмент лога ошибок:
[31-Aug-2019 23:05:33 Europe/Moscow] PHP Notice: Undefined index: route in /home/p/prognoex/prognoex.beget.tech/public_html/index.php on line 11

Что не так с путями? Подскажите, пожалуйста.

ivashkevich 01.09.2019 в 07:11

.htaccess загрузил хоть?) Если проблема не в этом, напиши в личку в ВК или в телеге.

V0yager 09.10.2019 в 15:42

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

Роут для страницы добавления коммента: /articles/1/comments/add
Контроллер: ArticlesController
Экшн: addComment

Просто получается так, что в случае возникновения ошибки, надо открывать страницу со статьёй, но добавляя туда переменную с этой ошибкой.
Была мысль записать в массив свойств view ($this->view->setVar('error', $excep->getMessage())), и уже при рендеринге страницы со статьёй вывести ошибку, но мне показалось, что это бредовато.

ivashkevich 09.10.2019 в 19:32

Да нет, вполне рабочее решение. В реальных проектах запрос отправляется через js на отдельный роут и если возвращается ошибка, то всплывает окошечко с ошибкой. Но здесь в учебном примере будет нормально так как ты предложил.

zeexo 01.12.2019 в 13:55

Артём, здравствуйте!
Подскажите, пожалуйста, касаемо архитектуры.
Есть UsersController(и модель его, как мы делали в уроках ранее).
Возник вопрос. Допустим есть различные таблицы с логами.
Как мне правильно вывести их в "Профиле"?
Создать метод profile в классе UsersController (Controllers/UsersController.php), затем в Models создать файлы с классами, которые наследуются от ActiveRecord и в методе profile класса UsersController сделать так(?):

Controllers/UsersController.php

public function profile()
{
    $user = $this->user;
    if($user === null) {
        header('Location: /');
    } else {
        $log = $user->getLogs($user);
        $log2 = $user->getLogs2($user);
        $log3 = $user->getLogs3($user);
        $this->view->renderHtml('user/profile.php', [
            'log' => $log,
            'log2' => $log2,
            'log3' => $log3
        ]);
    }
}

Models/Users/User.php

public static function getLogs($user)
{
    return \Application\Models\Logs\Log1::getById($user->id);
}

public static function getLogs2($user)
{
    return \Application\Models\Logs\Log2::getById($user->id);
}

public static function getLogs3($user)
{
    return \Application\Models\Logs\Log3::getById($user->id);
}

Models/Logs/Log1.php

namespace Application\Models\Logs;
use Application\Models\ActiveRecordEntity;

class Log1 extends ActiveRecordEntity
{
    protected static function getTableName(): string
    {
        return 'log1';
    }
}

На сколько правильный такой способ реализации? Или лучше иначе, и как?

ivashkevich 01.12.2019 в 17:02

Мне непонятно что это за логи, и почему их нужно выводить в профиле. Опишите чуть подробнее, пожалуйста.

zeexo 01.12.2019 в 17:56

Есть таблица users(пользователи).
Допустим, есть таблицы логов:
таблица log_users (id, user_id, action) - логи действий юзера
таблица calls_log (id, user_id, call_id, call_description) - логи звонков
После авторизации пользователя по логину и паролю, делаю переход на страницу профиля юзера.
Но сам профиль в роутах я сделал так:

'~^profile$~' => [\MyApp\Controllers\UsersController::class, 'profile']

То есть создал метод profile в UsersController.
И на странице профиля пользователя мне нужно вывести ДВЕ таблицы(html) с логами:
Лог действий пользователя(авторизовавшегося):

SELECT * FROM log_users WHERE user_id = 'id_аккаунта_из_users'

Лог звонков(авторизовавшегося):

SELECT * FROM calls_log WHERE user_id = 'id_аккаунта_из_users'

И два вопроса:

  1. Правильно ли делать "профиль" пользователя через UsersController? Или правильней ProfileController создавать?
  2. Можно ли вывести html таблицы на странице "профиля" создав модели(в директории MyApp\Models\Logs) для каждой из вышеуказанных таблиц логов(log_users и calls_log) и в контроллере UsersController(или ProfileController?) в методе profile получать объекты с данными логов авторизовавшегося юзера способом, как я описывал в предыдущем сообщении?
    А если нет, то как правильно?
ivashkevich 01.12.2019 в 21:14

Понятно, спасибо за детали.

  1. Да можно и так и так, смотря что есть профиль. Если пользователь, по сути, это и есть профиль, то можно в этом же контроллере. Если же сущность пользователя это вообще другое, то лучше отдельный контроллер.
  2. Да, вполне нормальное решение)
zeexo 01.12.2019 в 23:01

Спасибо, понял!
Подскажите, пожалуйста, насчёт ещё пары моментов:

  1. Если у меня в модели Models/Users/User.php нужно сделать дополнительную функцию, например я получил геттером поле со временем последней авторизации юзера в виде timestamp, и мне нужно конвертировать это в просто понятную для человека дату, допустимо ли добавить функцию конвертации сюда же, в модель User наследуемую от ActiveRecordEntity?
  2. Если не только в контроллере UsersController, но и в других контроллерах я хочу сделать методы, к которым клиент будет обращаться POST запросом через ajax, как лучше сделать проверку на это с точки зрения архитектуры, чтобы не делать в каждом нужном методе проверку if($_POST && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { ... }, а вынести это в отдельную функцию? Куда, в какую модель выносить подобного рода функции?
ivashkevich 05.12.2019 в 14:41
  1. Да. Хоть прямо в геттере/сеттере выполняйте приведение.
  2. В роутниге по-хорошему разруливать тип запроса и в зависимости от типа вызывать разные экшены. То есть помимо просто роута в конфиге появляется еще тип запроса.
andreskrip 19.02.2020 в 18:07

Подскажите, пожалуйста, верной ли я дорогой вообще иду:
1) т.к. мы создаем для комментариев отдельную таблицу, то согласно ORM необходимо создать класс(модель) Comment. Я создал его в неймспейсе MyProject\Models\Articles (я посчитал, что комментарии — сущность, зависимая от статьи, и положил туда, но возможно надо было отдельную директорию создавать)?
2) Для вывода одиночной статьи, а также вывода комментариев к ней используется один и тот же роут, с одним и тем же экшеном (ArticlesController->view()), соответственно вся обработка данных от пользователя и передача в модель будет находиться тут? Если да, то каким образом передавать новые переменные во View (есть мысль использовать setVar, но в абстрактном контроллере он занимается передачей пользователя)

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

ivashkevich 19.02.2020 в 19:25
  1. Правильнее будет отдельный неймспейс завести
  2. Что мешает просто передать эти новые переменные в рендерер?
andreskrip 19.02.2020 в 20:48

Ну, например в этом коде, если будет поймана ошибка, то вылезет фатал эррор, т.к. не может подгрузиться сама статья и уже имеющиеся комменты (содержимое метода view). И получается я в этом методе должен их дублировать и передавать переменные в рендерер? Мне кажется я что-то делаю не так

public function view(int $articleId): void
    {
        $article = Article::getById($articleId);

        if ($article === null) {
            throw new NotFoundException();
        }
        //вывод всех комментариев относящихся к определенной статье
        $comments = Comment::getCommentsByArticleId($articleId);

        $this->view->renderHtml('articles/view.php', ['article' => $article, 'comments' => $comments]);
    }

public function addComment(): void
    {
        if (!empty($_POST)) {
            try {
                $comment = Comment::add($_POST, $this->user);
            } catch (InvalidArgumentException $e) {
                $this->view->renderHtml('articles/view.php', ['error' => $e->getMessage()]);
                return;
            }
            header('Location: /articles/' . $_POST['articleId'] . '#comment' . $comment->getId(), true, 302);
            exit();
        }
    }
ivashkevich 20.02.2020 в 04:27

Не понял про какой fatal error вы говорите, но по коду вообще все отлично

andreskrip 20.02.2020 в 12:41

когда в методе addComment ловится ошибка, то рендерится статья, но из-за отсутствия $article и $comments в переданных переменных (которые прописаны в методе view) выходит фатал еррор. Можно, конечно продублировать код метода view сюда, но мне кажется это плохое решение
ссылка на скрин

P.S. решил таким способом:

public function addComment(): void
    {
        if (!empty($_POST)) {
            try {
                $comment = Comment::add($_POST, $this->user);
            } catch (InvalidArgumentException $e) {
                $this->view->setVar('error', $e->getMessage());
                $this->view($_POST['articleId']);
                return;
            }
            header('Location: /articles/' . $_POST['articleId'] . '#comment' . $comment->getId(), true, 302);
            exit();
        }
    }

скрин
И эксепшн высвечивается, и комменты со статьей на месте. Единственное, не догадывался, что setVar не перезаписывает переменные, а добавляет к имеющимся (в сетваре же лежит информация о $user в абстрактном контроллере), но это уже прямой затык (взглянул бы сразу в класс View, то не было бы никаких догадок).

Такое решение проблемы допустимо?

ivashkevich 21.02.2020 в 06:28

Продублировать 2 строчки из view - самое простое и понятное решение. Это нормально, вам ведь нужно убедиться что статья существует, прежде чем добавлять к ней комментарий.

andreskrip 21.02.2020 в 13:29

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

ivashkevich 23.02.2020 в 19:23

Отлично. Для редактирования отдельный роутинг это правильное решение. В дальнейшем можно будет сделать это с помощью запроса в API.

OneMoreTime 26.03.2020 в 00:38

Думал сделать все, а потом вопросы позадавать, но их УЖЕ оказалось много, и скорее всего будут еще, пока только часть работы, движется очень медленно, некоторые вещи(методы) уже повылетали из головы для чего делали(нужно комментарии писать - однозначно), а за некоторые вообще забыл, что делали и напрасно заново мозговал как сделать, хотя уже были готовые методы, и при повторном разборе становится понятно что и для чего, но опять - дополнительное время...
1.
Много аргументов при рендеринге с выводом ошибки, в массиве параметров - и ошибка и статья и комменты. какая-то куча-мала получается. Это не очень нормальное явление? А по другому не получается, т.к. на одной страничке и статья и комментарии.

public function addComment(int $articleId):void
    {
        if (!empty($_POST)) {
            try {
                $comment = Comment::add($_POST, $this->user, $articleId);
            }catch(InvalidArgumentException $e){
                $this->view->renderHtml('articles/view.php',
                    [
                        'error' => $e->getMessage(),
                        'article' => Article::getById($articleId),
                        'comments' => Comment::getAllById($articleId)
                    ]
                );
                return;
            }
            header('Location: /articles/' .$articleId.'#comment'.$comment->getId(), true, 302);
            exit;
        }
        $this->view->renderHtml('articles/'.$articleId);
        return;
    }

2.
Корректно ли использование в разметке вложенных условий через двоеточия таким образом:? Ну и сюда же вопрос - корректно ли использование такое количество и формат(через геттеры и т.п.) переменных в разметке? Правильнее переносить логику проверок в контроллер/модель или в разметке тоже можно? Либо там нагромождение проверок получается, либо там.

<?php if (isset($user)):
            if ($user->isAdmin() || ($comment->getAuthor()->getNickname() === $user->getNickname())):?>
    <p><a href="/comments/<?=$comment->getId()?>/edit">Edit</a>
                <?php if (isset($user) && $user->isAdmin()):?>
        <a href="/comments/<?=$comment->getId()?>/delete">|Delete</a>
                <?php endif;?>
    </p>
    <?php endif;?>
    <?php endif;?>

3.
частично продолжение предыдущего вопроса:
В разметке проверки лучше делать более читаемыми, или можно и через тернарный оператор?:

<?= !empty($user) ? 'Hello, ' . $user->getNickname().' | <a href="/users/logout">Log out</a>' : '<a href="/users/login">Log in</a> | <a href="/users/register">Sign Up</a>' ?>

4.
Нужно ли делать неочевидные проверки, или они должны быть абсолютно на все случаи, даже не критические? Например - проверка на то, залогинен ли уже пользователь, когда нажимается на ссылку LOGIN? При этом, на уровне UI такая возможность исключена, т.е. у пользователя нигде такая ссылка не появится. Вопрос вылез из ситуации, появившейся ранее в курсе, когда при выводе ошибке были активны ссылки залогиниться/зарегистрироваться, при этом пользователь УЖЕ был залогинен. Я сделал чтобы ненужные ссылки пользователь не увидит, но без соответствующей проверки сама возможность залогинивания уже залогиненного пользователя существует.

5.
Сделал для статей и комментариев кнопочки редактировать/отменить. Как правильнее реализовать отмену? Кнопки я разместил в форме редактирования, отмену обрабатывал в контроллере, например таким образом для отмены редактирования статьи со странички редактирования возвращаемся на страничку с этой стаьей:

if ($_POST['cancel'] === 'Cancel'){
                header('Location: /articles/'.$articleId, true, 302);
                exit;
            }

Если бы это было организовано не кнопками в форме, а отдельно, или не кнопками, а ссылками, просто бы в разметке обернул бы в теги <a> и отправлял бы по нужному пути. Как это делать правильно - не знаю... Тут же вопрос - что для редактирования/удаления/отмены корректнее использовать - кнопки или ссылки? Или это чисто дизайнерские вопросы?

6.
В контроллере статей получилось общее месиво из методов - по две штуки редактирования/добавления и удаления - и для статей и для комментариев. Добавил модель дя комментариев. Может я не правильно архитектуру построил? По другому не придумал как можно сделать, чтобы в отдельных контроллерах обрабатывать стать и комментарии.

7.
Нужно везде(на странице с отдельной статьей + в общем списке статей) обеспечить возможности удалять/редактировать(с администраторскими правами) или только в админке? Или это опять таки чисто пользовательский/дизайнерский нюанс? Реализовать - не проблема - вопрос в том, нужно ли?

8.
Какой должен быть механизм действий после удаления комментария из открытого спика комментариев? Если комментариев много и удаляемый комментарий находится за пределами экрана, после его удаления выполняется редирект на ту же статью с комментариями . т.е. та же страничка просто полностью перезагружается но уже без удаленной статьи, и, чтобы вернуться к месту, где было удалено сообщение, нужно опять листать список за пределы экрана. Это неудобство можно решить только посредством множественного удаления, если нужно удалять больше одного коммента за раз?
Скрин блога №1
Скрин блога №2

ivashkevich 26.03.2020 в 02:51
  1. Три параметра - это не много, это вообще чуть-чуть. Ну а вообще, много их быть не может, это данные, необходимые для отрисовки шаблона. Без них никак.
  2. Корректно. Всё правильно говорите - либо там, либо там будет написано. Если разницы нет - решайте сами где будете писать. Если это общая часть шаблонов (хедер или футер), и проверка нужна для всех случаев одинаковая, логичнее будет ее сделать в шаблоне, чтобы не повторять каждый раз в каждом экшене каждого контроллера.
  3. Вариант с if выглядит более читаемым
  4. На ваше усмотрение) и сильно зависит от требований проекта. На пет-проджекте я бы не стал такого делать 100%, а вот если на работе - может быть, но сперва обсудил бы этот функционал с овнером продукта.
  5. Что то, что это. Разницы никакой нет, делайте как удобнее и проще. С отменой непонятно для чего делать редирект на бэке, кажется простой ссылки достаточно.
  6. Для комментариев стоит сделать отдельный контроллер
  7. Вопрос именно в том, нужно ли) а можно и там и там, каких-либо проблем нет.
  8. Либо удалять с помощью запроса в API, запрос отправлять с помощью JS.
OneMoreTime 29.03.2020 в 12:12

Спасибо за корректировки.)
Контроллер для комментариев переделал, сделал отдельно от статей. Для каждой сущности - отдельный.
Есть еще несколько вопросов:
1.

Админка.
В админку могут попасть только администраторы сайта. Это стоит проверять в контроллере самой админки. Кроме того, в шаблоне стоит проверять является ли текущий пользователь админом.

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

2.
Относительно архитектуры. Например:

'~^users/(\d+)/(articles)$~' 

По этому роуту получаем список статей для конкретного пользователя в админке. Где правильнее обработчик делать - в контроллере статей или в контроллере юзеров? Вроде как относится к конкретному пользователю, с другой стороны - речь в итоге о статьях. Я сделал в контроллере юзера, но терзают меня смутные сомнения...

3.
Сделал статус онлайн, оффлайн, но по его бэк реализации больше вопросов... Я сделал достаточно примитивно - добавил в таблицу юзера поле "время последнего посещения" и обновляю его в абстрактном контроллере - т.е. при каждом обращении на сервер. В том же абстрактном контроллере проверяю - если время последнего посещения было больше 2 минут назад - устанавливаю статус оффлайн. Видел реализации на сессиях, возможно так правильнее. А как правильно без фронтэнда?

4.
Тоже вопрос по реализации - отображаемое время(часовой пояс и переход на летнее время и обратно). Пересмотрел курс по БД, почитал много интернета, решения пока не нашел, возможно плохо искал. Если отображение времени можно менять просто включив/отключив соответствующую опцию на блоге/сайте/форуме у юзера в профиле, возможно, это какая-то инструкция/набор инструкций для БД, согласно которой/которым для конкретного юзера, время с БД будет восприниматься клиентом у этого юзера со смещением соответственным выбранному. А как это реализуется в реальности, хотя бы в двух словах?

5.
Наверное самый главный вопрос:

в рамках данной статьи вам будет дано задание самостоятельно доработать блог до конца.

А насколько "до конца" целесообразно на данном этапе обучения? Я потратил больше чем 24 часа, и кроме того, что вспоминал что УЖЕ сделали, но подзабыл и больше вник, изучил параллельно некоторые вещи, доделал в блоге много чего, но можно было бы доделать еще на порядок больше, и даже не нужно мучаться - придумывать что доделать, что займет еще много времени, дорабатывать можно месяцами)). Вопрос в том, что много еще чего не изучено, и я уверен, что определенные вещи я делаю за счет костылей, т.к. с некоторыми вещами(да хоть с теми же сессииями) еще на работал/не пробовал, а о существовании некоторых даже не подозреваю. Стоит ли сейчас продолжать доделывать различные многочисленные функции на блоге, или цель данного урока - освоиться в том материале, что был дан до этого момента, попробовать реализовывать какой-то дополнительный функционал в имеющейся архитектуре и научиться трансформировать/дорабатывать текущую архитектуру под расширенный функционал?

ivashkevich 29.03.2020 в 13:13
  1. Действительно, незачем, если сообщение об ошибке доступа будет в отдельном шаблоне.
  2. Работаем со статьями, значит в контроллере статей. Более правильно будет роут начать тоже со статей, вроде: /articles/by_user/123
  3. Всё было ок до "В том же абстрактном контроллере проверяю - если время последнего посещения было больше 2 минут назад - устанавливаю статус оффлайн."
    Это ещё зачем? Можно же в месте где нужен статус просто сравнивать две даты. Не нужно это в абстрактном контроллере.
  4. Ничего не понял, давай в личку напишешь по этому вопросу?
  5. Цель - придумать фичу какую-то и реализовать самостоятельно. Судя по твоим вопросам ты это успешно сделал)
OneMoreTime 29.03.2020 в 13:43

Судя по твоим вопросам ты это успешно сделал)

Сделал кроме основного меню админ-панель для быстрого доступа к функциям админки, профиль юзера с возможностью редактирования его админом. Из профиля можно посмотреть все статьи пользователя, все комментарии, поменять юзера на админа, админа на юзера, отменить активацию профиля/забанить, Пользователь может изменить данные о себе. Статьи и комменты можно сортировать по дате, алфавиту, номеру(возр/убыв). Отображение статуса юзера в профиле и возле комментариев, Посещаемость юзеров на блоге, вроде все. Все работает

Работаем со статьями, значит в контроллере статей. Более правильно будет роут начать тоже со статей, вроде: /articles/by_user/123

А что приоритетнее - сделать более компактный код, немного нарушив взаимосвязи в архитектуре, или соблюсти строгую иерархию?
Я сделал изначально все управление в контроллере юзера - и получение списка юзеров и статей конкретного юзера и комментов конкретного юзера в одном методе в юзерконтроллере, только разные аргументы передавал в метод.
Уже переделал, но все же кода стало больше - еще в двух классах добавилось по методу.

Всё было ок до "В том же абстрактном контроллере проверяю - если время последнего посещения было больше 2 минут назад - устанавливаю статус оффлайн."
Это ещё зачем? Можно же в месте где нужен статус просто сравнивать две даты. Не нужно это в абстрактном контроллере.

Я тут похоже сам запутался, когда объяснял)
Выглядит это так:
AbstractController.php

if($this->user !== null)$this->user->setLastVisitTime();

Models\Users\User.php

public function setLastVisitTime(): void 
        {
        $this->setLastVisit(date('Y-m-d H:i:s'));
        $this->save();
    }
public function isOnline(): bool
    {
        $time = date('Y-m-d H:i:s', time() - 60 * 2);
        return($time < $this->getLastVisit());
    }

В шаблоне, где нужен статус делаю проверку

<?php if $userById->isOnline()?>
...
ivashkevich 29.03.2020 в 20:18

За компактностью кода гнаться не стоит. В первую очередь - простота и логичность.

if($this->user !== null)$this->user->setLastVisitTime();

За отсутствие фигурных скобок в блоках if-else уже давно бьют по рукам. У нас есть PSR, их нужно соблюдать. В своих уроках я ни разу так не писал, где-то уже нахватались плохих примеров)

setLastVisitTime()

Нельзя делать метод с именем set и не делать у него аргументов, которые будут сеттиться. Тогда уж update... какой-нибудь

$time = date('Y-m-d H:i:s', time() - 60 * 2);

Что за $time? Текущее время? Двухминутной давности? Тогда $timeTwoMinutesAgo

return($time < $this->getLastVisit());
    }

После return ставится пробел. Для чего круглые скобки вдруг добавили?

В целом все это заменяется более элегантной конструкцией

return new DateTime('-2 minutes') < $this->getLastVisit();

Учитесь пользоваться DateTime.

<?php if $userById->isOnline()?>

После if всегда нужно писать круглые скобки. После условия обязательно или двоеточие или открывающая фигурная скобка. Здесь вообще нерабочий код.

OneMoreTime 29.03.2020 в 21:12

Скобки и круглы и фигурные- это все невнимательность, похоже уже вошло в плохую привычку... Принял во внимание.

В целом все это заменяется более элегантной конструкцией

Я чувствовал, что у меня как то коряво... Принял во внимание.

После if всегда нужно писать круглые скобки. После условия обязательно или двоеточие или открывающая фигурная скобка. Здесь вообще нерабочий код.

Да, это не копипаст рабочего кода.Тут просто показывал пример, с помощью чего в шаблоне статус получаю. В рабочем шаблоне записано так:

Status: <?= $userById->isOnline() ? '<span style="color:green">ON</span>' : '<span style="color:red">OFF</span>'?>

Не написал полностью, т.к. тут обычный примитив. Думал сделать спрайт, потом решил не тратить на красоту время. Кстати, где в проекте нужно размещать папку с картинками?
Спасибо за замечания. Исправляю. Конспектирую.

ivashkevich 30.03.2020 в 09:05

Пожалуйста) По поводу картинок - внутри корневой папки вебсервера, www (или public_html, смотря что у вас за система) создайте папку uploads и в нее сохраняйте.

OneMoreTime 30.03.2020 в 17:30

В целом все это заменяется более элегантной конструкцией

return new DateTime('-2 minutes') < $this->getLastVisit();

Учитесь пользоваться DateTime.

Почитал про класс DateTime, там и таймзоны и другого много всего интересного. Применил к своему коду, правда такой элегантности у меня не получилось, т.к. new DateTime возвращает объект, поэтому все равно пришлось использовать метод format. Фактически получается:

return (new \DateTime('- 2 minutes'))->format('Y-m-d H:i:s')< $this->getLastVisit();

против

return date('Y-m-d H:i:s', time() - 60 * 2) < $this->getLastVisit();

Второй вариант вроде более читаемый. Оправдано ли использование тут класса DateTime? Или я что-то перемудрил?

ivashkevich 30.03.2020 в 20:54

Эмм, а чего это вы 2 строки сравниваете, это незаконно. getLastVisit тоже должен возвращать DateTime, и их уже сравнивать между собой будет корректно.

OneMoreTime 30.03.2020 в 22:44

getLastVisit тоже должен возвращать DateTime

Я правильно понял, что нужно тогда и для установления времени последнего визита в БД использовать класс DateTime. т.е. каждый раз при обращении клиента к серверу создавать и ложить в БД новый объект в поле "время последнего обновления"? Если да, то сделать это не получается, т.к. на этапе выполнения подготовленного запроса в БД

$result = $sth->execute($params);

происходит фатальная ошибка . Текст ошибки - невозможно преобразовать объект в строку.

ivashkevich 31.03.2020 в 05:51

Храните в свойстве объекта строку, а при работе с геттером и сеттером выполняйте преобразование в DateTime и обратно. Модель в ActiveRecord сама должна знать о том, как сохранять значения в базу, вот и предоставьте ей эту возможность.

OneMoreTime 31.03.2020 в 14:37

У меня стойкоее ощущение, что я делаю какой-то конкретный костыль..
Не пойму, как можно хранить в свойстве lastVisit строку и делать чтобы getLastVisit() возвращал dateTime.

Я сделал так, с использованием класса DateTime, но не думаю, что это правильно, или как минимум хорошо:

/** @var string */
protected $lastVisit;

/**
 * @return string
 */
public function getLastVisit() 
{
    return $this->lastVisit;
}

/**
 * @param string $lastVisit
 */
public function setLastVisit($lastVisit) 
{
    $this->lastVisit = $lastVisit;
}

public function updateLastVisitTime(): void
{
    $this->setLastVisit((new \DateTime())->format('Y-m-d H:i:s'));
    $this->save();
}

public function isOnline(): bool
{
    return new \DateTime('- 2 minutes')< \DateTime::createFromFormat('Y-m-d H:i:s',$this->lastVisit);
}

Очевидно, я не понял идеи, как нужно сделать/переделать и самое главное - для чего. Все это для того, чтобы только сравнить текущее время с временем из БД в виде объектов? Зачем их нужно сравнивать именно как объекты?

ivashkevich 01.04.2020 в 04:36
public function getLastVisit(): \DateTime
{
    return new \DateTime($this->lastVisit);
}

public function setLastVisit(\DateTime $lastVisit): void
{
    $this->lastVisit = $lastVisit->format(DATE_W3C);
}
OneMoreTime 01.04.2020 в 14:43
format(DATE_W3C)

Я еще вчера пересмотрел все виды форматов, искал более красивую замену 'Y-m-d H:m:s' но не один не подошел, в том числе и DATE_W3C или ихеще как -то нужно преоразовывать. В этом формате не пишет в БД, т.к. там дата в другом формате. Вариант ниже работает:

format('Y-m-d H:m:s')

И опять таки - с типом данных, который возвращает getLastVisit(). Я не зря спрашивал. У меня в шаблоне на это свойство есть запросы, поэтому непонятно было, как сделать и чтобы в свойстве хранилась строка и чтобы геттер возвращал объект DateTime и при этом еще использовать геттер в шаблоне. Делать еще один геттер, только для того чтобы получать свойство в шаблоне - мне кажется не очень правильно, поэтому перенес логику преобразования формата даты прямо в метод проверки на соответствие времени, а в геттере получаю строку из свойства:

public function isOnline(): bool
    {
        return new \DateTime('- 2 minutes')< (new \DateTime($this->lastVisit));
    }

/**
     * @return string
     */
    public function getLastVisit()
    {
        return $this->lastVisit;
    }

Ну и таки остался вопрос: Зачем эти времена нужно сравнивать именно как объекты? Только в угоду ООП, чтобы везде где есть возможность использовать классы/объекты? Если это какие-то правила написания кода, то понятно откуда ноги растут, но получается, что - это наоборот усложняет код, как в этом случае, например.

ivashkevich 02.04.2020 в 08:05
  1. Да, сорян, для MySQL будет правильно format('Y-m-d H:m:s')

  2. В шаблоне нужно просто вызывать format у DateTime-объекта.

  3. Зачем эти времена нужно сравнивать именно как объекты?
    Потому что именно так и нужно сравнивать даты. Сравнивать строки некорректно.

  4. Усложнения здесь никакого нет.
    new DateTime('-2 minutes') < $this->getLastVisit();

    Это проще чем:

    new \DateTime('- 2 minutes')< (new \DateTime($this->lastVisit));
Dimitry 10.04.2020 в 05:08

У меня проблема возникла, я когда сделал класс для комментариев, сделал возможность их добавления, при редактировании статьи получил ошибку, пытался исправить, Xdebug показал, что в методе update, при вызове query: $db = Db::getInstance();
$db->query($sql, $params2values, static::class);
В самом методе query

ublic function query(string $sql, array $params = [], string $className = 'stdClass'): ?array
    {
        $sth = $this->pdo->prepare($sql);
        $result = $sth->execute($params);

        if (false === $result) {
            return null;
        }

$result почему-то равен 0
https://www.dropbox.com/sh/fl1un4gugpsjpty/AADqAIUGg8k84Jj3Xulyd2CXa?dl=0 Здесь весь код
Некоторые куски прикреплю здесь

//ArticlesController
<?php

namespace MyProject\Controllers;

use MyProject\Exceptions\NotFoundException;
use MyProject\Models\Articles\Article;
use MyProject\Models\Users\User;
use MyProject\Services\UsersAuthService;
use MyProject\View\View;
use MyProject\Exceptions\UnauthorizedException;
use MyProject\Exceptions\InvalidArgumentException;
use MyProject\Exceptions\Forbidden;
use MyProject\Models\Articles\Comment;

class ArticlesController    extends AbstractController
{

    public function view(int $articleId)
    {
        $article = Article::getById($articleId);

        if ($article === null) {
            throw new NotFoundException();
        }
        $comments= Comment::findByArticleId($articleId);
        if ($this->user){
            $admin = $this->user->isAdmin();
        }
        else
            $admin=null;
        $this->view->renderHtml('articles/view.php', [
        'article' => $article,
        'comments'=> $comments,
        'admin'   =>$admin
    ]);
    }

    public function edit(int $articleId)
    {
        $article = Article::getById($articleId);

        if ($article === null) {
            throw new NotFoundException();
        }

        if ($this->user === null) {
            throw new UnauthorizedException();
        }
        if (!$this->user->isAdmin()){
            throw new Forbidden();
        }

        if (!empty($_POST)) {
            try {
                $article->updateFromArray($_POST);
            } catch (InvalidArgumentException $e) {
                $this->view->renderHtml('articles/edit.php', ['error' => $e->getMessage(), 'article' => $article]);
                return;
            }

            header('Location: /articles/' . $article->getId(), true, 302);
            exit();
        }

        $this->view->renderHtml('articles/edit.php', ['article' => $article]);
    }

    public function add(): void
    {
        if ($this->user === null) {
            throw new UnauthorizedException();
        }
        if (!$this->user->isAdmin()){
            throw new Forbidden();
        }

        if (!empty($_POST)) {
            try {
                $article = Article::createFromArray($_POST, $this->user);
            } catch (InvalidArgumentException $e) {
                $this->view->renderHtml('articles/add.php', ['error' => $e->getMessage()]);
                return;
            }

            header('Location: /articles/' . $article->getId(), true, 302);
            exit();
        }

        $this->view->renderHtml('articles/add.php');
    }
    public function comments(int $articleId) :void
    {

        if (!empty($_POST)){
            try {
                $comment = Comment::createComment($_POST, $this->user, $articleId );
            } catch (InvalidArgumentException $e) {
                $this->view->renderHtml('articles/view.php', ['error' => $e->getMessage()]);
                return;
            }
            header('Location: /articles/' . $comment->getArticleId().'#comment'.$comment->getId(), true, 302);
        }
    }
}
//Article
<?php

namespace MyProject\Models\Articles;

use MyProject\Models\ActiveRecordEntity;
use MyProject\Models\Users\User;
use MyProject\Exceptions\InvalidArgumentException;

class Article extends ActiveRecordEntity
{
    /** @var string */
    protected $name;

    /** @var string */
    protected $text;

    /** @var string */
    protected $authorId;

    /** @var string */
    protected $createdAt;
    ///** @var array*/
    //protected $comments;
    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getText(): string
    {
        return $this->text;
    }
    /**
     * @return User
     */
    public function getAuthor(): User
    {
        return User::getById($this->authorId);
    }
    //public function getComments():

    public function setName($name): void
    {
        $this->name=$name;
    }
    public function setText($text): void
    {
        $this->text=$text;
    }
    public function setAuthor(User $author): void
    {
        $this->authorId=$author->id;
    }

    protected static function getTableName(): string
    {
        return 'articles';
    }
    public static function createFromArray(array $fields, User $author): Article
    {
        if (empty($fields['name'])) {
            throw new InvalidArgumentException('Не передано название статьи');
        }

        if (empty($fields['text'])) {
            throw new InvalidArgumentException('Не передан текст статьи');
        }

        $article = new Article();

        $article->setAuthor($author);
        $article->setName($fields['name']);
        $article->setText($fields['text']);

        $article->save();

        return $article;
    }
    public function updateFromArray(array $fields): Article
    {
        if (empty($fields['name'])) {
            throw new InvalidArgumentException('Не передано название статьи');
        }

        if (empty($fields['text'])) {
            throw new InvalidArgumentException('Не передан текст статьи');
        }

        $this->setName($fields['name']);
        $this->setText($fields['text']);

        $this->save();

        return $this;
    }
}
//ActiveRecordEntity
<?php

namespace MyProject\Models;

use MyProject\Services\Db;

abstract class ActiveRecordEntity
{
    /** @var int */
    protected $id;

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    public function __set(string $name, $value)
    {
        $camelCaseName = $this->underscoreToCamelCase($name);
        $this->$camelCaseName = $value;
    }

    private function underscoreToCamelCase(string $source): string
    {
        return lcfirst(str_replace('_', '', ucwords($source, '_')));
    }
    private function mapPropertiesToDbFormat(): array
    {
        $reflector = new \ReflectionObject($this);
        $properties = $reflector->getProperties();

        $mappedProperties = [];
        foreach ($properties as $property) {
            $propertyName = $property->getName();
            $propertyNameAsUnderscore = $this->camelCaseToUnderscore($propertyName);
            $mappedProperties[$propertyNameAsUnderscore] = $this->$propertyName;
        }

        return $mappedProperties;
    }
    public function save(): void
    {
        $mappedProperties = $this->mapPropertiesToDbFormat();
        if ($this->id !== null) {
            $this->update($mappedProperties);
        } else {
            $this->insert($mappedProperties);
        }
    }
    private function insert(array $mappedProperties): void
    {
        $filteredProperties = array_filter($mappedProperties);

        $columns = [];
        $paramsNames = [];
        $params2values = [];
        foreach ($filteredProperties as $columnName => $value) {
            $columns[] = '`' . $columnName. '`';
            $paramName = ':' . $columnName;
            $paramsNames[] = $paramName;
            $params2values[$paramName] = $value;
        }

        $columnsViaSemicolon = implode(', ', $columns);
        $paramsNamesViaSemicolon = implode(', ', $paramsNames);

        $sql = 'INSERT INTO ' . static::getTableName() . ' (' . $columnsViaSemicolon . ') VALUES (' . $paramsNamesViaSemicolon . ');';

        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
        $this->id = $db->getLastInsertId();
    }
    private function update(array $mappedProperties): void
    {
        $columns2params = [];
        $params2values = [];
        $index = 1;
        foreach ($mappedProperties as $column => $value) {
            $param = ':param' . $index; // :param1
            $columns2params[] = $column . ' = ' . $param; // column1 = :param1
            $params2values[':param' . $index] = $value; // [:param1 => value1]
            $index++;
        }
        $sql = 'UPDATE ' . static::getTableName() . ' SET ' . implode(', ', $columns2params) . ' WHERE id = ' . $this->id;
        $db = Db::getInstance();
        $db->query($sql, $params2values, static::class);
    }

    public function delete(){
        $db = Db::getInstance();
        $db->query(
            'DELETE FROM `' . static::getTableName() . '` WHERE id = :id',
            [':id' => $this->id]
        );
        $this->id = null;
    }
    public static function findOneByColumn(string $columnName, $value): ?self
    {
        $db = Db::getInstance();
        $result = $db->query(
            'SELECT * FROM `' . static::getTableName() . '` WHERE `' . $columnName . '` = :value LIMIT 1;',
            [':value' => $value],
            static::class
        );
        if ($result === []) {
            return null;
        }
        return $result[0];
    }

    private function camelCaseToUnderscore(string $source): string
    {
        return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $source));
    }
    /**
     * @param int $id
     * @return static|null
     */
    public static function getById(int $id): ?self
    {
        $db = Db::getInstance();
        $entities = $db->query(
            'SELECT * FROM `' . static::getTableName() . '` WHERE id=:id;',
            [':id' => $id],
            static::class
        );
        return $entities ? $entities[0] : null;
    }

    /**
     * @return static[]
     */
    public static function findAll(): array
    {
        $db = Db::getInstance();
        return $db->query('SELECT * FROM `' . static::getTableName() . '`;', [], static::class);
    }

    abstract protected static function getTableName(): string;
}
ivashkevich 10.04.2020 в 13:05
        }
        else
            $admin=null;

Ох, ну и где вы этого нахватались? Перечитайте урок по условиям из курса для начинающих. В этом курсе так писать уже точно нельзя.

И что значит почему-то? Смотрите дебаггером запрос и данные, которые в него передаются.

dima1 21.05.2020 в 19:08

А для комментариев обычно делают новый контроллер?

ivashkevich 22.05.2020 в 08:11

Ага)

OneMoreTime 16.06.2020 в 23:03

В попытках доделать/переделать блог возникает все больше вопросов..
Один из них насчет архитектуры:
Как корректно совокупить комментарии и статьи? Например в контроллере статей я делаю метод, который отображает статью и из модели Comment подтягивает на эту же странчку все комментарии к этой статье. Если комментарий удаляется или редактируется, рендерится опять же та же статья с соответствующей ошибкой или сообщением об удалении комментария.
Чисто с пользовательской точки зрения - это удобно, когда не разделены комментарии и статья. Все на одной страничке. Но, если я хочу например в админке иметь список всех комментариев и прямо в админке иметь возможность вызывать функции редактирования/удаления отдельных комментариев, имеющиеся методы удаления/редактирования комментариев использовать уже неудобно, т.к. они имеют условия перенаправления в случае удачи/неудачи на страничку со статьей к которой относится комментарий, над которым производится действие, а выполняя эти действия в админке, нужно возвращаься к списку комментариев туда же в админку.

Использовать для админки и для комментариев на страничках отдельных статей общие методы, которые в контроллере комментариев и при этом делать в контроллере комментариев условия перенаправления в методах редактирования/удаления в зависимости от того кто вызывал метод - админконтоллер или комментсконтроллер - вроде как-то слишком коряво и не универсально для самих этих методов...

Делать в админконтроллере отдельные методы для редактирования и удаления комментариев - вроде как практически дублирование логики методов будет, за исключением только содержания и адреса рендеринга, да и корректно ли будет размещать логику работы с далением, редактированием комментариев в админконтроллере?

Еще вариант - сделать под статьей кнопку, коотрая будет открывать комментарии в отдельном окне, В этом случае, не нужно будет каждый раз при рендеринге связывать в представлении статью и комментарии к ней. Тогда одни и те же методы из контроллера комментариев можно будет универсально использовать и для обработки в контексте самих комментариев в дополнение статей и в контексте админки со своим отдельным представлением. Только при этом как мне кажется для пользователя - это менее удобно.
Может есть еще какие-то не учтенные мной варианты решения вопроса?

ivashkevich 17.06.2020 в 07:45

Выход - сделать API для работы с комментариями. В контроллере статей единственное что делать с комментариями - выводить их. Это нормальное решение в плане разделение ответственности и правильной архитектуры. Но без знания JS можно просто это нарушить и делать в контроллере статей. Никто вас за это не расстреляет :) Либо как вы написали - отдельная страничка для редактирования и администрирования - промежуточный вариант между ОТЛИЧНО и ПЛОХО.

SkSeMi 31.01.2021 в 23:06

Пытаюсь реализовать вывод комментариев и формы добавления авторизованных комментариев на странице любой статьи.
Поймал себя на мысли, что возник вопрос, если необходимо внести свои пользовательские так сказать те же стили, то как отобразить их в templates/articles/view.php если там уже есть header.php тогда его убрать и жестко прописать полностью его содердимое в view.php?

Либо второй вариант каким-то образом во внутрь view.php интегрировать отображение формы добавления нового комментария templates/comments/form_add.php

Либо передавать каким -то образом в сущность View загаловочную часть, если не было пекредано, то по умолчанию что прописано в header.php?
Здесь речь про title, css, js файлы.

Как быть подскажите, пожалуйста?!

ivashkevich 01.02.2021 в 06:42

Ну а в чем проблема сделать стили для всего сайта, для комментариев использовать специальный CSS-класс?

SkSeMi 01.02.2021 в 08:47

Тогда просто может получиться большая css портянка просто...

ivashkevich 01.02.2021 в 09:34

css в реальном проекте это почти всегда портянка) не нужно этого бояться

[email protected] 26.02.2021 в 22:31

столкнулся с фобией, сделать что-то неправильно и нарушить целостность текущей архитектуры. в частности, не могу придумать, как сделать админку. я сейчас попробую перечислить шаги, а вы, если можете, скажите где не туда свернул:

  1. сделал контроллер AdminsController + роутинг( вида: admins/login, admins/articles, admins/comments и т.д).
  2. реализовал функционал для AdminsController.
  3. в папке templates создал папку admins и там храню шаблоны для администраторских функций.
  4. создаю в templates/admin футер для администратора с набором ссылок для перехода.

ииии, дальше я попал в тупик, т.к. столкнулся с тем, что при переходе по страницам, может затеряться админский футер. я вижу решение - удалить футер для админа и в обычном (footer.php) сделать 2 варианта отображения ссылок в зависимости от типа пользователя.
но правильно ли это?
в моем понимании, страницы не должны разделять контент в зависимости от того, какой тип пользователя. и, для админов необходимо реализовать новые (хоть и заимствуя некоторый код от обычных) страницы.
где я прав, а где не прав? спасибо)

ivashkevich 28.02.2021 в 06:16

Везде прав) Можно и так и так, разницы нет с точки зрения "правильности".
Я бы футер для админа подключал непосредственно в админских шаблонах перед основным футером.

[email protected] 28.02.2021 в 13:46

а как быть с пересекающимися функциями у AdminsController и ArticlesController? ведь за добавление и редактирование статей отвечает ArticlesController, но за эти операции отвечает администратор, действия которого обрабатывает AdminsController.

ivashkevich 08.03.2021 в 10:30

Ну пусть редактирование будет в ArticlesController

Логические задачи с собеседований