Блог на Laravel 7, часть 16. Роль нового пользователя, сообщение админу о новом посте

21.02.2021

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхБлогПанельУправленияПраваДоступаПрактикаФреймворк

Назначение роли при регистрации

После регистрации и подтверждения адреса почты мы должны назначить пользователю роль user, чтобы он мог добавлять посты и оставлять комментарии. Это значит, что роль user должна существовать на момент запуска блога и эту роль нельзя удалить через панель управления. Поэтому сделаем так, чтобы роль user нельзя было удалить и невозможно было изменить slug роли.

class RoleController extends Controller {
    /* ... */
    public function update(Request $request, Role $role) {
        if ($role->id === 1) {
            return redirect()
                ->route('admin.role.index')
                ->withErrors('Эту роль нельзя редактировать');
        }
        $this->validator($request->all(), $role->id)->validate();
        if (in_array($role->id, [2, 3])) {
            $role->update($request->except('slug'));
        } else {
            $role->update($request->all());
        }
        $role->permissions()->sync($request->perms ?? []);
        return redirect()
            ->route('admin.role.index')
            ->with('success', 'Роль была успешно отредактирована');
    }
    /* ... */
    public function destroy(Role $role) {
        if (in_array($role->id, [1, 2, 3])) {
            return redirect()
                ->route('admin.role.index')
                ->withErrors('Эту роль нельзя удалить');
        }
        $role->delete();
        return redirect()
            ->route('admin.role.index')
            ->with('success', 'Роль была успешно удалена');
    }
    /* ... */
}

Теперь будем назначать для новых пользователей роль user:

class VerifyEmailController extends Controller {
    /* ... */
    public function verify($token, $id) {
        /* ... */
        // все проверки пройдены, активируем аккаунт
        $user->update(['email_verified_at' => Carbon::now()]);
        // назначаем роль для нового пользователя
        $user->assignRoles('user');
        return redirect()
            ->route('auth.login')
            ->with('success', 'Вы успешно подтвердили свой адрес почты');
    }
    /* ... */
}

Извещение админа о новых постах

После создания нового поста пользователем, нам надо известить об этом администратора, чтобы он мог опубликовать его. Будем отправлять письмо двум случайным администраторам, чтобы равномерно распределить между ними работу. Есть много способов это сделать, рассмотрим их последовательно, от простого к сложному — с целью изучения Laravel.

1. Отправляем письмо из контроллера

Тут все просто — из контроллера User\PostController, сразу после создания поста, отправляем письмо с помощью фасада Mail:

use Illuminate\Support\Facades\Mail;

class PostController extends Controller {
    /**
     * Сохраняет новый пост в базу данных
     */
    public function store(PostRequest $request) {
        /* ... */
        $this->notify($post);
        /* ... */
    }
    /**
     * Отправляет письмо админу о создании нового поста
     */
    private function notify(Post $post) {
        $users = User::inRandomOrder()->get();
        $editors = [];
        foreach ($users as $user) {
            if ($user->hasPermAnyWay('publish-post')) {
                $editors[] = $user->email;
            }
        }
        if (count($editors)) {
            Mail::send(
                'email.new-post',
                ['post' => $post],
                function ($message) use ($editors) {
                    $message->to($editors[0]);
                    if (isset($editors[1])) {
                        $message->cc($editors[1]);
                    }
                    $message->subject('Новый пост блога');
                }
            );
        }
    }
}

Шаблон письма resources/views/email/new-post.blade.php:

<h3>Новый пост блога</h3>

<p>Заголовок: {{ $post->name }}</p>
<p>Автор: {{ $post->user->name }}</p>
<p>Анонс: {{ $post->excerpt }}</p>

2. Создаем отдельный класс Mailable

В Laravel каждый тип почтового сообщения (обратная связь, заказ в магазине, новый пост блога), отправляемых приложением, представлен классом Mailable. Эти классы хранятся в директории app/Mail, которая будет создана при создании первого такого класса.

> php artisan make:mail NewPostMailer
namespace App\Mail;

