Перевод статьи подготовлен специально для студентов курса «Framework Laravel».




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

К счастью, с Laravel и Pusher реализация этого функционала довольно проста.

Уведомления в реальном времени


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

Лучшим подходом является использование возможностей WebSockets и получение уведомлений в момент их отправки. Это именно то, что мы собираемся реализовать в этой статье.

Pusher


Pusher — это веб-сервис для интеграции двунаправленной функциональности в реальном времени через WebSockets в веб и мобильные приложения.

У него очень простой API, но мы собираемся сделать его использование еще проще с Laravel Broadcasting и Laravel Echo.

В этой статье мы собираемся добавить уведомления в реальном времени в уже существующий блог.

Проект


Инициализация


Сначала мы клонируем простой блог Laravel:

git clone   https://github.com/marslan-ali/laravel-blog

Затем мы создадим базу данных MySQL и настроим переменные среды, чтобы предоставить приложению доступ к базе данных.

Давайте скопируем env.example в .env и обновим переменные, связанные с базой данных.

cp .env.example .envDB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Теперь давайте установим зависимости проекта с помощью

composer install

И запустим команду миграции и заполнения, чтобы заполнить базу данных какими-нибудь данными:

php artisan migrate --seed

Если вы запустите приложение и зайдите в /posts, вы сможете увидеть список сгенерированных постов.

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

Подписка на пользователей


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

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

php artisan make:migration create_followers_table --create=followers

Нам нужно добавить несколько полей к этой миграции: user_id для представления пользователя, который подписан, и поле follows_id для представления пользователя, на которого подписались.

Обновите миграцию следующим образом:

public function up()
{
    Schema::create('followers', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id')->index();
        $table->integer('follows_id')->index();
        $table->timestamps();
    });
}

Теперь перейдем к созданию таблицы:

php artisan migrate

Давайте добавим методы отношений в модель User.

// ...

class extends Authenticatable
{
    // ...

    public function followers() 
    {
        return $this->belongsToMany(self::class, 'followers', 'follows_id', 'user_id')
                    ->withTimestamps();
    }

    public function follows() 
    {
        return $this->belongsToMany(self::class, 'followers', 'user_id', 'follows_id')
                    ->withTimestamps();
    }
}

Теперь, когда модель User имеет необходимые отношения, followers возвращает всех подписчиков пользователя, а follows возвращает всех, на кого подписан сам пользователь.

Нам понадобятся некоторые вспомогательные функции, позволяющие пользователю подписываться на других пользователей — follow, и проверять, подписан ли пользователь на какого-нибудь конкретного пользователя — isFollowing.

// ...

class extends Authenticatable
{
    // ...

    public function follow($userId) 
    {
        $this->follows()->attach($userId);
        return $this;
    }

    public function unfollow($userId)
    {
        $this->follows()->detach($userId);
        return $this;
    }

    public function isFollowing($userId) 
    {
        return (boolean) $this->follows()->where('follows_id', $userId)->first(['id']);
    }

}

Отлично. После подготовки модели нужно составить список пользователей.

Список пользователей


Давайте начнем с определения необходимых маршрутов

/...
Route::group(['middleware' => 'auth'], function () {
    Route::get('users', 'UsersController@index')->name('users');
    Route::post('users/{user}/follow', 'UsersController@follow')->name('follow');
    Route::delete('users/{user}/unfollow', 'UsersController@unfollow')->name('unfollow');
});

Затем пришло время создать новый контроллер для пользователей:

php artisan make:controller UsersController

Мы добавим к нему метод index:

// ...
use App\User;
class UsersController extends Controller
{
    //..
    public function index()
    {
        $users = User::where('id', '!=', auth()->user()->id)->get();
        return view('users.index', compact('users'));
    }
}

