📜 ⬆️ ⬇️

Work with events in Laravel. Sending push notifications when publishing an article

In the comments to one of the first articles in my blog, the reader advised me to fasten push notifications through the Onesignal service. At that moment I had no idea what kind of animal it was and what it was eating with. Of course, I knew about the notifications themselves, but not about the service.
It was easy to google it and it turned out that this is a service that allows you to send push notifications of absolutely different kinds, across all platforms and devices. It has a convenient control panel / reporting, the possibility of deferred sending and so on.
I will not dwell on setting up the service itself. There are his Russian counterparts, links, if necessary, are easily located. Yes, and it's no longer about the service itself, but about the correct application architecture on Laravel.

Integration


Work with the service is divided into 2 parts: subscription of users and sending notifications. Therefore, the integration consists of two parts:
1) Client part: we place javascript
2) Server side: we are lazy people, so it’s not our method to go to the Onesignal admin area and post messages every time to send it manually. We would trust this business to smart machines! And, lo and behold! For this, onesignal has a JSON API.

Client part


Also I will not describe in detail, as everything is described on the service website. Let me just say that there are 2 ways. Simple: stupidly place them Javascript, which generates a button for a subscription. And more long: to impose a button with handles, on a click to call their URL.
As you may have guessed, I chose a simple way)
Below is the code for placement on the page, because I did not find a method for simple localization of all this near-button interface, I redefined all JS messages, since their library allows it. If someone needs Russian localization, you can take my already translated code.
<script src="https://cdn.onesignal.com/sdks/OneSignalSDK.js" async></script> <script> var OneSignal = OneSignal || []; OneSignal.push(["init", { appId: " id ", subdomainName: 'laravel-news', //   onesignal.com (   ) notifyButton: { enable: true, // Set to false to hide, size: 'large', // One of 'small', 'medium', or 'large' theme: 'default', // One of 'default' (red-white) or 'inverse" (whi-te-red) position: 'bottom-right', // Either 'bottom-left' or 'bottom-right' offset: { offset: { bottom: '90px', left: '0px', // Only applied if bottom-left right: '80px' // Only applied if bottom-right }, text: { "tip.state.unsubscribed": "       ", "tip.state.subscribed": "   ", "tip.state.blocked": "  ", "message.prenotify": "       ", "message.action.subscribed": "  !", "message.action.resubscribed": "   ", "message.action.unsubscribed": ",          ", "dialog.main.title": " ", "dialog.main.button.subscribe": "", "dialog.main.button.unsubscribe": "   ", "dialog.blocked.title": "      ", "dialog.blocked.message": "  ,   :" } }, prenotify: true, // Show an icon with 1 unread message for first-time site visitors showCredit: false, // Hide the OneSignal logo welcomeNotification: { "title": " Laravel", "message": "  !" }, promptOptions: { showCredit: false, // Hide Powered by OneSignal actionMessage: "   :", exampleNotificationTitleDesktop: "   ", exampleNotificationMessageDesktop: "     ", exampleNotificationTitleMobile: "  ", exampleNotificationMessageMobile: "     ", exampleNotificationCaption: "(    )", acceptButtonText: "".toUpperCase(), cancelButtonText: ", ".toUpperCase() } }]); </script> 

This completes the client side configuration.

Server part Architecture.


Getting to the most interesting.
Task: when placing a post (article) send push notifications.
But, at the same time, we keep in mind that soon when publishing an article, we will need to perform 100% more than one action. For example, send text to the “Original Texts” of a Yandex webmaster, tweet on Twitter and so on.
Therefore, it is necessary to organize this whole process in some way, rather than shove everything into a model or, upasiboh, controller.

Let's argue. The publication of the article itself is what? That's right - an event ! So let's use events . Their implementation in the chest is very good.
Of course, there was a spoiler in the title about the events, so everyone guessed right away)

According to the documentation, there are several ways to register events and create classes themselves. Let's stop on the most convenient variant.

Write the code


We will do this: in app / Providers / EventServiceProvider.php we indicate our event and its listener. The event is called PostPublishedEEvent, the listener is PostActionsListener.
 protected $listen = [ 'App\Events\PostPublishedEvent' => [ 'App\Listeners\PostActionsListener', ], ]; 

Then go to the console and run the command
 php artisan event:generate 

The command will create classes for the app / Events / PostPublishedEvent.php event and its listener app / Listeners / PostActionsListener.php
First we edit the event class, we will transfer a copy of our blog post to it.
 public $post; /** * PostPublishedEvent constructor. * @param Post $post */ public function __construct(Post $post) { $this->post = $post; } 

Hereinafter, the code does not forget to connect classes.
 use App\Models\Post; 

Now go to the app / Listeners / PostActionsListener.php listener.
I called him that way for a reason!
In order not to produce listeners for each type of event (I think there will not be many of them), I decided to start one.
We will decide what exactly to execute on the basis of which instance of the event came.
Like that
 /** * Handle the event. * * @param Event $event * @return void */ public function handle(Event $event) { if ($event instanceof PostPublishedEvent) { //   } } 

Now it remains to somehow make our PostPublishedEvent event happen. I suggest while doing this while saving the model.
In our case, the article may have 2 statuses (status field) Draft / Published .
Statuses I usually do class constants. In this case, they look like this:
 const STATUS_DRAFT = 0; const STATUS_PUBLISHED = 1; 

When you change the status to "Published" and you need to send notifications.
In order to make sure that this process will occur once, we will add an additional column, the flag that the notification on this post has been sent.
Add an additional field notify_status, its values ​​can be the same as for status.
Run in console:
 php artisan make:migration add_noty_status_to_post_table --table=post 

The created migration will be edited in the following way:
 public function up() { Schema::table('post', function (Blueprint $table) { $table->tinyInteger('notify_status')->default(0); }); } 

Perform in php artisan migrate console php artisan migrate

Event call


Now everything is ready to trigger the event itself.
To catch the process of saving the model in Laravel there are specially trained (again) events .
Let's get a static boot method in the Post model And add a listener to it on the save event, explained in the comments:
 public static function boot() { static::saving(function($instance) { //    –   «»,    ,     «» if ($instance->status == self::STATUS_PUBLISHED && $instance->notify_status < self::STATUS_PUBLISHED){ //     «» $instance->notify_status = self::STATUS_PUBLISHED; // «»  PostPublishedEvent,     . \Event::fire(new PostPublishedEvent($instance)); }); parent::boot(); } 

Tests


It's time to write the first test!
We need to test: firstly, that the necessary event occurs under the right conditions, and secondly, that the event does not occur when it is not necessary (status = draft for example)
If you read the article The First App on Laravel. Walkthrough (Part 1)
You already know about model factories, and how they are useful for testing. Create your own factory for the Post model
file database / factories / PostFactory.php:
 $factory->define(App\Models\Post::class, function (Faker\Generator $faker) { return [ 'title' => $faker->text(100), 'publish_date' => date('Ymd H:i'), 'short_text' => $faker->text(300), 'full_text' => $faker->realText(1000), 'slug' => str_random(50), 'status' => \App\Models\Post::STATUS_PUBLISHED, 'category_id' => 1 ]; }); 

And the tests / PostCreateTest.php test itself with one method so far:
 class PostCreateTest extends TestCase { public function testPublishEvent() { //,    \App\Events\PostPublishedEvent $this -> expectsEvents(\App\Events\PostPublishedEvent::class); //       $post = factory(App\Models\Post::class)->create(); //      $this -> seeInDatabase('post', ['title' => $post->title]); //  $post -> delete(); } } 

Please note: when testing events, the events themselves do not occur. Only the fact of their occurrence or non-occurrence is recorded.

Run phpunit. Everything should be fine OK (1 test, 1 assertion)
Now we add a reverse check that the event does not occur, for example, on drafts:
 public function testNoPublishEvent() { $this->doesntExpectEvents(\App\Events\PostPublishedEvent::class); //     –  status. $post = factory(App\Models\Post::class)->create( [ 'status' => App\Models\Post::STATUS_DRAFT ]); $this->seeInDatabase('post', ['title' => $post->title]); $post->delete(); } 

Run phpunit: OK (2 tests, 2 assertions)

Handling events, sending push notifications


Only trifles remained, just to handle the event and send push notifications through onesignal.com.
Go to the service website and smoke the REST API manual.
We are interested in the procedure for sending a message .
All parameters are described in detail, sample code is.

Instead of using curl_ * functions, I’ll install the anlutro / curl package wrapper I’m familiar with.
In the composer require anlutro/curl console, composer require anlutro/curl
All the sending procedure will be issued as a separate handler app / Handlers / OneSignalHandler.php: Here is his code completely. In the comments I will describe what's what
 <?php namespace App\Handlers; use anlutro\cURL\cURL; use App\Models\Post; class OneSignalHandler { //   private $test = false; //    " " public function __construct($test=false) { $this->test = $test; } // sendNotify     . public function sendNotify(Post $post) { //   $config = \Config::get('onesignal'); // app_id  ,   if (!empty($config['app_id'])) { //C    $data = array( 'app_id' => $config['app_id'], 'contents' => [ "en" => $post->short_text ], 'headings' => [ "en" => $post->title ], //(   WebPush ) 'isAnyWeb' => true, 'chrome_web_icon' => $config['icon_url'], 'firefox_icon' => $config['icon_url'], 'url' => $post->link ); //  test == true       , if ($this->test) { $data['include_player_ids'] = [$config['own_player_id']]; } else { //  -  . $data['included_segments'] = ["All"]; } //  !  ! if (strtotime($post->publish_date) > time()) { $data['send_after'] = date(DATE_RFC2822, strtotime($post->publish_date)); $data['delayed_option'] = 'timezone'; $data['delivery_time_of_day'] = '10:00AM'; } $curl = new cURL(); $req = $curl->newJsonRequest('post',$config['url'], $data)->setHeader('Authorization', 'Basic '.$config['api_key']); $result = $req->send(); //  ,    . if ($result->statusCode <> 200) { \Log::error('Unable to push to Onesignal', ['error' => $result->body]); return false; } $result = json_decode($result->body); if ($result->id) { //   -  - . return $result->recipients; } } } } 

Settings


To store the settings for onesignal I created a config
config / onesignal.php
 <?php return [ 'app_id' => env('ONESIGNAL_APP_ID',''), 'api_key' => env('ONESIGNAL_API_KEY',''), 'url' => env('ONESIGNAL_URL','https://onesignal.com/api/v1/notifications'), 'icon_url' => env('ONESIGNAL_ICON_URL',''), 'own_player_id' => env('ONESIGNAL_OWN_PLAYER_ID','') ]; 

The settings themselves in .env
 ONESIGNAL_APP_ID = 256aa8d2…. ONESIGNAL_API_KEY = YWR….. ONESIGNAL_ICON_URL = http://laravel-news.ru/images/laravel_logo_80.jpg ONESIGNAL_URL = https://onesignal.com/api/v1/notifications ONESIGNAL_OWN_PLAYER_ID = 830… 

'Own_player_id' appears in the config
This is my subscriber ID from admin. I need it for tests to send a notification only to myself.

Testing


Sending is ready - it's time to test it. This is very easy to do, since we have set the correct architecture and the process of sending an article is essentially isolated.
Let's add this method to our test:
 public function testSendOnesignal() { //      (   ) $post = factory(App\Models\Post::class)->make(); //     test = true $handler = new \App\Handlers\OneSignalHandler(true); //   $result = $handler->sendNotify($post); //  1,     . $this->assertEquals(1,$result); } 

In the phpunit console - the test passes successfully and a notification pops up (sometimes there are delays of up to several minutes)
If the test fails, look at the log and correct what the service does not like

Final chord


It remains only to add a call to the listener.
 /** * Handle the event. * * @param Event $event * @return void */ public function handle(Event $event) { if ($event instanceof PostPublishedEvent) { (new OneSignalHandler())->sendNotify($event->post); } } 

note


That's all for now, but our code has several disadvantages:
1) we are sending in real time when the model is saved, if heavier and slower operations are added, the saving will not reach and everything will fall.
2) when recording the sending status, we do not take into account the response of the service, if the service refuses to send, we will consider the article processed and we will not try to send more notifications on it.
Therefore, I do not recommend using this solution on the production server.
We will correct these shortcomings in future lessons. Wait for the continuation (spoiler in the first comment :)

UPD


Continuing the article Working with events in Laravel. Asynchronous queue processing.

')

Source: https://habr.com/ru/post/279385/


All Articles