use App\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class NewPostMailer extends Mailable {

    use Queueable, SerializesModels;

    private $post;

    public function __construct(Post $post) {
        $this->post = $post;
    }

    public function build() {
        $users = User::inRandomOrder()->get();
        $editors = [];
        // ищем тех, кто может опубликовать пост
        foreach ($users as $user) {
            if ($user->hasPermAnyWay('publish-post')) {
                $editors[] = ['name' => $user->name, 'mail' => $user->email];
            }
            if (count($editors) > 1) break;
        }
        if (count($editors)) {
            $this->to($editors[0]['mail'], $editors[0]['name']);
            if (isset($editors[1])) {
                $this->cc($editors[1]['mail'], $editors[1]['name']);
            }
        } else {
            // если письмо некому отправлять
            $this->to(config('mail.from.address'), config('mail.from.name'));
        }
        $this->from('noreply@example.com', 'Блог о веб-разработке');
        $this->subject('Новый пост блога');
        return $this->view('email.new-post', ['post' => $this->post]);
    }
}

Чтобы не указывать заголовок письма From (от кого), можно задать это в файле .env:

APP_NAME="Блог о веб-разработке"
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="${APP_NAME}"

Эти три значения используются далее в файле конфигурации отправки почты config/mail.php:

return [
    /* ... */
    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'noreply@example.com'),
        'name' => env('MAIL_FROM_NAME', 'Example'),
    ],
    /* ... */
];
class NewPostMailer extends Mailable {
    /* ... */
    public function build() {
        /* ... */
        // удаляем $this->from(..........);
        $this->subject('Новый пост блога');
        return $this->view('email.new-post', ['post' => $this->post]);
    }
}

Передать данные в шаблон можно через публичное свойство класса:

class NewPostMailer extends Mailable {

    use Queueable, SerializesModels;

    public $post;

    public function __construct(Post $post) {
        $this->post = $post;
    }

    public function build() {
        /* ... */
        $this->subject('Новый пост блога');
        return $this->view('email.new-post');
    }
}

Шаблон для письма мы уже создали ранее, так что осталось только отправить письмо из контроллера:

use App\Mail\NewPostMailer;
use Illuminate\Support\Facades\Mail;

class PostController extends Controller {
    /**
     * Сохраняет новый пост в базу данных
     */
    public function store(PostRequest $request) {
        /* ... */
        Mail::send(new NewPostMailer($post));
        /* ... */
    }
    /**
     * Отправляет письмо админу о создании нового поста
     */
    private function notify(Post $post) {
        // TODO: этот метод больше не нужен
    }
}

Отправка письма занимает время, так что можно поставить задачу в очередь:

Mail::queue(new NewPostMailer($post));

Можно задержать отправку сообщения с помощью метода later():

Mail::later(now()->addMinutes(30), new NewPostMailer($post));

Можно указать соединение и очередь, куда надо добавить задачу:

$message = new NewPostMailer($post)->onConnection('redis')->onQueue('email');
Mail::queue($message);

Если реализовать контракт ShouldQueue для класса NewPostMailer — задача всегда будет добавляться в очередь, даже если используется метод send() вместо queue().

use Illuminate\Contracts\Queue\ShouldQueue;

class NewPostMailer extends Mailable implements ShouldQueue {
    /* ... */
}

О том, как организовать очередь — мы еще поговорим чуть позже. Нам потребуется таблица базы данных для хранения задач в очереди и отдельный класс, который эти задачи будет выполнять.

3. Отправляем письмо по событию

Список предопределенных событий, которые можно использовать при работе с Eloquent моделями, можно посмотреть в трейте HasEvents. Это retrieved — при извлечении модели из базы данных, creating — перед записью новой модели в базу данных, created — после записи новой модели в базу данных и так далее. Подробнее о событиях и их обработке можно прочитать здесь, а о событиях Eloquent — здесь.

Создадим директорию app/Observers и разместим в ней файл класса PostObserver.php:

namespace App\Observers;

use App\Mail\NewPostMailer;
use App\Post;
use Illuminate\Support\Facades\Mail;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        Mail::send(new NewPostMailer($post));
    }
}

В сервис-провайдере AppServiceProvider зарегистрируем наблюдателя за событиями модели Post:

namespace App\Providers;

use App\Observers\PostObserver;
use App\Post;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider {

    public function register() {
        // ...
    }

    public function boot() {
        Post::observe(PostObserver::class);
    }
}

Отправка письма занимает время, так что можно поставить задачу в очередь:

Mail::queue(new NewPostMailer($post));

Можно задержать отправку сообщения с помощью метода later():

