Статьи

Как добавить уведомления в реальном времени в Laravel с помощью Pusher

Эта статья была рецензирована Рафи Юнесом и Верном Анчетой . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


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

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

Иллюстрация от Pusher с синхронизацией браузера и мобильного устройства с уведомлениями
Изображение через Pusher.com

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

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

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

толкатель

Pusher — это веб-сервис для

… Интеграция двунаправленной функциональности в реальном времени с помощью WebSockets в веб и мобильные приложения.

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

В этом уроке мы собираемся добавить уведомления в реальном времени в существующий блог.
Основные функциональные возможности аналогичны уведомлениям Laravel в реальном времени с помощью Stream .
Мы начнем с этого репо, сделанного Кристофером Вунди (я немного его изменил), который представляет собой простой блог, где пользователи могут выполнять CRUD для постов.

Проэкт

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

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

git clone https://github.com/vickris/simple-blog 

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

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

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

.env

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

 composer install 

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

 php artisan migrate --seed 

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

Следите за отношениями с пользователями

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

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

 php artisan make:migration create_followers_table --create=followers 

Нам нужно добавить некоторые поля к этой миграции: user_id для представления пользователя, который follows_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 

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

Давайте добавим методы отношений в модель 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(); } } 

app/User.php

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

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

 // ... 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']); } } 

app/User.php

Отлично. С установленной моделью пришло время составить список пользователей.

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

Начнем с настройки необходимых маршрутов

 //... 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'); }); 

routes/web.php

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

 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')); } } 

app/Http/Controllers/UsersController.php

Метод нуждается в представлении. Давайте создадим представление 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 

resources/views/users/index.blade.php

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

Чтобы подписаться или отписаться

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

 //... 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); // sending a notification $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}"); } } 

app/Http/Controllers/UsersController.php

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

Уведомления

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

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

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

Уведомление пользователя

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

 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, ]; } } 

app/Notifications/UserFollowed.php

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

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

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

И, наконец, реализуя 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); // add this to send a notification $user->notify(new UserFollowed($follower)); return back()->withSuccess("You are now friends with {$user->name}"); } return back()->withSuccess("You are already following {$user->name}"); } //... } 

app/Http/Controllers/UsersController.php

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

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

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

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

Давайте сделаем промежуточное программное обеспечение с помощью следующей команды:

 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); } } 

app/Http/Middleware/MarkNotificationAsRead.php

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

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

app/Http/Kernel.php

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

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

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

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

app/Http/Controllers/UsersController.php

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

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

routes/web.php

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

 <head> <!-- // ... // --> <!-- Scripts --> <script> window.Laravel = <?php echo json_encode([ 'csrfToken' => csrf_token(), ]); ?> </script> <!-- This makes the current user's id available in 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> <!-- // ... // --> 

resources/views/layouts/app.blade.php

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

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' }; 

app/resources/assets/js/app.js

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

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

Далее давайте «ПОЛУЧИМ» уведомления через AJAX.

 //... $(document).ready(function() { // check if there's a logged in user if(Laravel.userId) { $.get('/notifications', function (data) { addNotifications(data, "#notifications"); }); } }); function addNotifications(newNotifications, target) { notifications = _.concat(notifications, newNotifications); // show only last 5 notifications notifications.slice(0, 5); showNotifications(notifications, target); } 

app/resources/assets/js/app.js

Благодаря этому мы получаем последние уведомления от нашего 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'); } } 

app/resources/assets/js/app.js

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

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

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

 //... // Make a single notification string function makeNotification(notification) { var to = routeNotification(notification); var notificationText = makeNotificationText(notification); return '<li><a href="' + to + '">' + notificationText + '</a></li>'; } // get the notification route based on it's type function routeNotification(notification) { var to = '?read=' + notification.id; if(notification.type === NOTIFICATION_TYPES.follow) { to = 'users' + to; } return '/' + to; } // get the notification text based on it's type 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/resources/assets/js/app.js

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

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

app/resources/assets/sass/app.scss

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

 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, ]; } } 

app/Notifications/NewArticle.php

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

Давайте сделаем обозреватель для 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); } //... } 

app/Providers/AppServiceProvider.php

Теперь нам просто нужно отформатировать сообщение для отображения в 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; } 

app/resources/assets/js/app.js

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

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

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

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

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

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

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

config/broadcasting.php

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

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

config/app.php

Мы должны установить PHP Pusher SDK и Laravel Echo сейчас:

 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, ], ]; } } 

app/Notifications/UserFollowed.php

И 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, ], ]; } } 

app/Notifications/NewPost.php

Последнее, что нам нужно сделать, это обновить наш 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'); }); } }); 

app/resources/assets/js/app.js

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

демонстрация

Вывод

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

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

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

Позвольте мне услышать, что вы создали с этими технологиями в комментариях.