Метод нуждается в представлении. Давайте создадим представление users.index и поместим в него следующую разметку:

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="col-sm-offset-2 col-sm-8">

            <!-- Following -->
            <div class="panel panel-default">
                <div class="panel-heading">
                    All Users
                </div>

                <div class="panel-body">
                    <table class="table table-striped task-table">
                        <thead>
                        <th>User</th>
                        <th> </th>
                        </thead>
                        <tbody>
                        @foreach ($users as $user)
                            <tr>
                                <td clphpass="table-text"><div>{{ $user->name }}</div></td>
                                @if (auth()->user()->isFollowing($user->id))
                                    <td>
                                        <form action="{{route('unfollow', ['id' => $user->id])}}" method="POST">
                                            {{ csrf_field() }}
                                            {{ method_field('DELETE') }}

                                            <button type="submit" id="delete-follow-{{ $user->id }}" class="btn btn-danger">
                                                <i class="fa fa-btn fa-trash"></i>Unfollow
                                            </button>
                                        </form>
                                    </td>
                                @else
                                    <td>
                                        <form action="{{route('follow', ['id' => $user->id])}}" method="POST">
                                            {{ csrf_field() }}

                                            <button type="submit" id="follow-user-{{ $user->id }}" class="btn btn-success">
                                                <i class="fa fa-btn fa-user"></i>Follow
                                            </button>
                                        </form>
                                    </td>
                                @endif
                            </tr>
                        @endforeach
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
@endsection

Теперь вы можете посетить страницу /users, чтобы увидеть список пользователей.

Follow и Unfollow


В UsersController отсутствуют методы follow и unfollow. Давайте реализуем их, чтобы завершить эту часть.

//...
class UsersController extends Controller
{
    //...

    public function follow(User $user)
    {
        $follower = auth()->user();
        if ($follower->id == $user->id) {
            return back()->withError("You can't follow yourself");
        }
        if(!$follower->isFollowing($user->id)) {
            $follower->follow($user->id);

            // отправка уведомления
            $user->notify(new UserFollowed($follower));

            return back()->withSuccess("You are now friends with {$user->name}");
        }
        return back()->withError("You are already following {$user->name}");
    }

    public function unfollow(User $user)
    {
        $follower = auth()->user();
        if($follower->isFollowing($user->id)) {
            $follower->unfollow($user->id);
            return back()->withSuccess("You are no longer friends with {$user->name}");
        }
        return back()->withError("You are not following {$user->name}");
    }
}

С этим функционалом мы закончили. Теперь мы можем подписываться на пользователей и отписываться от них на странице /users.

Уведомления


Laravel предоставляет API для отправки уведомлений по нескольким каналам. Электронная почта, SMS, веб-уведомления и любые другие типы уведомлений могут быть отправлены с помощью класса Notification.

У нас будет два типа уведомлений:

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

Уведомление о подписке


Используя artisan-команды, мы можем сгенерировать миграцию для уведомлений:

php artisan notifications:table

Давайте выполним миграцию и создадим эту таблицу.

php artisan migrate

Мы начнем с уведомлений о подписке. Давайте выполним эту команду для создания класса уведомлений:

php artisan make:notification UserFollowed

Затем мы модифицируем файл класса уведомлений, который мы только что создали:

class UserFollowed extends Notification implements ShouldQueue
{
    use Queueable;

    protected $follower;

    public function __construct(User $follower)
    {
        $this->follower = $follower;
    }

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

    public function toDatabase($notifiable)
    {
        return [
            'follower_id' => $this->follower->id,
            'follower_name' => $this->follower->name,
        ];
    }
}

С помощью этих нескольких строк кода мы уже можем достичь многого. Сначала мы требуем, чтобы экземпляр $follower был внедрен при создании этого уведомления.

Используя метод via, мы говорим Laravel отправить это уведомление через канал database. Когда Laravel сталкивается с этим, он создает новую запись в таблице уведомлений.

user_id и type уведомления устанавливаются автоматически, плюс мы можем расширить уведомление дополнительными данными. Вот для чего предназначен toDatabase. Возвращаемый массив будет добавлен в поле data уведомления.

И, наконец, благодаря реализации ShouldQueue, Laravel автоматически поместит это уведомление в очередь, которая будет выполняться в фоновом режиме, что ускорит ответ. Это имеет смысл, потому что мы будем добавлять HTTP-вызовы, когда будем использовать Pusher позже.

Давайте реализуем уведомление о подписке на пользователя.