Mail::later(now()->addMinutes(30), new NewPostMailer($post));

Можно указать соединение и очередь, куда надо добавить задачу:

$message = new NewPostMailer($post)->onConnection('redis')->onQueue('email');
Mail::queue($message);

Теперь метод store() контроллера User\PostController больше не нуждается в вызове метода send() фасада Mail.

4. Добавляем отправку в очередь

Очереди позволяют отложить выполнение времязатратной задачи, такой как отправка e-mail, на более позднее время, ускоряя обработку запроса — после создания нового поста пользователю не надо будет ждать отправки письма. В файле настроек config/queue.php есть настройка connections, которая задает неколько коннектов к бэкенд-сервису обработки заданий. У каждого подключения может быть несколько очередей, каждая из которых содержит список задач, которые надо выполнить. При добавлении новой задачи в очередь, нужно указать — для какого подключения эта задача и в какую очередь ее добавить. Если подключение и очередь не указаны — будут использованы подключение default и очередь default.

Очередь заданий будем хранить в БД, поэтому редактируем файл .env:

QUEUE_CONNECTION=database

Теперь создадим таблицу базы данных для хранения задач в очереди:

> php artisan queue:table
Migration created successfully!
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateJobsTable extends Migration {
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up() {
        Schema::create('jobs', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('queue')->index();
            $table->longText('payload');
            $table->unsignedTinyInteger('attempts');
            $table->unsignedInteger('reserved_at')->nullable();
            $table->unsignedInteger('available_at');
            $table->unsignedInteger('created_at');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down() {
        Schema::dropIfExists('jobs');
    }
}
> php artisan migrate
Migrating: 2021_02_19_120057_create_jobs_table
Migrated:  2021_02_19_120057_create_jobs_table (0.04 seconds)

Создаем класс, который будет выполнять задания из очереди:

> php artisan make:job NewPostWorker
Job created successfully.
namespace App\Jobs;

use App\Post;
use \App\Mail\NewPostMailer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class NewPostWorker implements ShouldQueue {

    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $post;

    public function __construct(Post $post) {
        $this->post = $post;
    }

    public function handle() {
        Mail::send(new NewPostMailer($this->post));
    }
}

Теперь в PostObserver добавим задание на отправку письма в очередь:

namespace App\Observers;

// use App\Mail\NewPostMailer;
use App\Jobs\NewPostWorker;
use App\Post;
// use Illuminate\Support\Facades\Mail;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        // Mail::send(new NewPostMailer($post));
        NewPostWorker::dispatch($post);
    }
}

Теперь все готово, запускаем обработчика очереди и добавляем новый пост блога:

> php artisan queue:work
[2021-02-23 07:56:02][1] Processing: App\Jobs\NewPostWorker
[2021-02-23 07:56:03][1] Processed:  App\Jobs\NewPostWorker

Уже упоминал об этом выше, но давайте посмотрим, как это работает. Если наш класс отправки писем App\Mail\NewPostMailer реализует интерфейс ShouldQueue, то мы можем просто вызвать метод send() фасада Mail — вместо отправки письма будет добавлено задание в очередь.

class NewPostMailer extends Mailable implements ShouldQueue {
    /* ... */
}
namespace App\Observers;

use App\Mail\NewPostMailer;
// use App\Jobs\NewPostWorker;
use App\Post;
use Illuminate\Support\Facades\Mail;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        // вместо отправки письма добавляется задание в очередь, так как
        // класс App\Mail\NewPostMailer реализует интерфейс ShouldQueue
        Mail::send(new NewPostMailer($post));
        // NewPostWorker::dispatch($post);
    }
}

5. Отправляем через уведомления

Laravel Notifications позволяет отправлять уведомления пользователям — короткие сообщения, просто дающие представление об изменениях состояния. Одно из достоинств системы уведомлений — можно использовать различные каналы для доставки — это mail, sms, database, slack, telegram и прочие.

Документация laravel говорит, что уведомления можно отправлять двумя способами: используя метод notify() трейта Notifiable или используя фасад Notification:

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable {
    use Notifiable;
}
use App\Notifications\NewShopOrder;
// известить пользователя $user о новом заказе
$user->notify(new NewShopOrder($order));
use App\Notifications\NewShopOrder;
use Illuminate\Support\Facades\Notification;
// известить пользователей $users о новом заказе
Notification::send($users, new NewShopOrder($order));