// ...
use App\Notifications\UserFollowed;
class UsersController extends Controller
{
    // ...
    public function follow(User $user)
    {
        $follower = auth()->user();
        if ( ! $follower->isFollowing($user->id)) {
            $follower->follow($user->id);

            // добавить это, чтобы отправить уведомление
            $user->notify(new UserFollowed($follower));

            return back()->withSuccess("You are now friends with {$user->name}");
        }

        return back()->withSuccess("You are already following {$user->name}");
    }

    //...
}

Мы можем вызвать метод notify для модели User, потому что она уже использует черту Notifiable.

Любая модель, которую вы хотите уведомить, должна использовать ее для получения доступа к методу notify.

Отмечаем уведомление как прочитанное

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

Мы собираемся создать прослойку, которая будет проверять, есть ли в запросе вхождение ?read=notification_id и помечает его как прочитанное.

Давайте сделаем эту прослойку с помощью следующей команды:

php artisan make:middleware MarkNotificationAsRead

Затем давайте поместим этот код в метод handle прослойки:

class MarkNotificationAsRead
{
    public function handle($request, Closure $next)
    {
        if($request->has('read')) {
            $notification = $request->user()->notifications()->where('id', $request->read)->first();
            if($notification) {
                $notification->markAsRead();
            }
        }
        return $next($request);
    }
}

Для того, чтобы наша прослойка выполнялась для каждого запроса, мы добавим ее в $middlewareGroups.

//...
class Kernel extends HttpKernel
{
    //...
    protected $middlewareGroups = [
        'web' => [
            //...
            \App\Http\Middleware\MarkNotificationAsRead::class,
        ],
        // ...
    ];
    //...
}

После этого давайте займемся отображением уведомлений.

Отображение уведомлений


Мы должны показать список уведомлений, используя AJAX, а затем обновить его в режиме реального времени с помощью Pusher. Во-первых, давайте добавим метод notifications в контроллер:

// ...
class UsersController extends Controller
{
    // ...
    public function notifications()
    {
        return auth()->user()->unreadNotifications()->limit(5)->get()->toArray();
    }
}

Этот код вернет последние 5 непрочитанных уведомлений. Нам просто нужно добавить маршрут, чтобы сделать его доступным.

//...
Route::group([ 'middleware' => 'auth' ], function () {
    // ...
    Route::get('/notifications', 'UsersController@notifications');
});

Теперь добавим выпадающий список для уведомлений в шапке.

<head>
    <!-- // ... // -->
    <!-- Scripts -->
    <script>
        window.Laravel = <?php echo json_encode([
            'csrfToken' => csrf_token(),
        ]); ?>
    </script>
    <!-- Это делает id текущего пользователя доступным в JavaScript -->
    @if(!auth()->guest())
        <script>
            window.Laravel.userId = <?php echo auth()->user()->id; ?>
        </script>
    @endif
</head>
<body>
    <!-- // ... // -->
    @if (Auth::guest())
        <li><a href="{{ url('/login') }}">Login</a></li>
        <li><a href="{{ url('/register') }}">Register</a></li>
    @else
        <!-- // add this dropdown // -->
        <li class="dropdown">
            <a class="dropdown-toggle" id="notifications" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
                <span class="glyphicon glyphicon-user"></span>
            </a>
            <ul class="dropdown-menu" aria-labelledby="notificationsMenu" id="notificationsMenu">
                <li class="dropdown-header">No notifications</li>
            </ul>
        </li>
<!-- // ... // -->

Мы также добавили глобальную переменную window.Laravel.userId в скрипт, чтобы получить ID текущего пользователя.

JavaScript и SASS


Мы собираемся использовать Laravel Mix для компиляции JavaScript и SASS. Во-первых, нам нужно установить npm-пакеты.

npm install

Теперь давайте добавим этот код в app.js:

window._ = require('lodash');
window.$ = window.jQuery = require('jquery');
require('bootstrap-sass');
var notifications = [];
const NOTIFICATION_TYPES = {
    follow: 'App\\Notifications\\UserFollowed'
};

Это всего лишь инициализация. Мы собираемся использовать notifications для хранения всех объектов уведомлений, независимо от того, извлекаются ли они через AJAX или Pusher.

Как вы, наверное, уже догадались, NOTIFICATION_TYPES содержит типы уведомлений.

Теперь давайте получим (“GET”) уведомления через AJAX.