Здесь уведомление представлено классом App\Notifications\NewShopOrder, который можно создать командой:

> php artisan make:notification NewShopOrder
namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class NewShopOrder extends Notification {

    use Queueable;

    public function __construct() {
        // ...
    }

    public function via($notifiable) {
        return ['mail'];
    }

    public function toMail($notifiable) {
        return (new MailMessage())
                    ->line('The introduction to the notification.')
                    ->action('Notification Action', url('/'))
                    ->line('Thank you for using our application!');
    }

    public function toArray($notifiable) {
        return [
            // ...
        ];
    }
}

Это была теория, а теперь практика. Создадим класс для отправки уведомления о создании нового поста блога:

> php artisan make:notification NewPostNotifier
namespace App\Notifications;

use App\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class NewPostNotifier extends Notification {

    use Queueable;

    private $post;

    public function __construct(Post $post) {
        $this->post = $post;
    }

    public function via($notifiable) {
        return ['mail'];
    }

    public function toMail($notifiable) {
        return (new MailMessage())
                    ->subject('Новый пост блога')
                    ->greeting('Новый пост блога')
                    ->line('Заголовок: ' . $this->post->name)
                    ->line('Автор: ' . $this->post->user->name)
                    ->line('Анонс: ' . $this->post->excerpt);
    }

    public function toArray($notifiable) {
        return [
            // ...
        ];
    }
}

В обработчике события добавления нового поста PostObserver::created() будем отправлять уведомление с помощью метода notify():

namespace App\Observers;

use App\Notifications\NewPostNotifier;
use App\Post;
use App\User;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        User::getRandomEditor()->notify(new NewPostNotifier($post));
    }
}

В модель User добавим метод getRandomEditor(), чтобы получить случайного пользователя, который может опубликовать новый пост:

class User extends Authenticatable {
    /* ... */
    public static function getRandomEditor() {
        $users = self::inRandomOrder()->get();
        foreach ($users as $user) {
            if ($user->hasPermAnyWay('publish-post')) {
                return $user;
            }
        }
    }
    /* ... */
}

Метод App\Notifications\NewPostNotifier::toMail() может возвращать не только экземпляр класса MailMessage, но и экземпляр Mailable-класса, то есть App\Mail\NewPostMailer. Но в этом случае обязательно должен быть вызван метод to(), чтобы установить получателя сообщения. У нас это уже все есть — в методе build() мы устанавливаем получателя сообщения + отправляем копию еще одному администратору.

class NewPostNotifier extends Notification {
    /* ... */
    public function toMail($notifiable) {
        return (new \App\Mail\NewPostMailer($this->post));
    }
    /* ... */
}

Теперь в PostObserver::created() нам нет необходимости вызывать метод getRandomEditor(), потому что он нужен исключительно для того, чтобы знать, кому отправлять сообщение. Метод getRandomEditor() возвращает объект User, а Laravel ищет свойсто email этого объекта — и отправляет сообщение по этому адресу. Поскольку адрес получателя мы задаем сами, то и вызвать метод notify() можем на любом экземпляре модели User — текущий пользователь вполне подойдет (как и любой другой).

namespace App\Observers;

use App\Notifications\NewPostNotifier;
use App\Post;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        auth()->user()->notify(new NewPostNotifier($post));
    }
}

Отправку уведомления можно добавить в очередь, для этого достаточно добавить интерфейс ShouldQueue:

class NewPostNotifier extends Notification implements ShouldQueue {
    /* ... */
}

Больше ничего делать не нужно — при вызове метода notify() вместо отправки уведомления будет добавлено задание в очередь. При добавлении в очередь можно отложить доставку уведомления с помощью метода delay().

namespace App\Observers;

use App\Notifications\NewPostNotifier;
use App\Post;

class PostObserver {
    /*
     * Срабатывает после создания нового поста
     */
    public function created(Post $post) {
        auth()->user()->notify((new NewPostNotifier($post))->delay(now()->addMinutes(30)));
    }
}
Мы рассмотрели пять способов отправки сообщения админам о новых постах блога, но оставим только один — третий, без использования очередей. Но классы NewPostWorker и NewPostNotifier не будем удалять, чтобы можно было в любой момент перейти на четвертый или пятый способ.

Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Панель управления • Права доступа • Практика • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.