//...
$(document).ready(function() {
    // проверить, есть ли вошедший в систему пользователь
    if(Laravel.userId) {
        $.get('/notifications', function (data) {
            addNotifications(data, "#notifications");
        });
    }
});
function addNotifications(newNotifications, target) {
    notifications = _.concat(notifications, newNotifications);
    // показываем только последние 5 уведомлений
    notifications.slice(0, 5);
    showNotifications(notifications, target);
}

Благодаря этому коду мы получаем последние уведомления от нашего API и помещаем их в раскрывающийся список.

Внутри addNotifications мы объединяем имеющиеся уведомления с новыми, используя Lodash, и берем только последние 5, которые и будут показаны.

Нам нужно еще несколько функций, чтобы закончить работу.

//...
function showNotifications(notifications, target) {
    if(notifications.length) {
        var htmlElements = notifications.map(function (notification) {
            return makeNotification(notification);
        });
        $(target + 'Menu').html(htmlElements.join(''));
        $(target).addClass('has-notifications')
    } else {
        $(target + 'Menu').html('<li class="dropdown-header">No notifications</li>');
        $(target).removeClass('has-notifications');
    }
}

Эта функция создает строку всех уведомлений и помещает ее в раскрывающийся список.
Если не было получено ни одного уведомления, отображается просто «Нет уведомлений».

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

Наконец, некоторые вспомогательные функции для создания строк уведомлений.

//...
// Сделать строку уведомления
function makeNotification(notification) {
    var to = routeNotification(notification);
    var notificationText = makeNotificationText(notification);
    return '<li><a href="' + to + '">' + notificationText + '</a></li>';
}
// получить маршрут уведомления в зависимости от его типа
function routeNotification(notification) {
    var to = '?read=' + notification.id;
    if(notification.type === NOTIFICATION_TYPES.follow) {
        to = 'users' + to;
    }
    return '/' + to;
}
// получить текст уведомления в зависимости от его типа
function makeNotificationText(notification) {
    var text = '';
    if(notification.type === NOTIFICATION_TYPES.follow) {
        const name = notification.data.follower_name;
        text += '<strong>' + name + '</strong> followed you';
    }
    return text;
}

Теперь мы просто добавим это в наш файл app.scss:

//... 
#notifications.has-notifications {
  color: #bf5329
}

Давайте скомпилируем ассеты:

npm run dev

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

Уведомление о новом посте


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

Начнем с создания класса уведомлений.

php artisan make:notification NewPost

Давайте модифицируем сгенерированный класс следующим образом:

// ..
use App\Post;
use App\User;
class NewArticle extends Notification implements ShouldQueue
{
    // ..
    protected $following;
    protected $post;
    public function __construct(User $following, Post $post)
    {
        $this->following = $following;
        $this->post = $post;
    }
    public function via($notifiable)
    {
        return ['database'];
    }
    public function toDatabase($notifiable)
    {
        return [
            'following_id' => $this->following->id,
            'following_name' => $this->following->name,
            'post_id' => $this->post->id,
        ];
    }
}

Далее нам нужно отправить уведомление. Есть несколько способов сделать это.

Мне нравится использовать наблюдатели Eloquent.

Давайте создадим наблюдателя за Post и будем слушать его события. Мы создадим новый класс: app/Observers/PostObserver.php

namespace App\Observers;
use App\Notifications\NewPost;
use App\Post;
class PostObserver
{
    public function created(Post $post)
    {
        $user = $post->user;
        foreach ($user->followers as $follower) {
            $follower->notify(new NewPost($user, $post));
        }
    }
}

Затем зарегистрируем наблюдателя в AppServiceProvider:

//...
use App\Observers\PostObserver;
use App\Post;
class AppServiceProvider extends ServiceProvider
{
    //...
    public function boot()
    {
        Post::observe(PostObserver::class);
    }
    //...
}

Теперь нам просто нужно отформатировать сообщение для отображения в JS:

// ...
const NOTIFICATION_TYPES = {
    follow: 'App\\Notifications\\UserFollowed',
    newPost: 'App\\Notifications\\NewPost'
};
//...
function routeNotification(notification) {
    var to = `?read=${notification.id}`;
    if(notification.type === NOTIFICATION_TYPES.follow) {
        to = 'users' + to;
    } else if(notification.type === NOTIFICATION_TYPES.newPost) {
        const postId = notification.data.post_id;
        to = `posts/${postId}` + to;
    }
    return '/' + to;
}
function makeNotificationText(notification) {
    var text = '';
    if(notification.type === NOTIFICATION_TYPES.follow) {
        const name = notification.data.follower_name;
        text += `<strong>${name}</strong> followed you`;
    } else if(notification.type === NOTIFICATION_TYPES.newPost) {
        const name = notification.data.following_name;
        text += `<strong>${name}</strong> published a post`;
    }
    return text;
}

И вуаля! Пользователи получают уведомления о подписках и новых постах! Попробуйте сами!

Выходим в режим реального времени с Pusher


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

Зарегистрируйте бесплатную учетную запись Pusher на pusher.com и создайте новое приложение.

...
BROADCAST_DRIVER=pusher
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_APP_ID=

Задайте параметры своей учетной записи в файле конфигурации broadcasting:

//...
    'connections' => [
            'pusher' => [
                //...
                'options' => [
                    'cluster' => 'eu',
                    'encrypted' => true
                ],
            ],
    //...

Затем мы зарегистрируем App\Providers\BroadcastServiceProvider в массиве providers.

// ...
'providers' => [
    // ...
    App\Providers\BroadcastServiceProvider
    //...
],
//...

Теперь мы должны установить PHP SDK и Laravel Echo от Pusher:

composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js

Нам нужно настроить данные уведомлений для трансляции. Давайте модифицируем уведомление UserFollowed:

//...
class UserFollowed extends Notification implements ShouldQueue
{
    // ..
    public function via($notifiable)
    {
        return ['database', 'broadcast'];
    }
    //...
    public function toArray($notifiable)
    {
        return [
            'id' => $this->id,
            'read_at' => null,
            'data' => [
                'follower_id' => $this->follower->id,
                'follower_name' => $this->follower->name,
            ],
        ];
    }
}

И NewPost:

//...
class NewPost extends Notification implements ShouldQueue
{
    //...
    public function via($notifiable)
    {
        return ['database', 'broadcast'];
    }
    //...
    public function toArray($notifiable)
    {
        return [
            'id' => $this->id,
            'read_at' => null,
            'data' => [
                'following_id' => $this->following->id,
                'following_name' => $this->following->name,
                'post_id' => $this->post->id,
            ],
        ];
    }
}

Последнее, что нам нужно сделать, это обновить наш JS. Откройте app.js и добавьте следующий код

// ...
window.Pusher = require('pusher-js');
import Echo from "laravel-echo";
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key',
    cluster: 'eu',
    encrypted: true
});
var notifications = [];
//...
$(document).ready(function() {
    if(Laravel.userId) {
        //...
        window.Echo.private(`App.User.${Laravel.userId}`)
            .notification((notification) => {
                addNotifications([notification], '#notifications');
            });
    }
});

И на этом все. Уведомления добавляются в режиме реального времени. Теперь вы можете поиграть с приложением и посмотреть, как обновляются уведомления.

Вывод


У Pusher очень удобный API, который делает реализацию событий в реальном времени невероятно простой. В сочетании с уведомлениями Laravel мы можем отправлять уведомления по нескольким каналам (электронная почта, SMS, Slack и т. д.) из одного места. В этом руководстве мы добавили функциональные возможности для отслеживания активности пользователей в простой блог и усовершенствовали его с помощью вышеупомянутых инструментов, чтобы получить некоторый плавный функционал в реальном времени.

У Pusher и Laravel есть гораздо больше уведомлений: в тандеме сервисы позволяют отправлять pub/sub-сообщения в режиме реального времени на браузеры, мобильные телефоны и IOT-устройства. Существует также API для получения online/offline статуса пользователей.

Пожалуйста, ознакомьтесь с их документацией (Pusher docs, Pusher tutorials, Laravel docs), чтобы узнать более подробную информацию о их использовании и истинном потенциале.

Если у вас есть какие-либо комментарии, вопросы или рекомендации, не стесняйтесь делиться ими в комментариях ниже!



Узнать подробнее о курсе.