Skip to content

Commit fbe808b

Browse files
committed
Introduce FcmTopicChannel
1 parent 3347e87 commit fbe808b

File tree

9 files changed

+292
-42
lines changed

9 files changed

+292
-42
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This package makes it easy to send notifications using [Firebase Cloud Messaging
1414
- [Setting up the FCM service](#setting-up-the-fcm-service)
1515
- [Usage](#usage)
1616
- [Available message methods](#available-message-methods)
17+
- [Topic notifications](#topic-notifications)
1718
- [Custom clients](#custom-clients)
1819
- [Handling errors](#handling-errors)
1920
- [Changelog](#changelog)
@@ -156,6 +157,17 @@ FcmMessage::create()
156157
->custom(['notification' => []]);
157158
```
158159

160+
## Topic notifications
161+
162+
You can also send FCM notifications to topics with the `FcmTopicChannel`. Use an on-demand notification to to route the notification to the topic, or you can set the topic on the message itself in the `toFcm` method.
163+
164+
```php
165+
use NotificationChannels\Fcm\FcmTopicChannel;
166+
167+
Notification::route(FcmTopicChannel::class, 'news')
168+
->notify(new BlogCreated($blog));
169+
```
170+
159171
## Custom clients
160172

161173
You can change the underlying Firebase Messaging client on the fly if required. Provide an instance of `Kreait\Firebase\Contract\Messaging` to your FCM message and it will be used in place of the default client.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"kreait/laravel-firebase": "^6.0"
2020
},
2121
"require-dev": {
22+
"laravel/pint": "^1.25",
2223
"mockery/mockery": "^1.6.0",
2324
"phpunit/phpunit": "^11.0"
2425
},

src/FcmChannel.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
class FcmChannel
1515
{
1616
/**
17-
* The maximum number of tokens we can use in a single request
18-
*
19-
* @var int
17+
* The maximum number of tokens we can use in a single request.
2018
*/
21-
const TOKENS_PER_REQUEST = 500;
19+
const int TOKENS_PER_REQUEST = 500;
2220

2321
/**
2422
* Create a new channel instance.
@@ -39,11 +37,13 @@ public function send(mixed $notifiable, Notification $notification): ?Collection
3937
return null;
4038
}
4139

42-
$fcmMessage = $notification->toFcm($notifiable);
40+
$message = $notification->toFcm($notifiable);
41+
42+
$client = $message->client ?? $this->client;
4343

4444
return Collection::make($tokens)
4545
->chunk(self::TOKENS_PER_REQUEST)
46-
->map(fn ($tokens) => ($fcmMessage->client ?? $this->client)->sendMulticast($fcmMessage, $tokens->all()))
46+
->map(fn ($tokens) => $client->sendMulticast($message, $tokens->all()))
4747
->map(fn (MulticastSendReport $report) => $this->checkReportForFailures($notifiable, $notification, $report));
4848
}
4949

src/FcmMessage.php

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __construct(
2121
public ?string $topic = null,
2222
public ?string $condition = null,
2323
public ?array $data = [],
24-
public array $custom = [],
24+
public ?array $custom = [],
2525
public ?Notification $notification = null,
2626
public ?Messaging $client = null,
2727
) {
@@ -81,7 +81,7 @@ public function condition(?string $condition): self
8181
*/
8282
public function data(?array $data): self
8383
{
84-
if (! empty(array_filter($data, fn($value) => ! is_string($value)))) {
84+
if (! empty(array_filter($data, fn ($value) => ! is_string($value)))) {
8585
throw new InvalidArgumentException('Data values must be strings.');
8686
}
8787

@@ -103,26 +103,19 @@ public function custom(?array $custom = []): self
103103
/**
104104
* Set Aandroid specific custom options.
105105
*/
106-
public function android(array $options = []): self
106+
public function android(?array $options = []): self
107107
{
108-
$this->custom([
109-
...$this->custom,
110-
'android' => $options,
111-
]);
108+
$this->custom([...$this->custom, 'android' => $options]);
112109

113110
return $this;
114111
}
115112

116113
/**
117114
* Set APNs-specific custom options.
118115
*/
119-
public function ios(array $options = []): self
116+
public function ios(?array $options = []): self
120117
{
121-
$this->custom([
122-
...$this->custom,
123-
'apns' => $options,
124-
'apns' => $options,
125-
]);
118+
$this->custom([...$this->custom, 'apns' => $options]);
126119

127120
return $this;
128121
}
@@ -150,7 +143,7 @@ public function usingClient(Messaging $client): self
150143
/**
151144
* Get the array represenation of the message.
152145
*/
153-
public function toArray()
146+
public function toArray(): array
154147
{
155148
return array_filter([
156149
'name' => $this->name,

src/FcmTopicChannel.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace NotificationChannels\Fcm;
4+
5+
use Illuminate\Contracts\Events\Dispatcher;
6+
use Illuminate\Notifications\AnonymousNotifiable;
7+
use Illuminate\Notifications\Events\NotificationFailed;
8+
use Illuminate\Notifications\Notification;
9+
use Kreait\Firebase\Contract\Messaging;
10+
use Kreait\Firebase\Exception\MessagingException;
11+
12+
class FcmTopicChannel
13+
{
14+
/**
15+
* Create a new channel instance.
16+
*/
17+
public function __construct(protected Dispatcher $events, protected Messaging $client)
18+
{
19+
//
20+
}
21+
22+
/**
23+
* Send the given notification.
24+
*/
25+
public function send(mixed $notifiable, Notification $notification): ?array
26+
{
27+
$message = $notification->toFcm($notifiable);
28+
29+
$topic = $notifiable instanceof AnonymousNotifiable
30+
? $notifiable->routeNotificationFor('fcm-topic')
31+
: $message->topic;
32+
33+
if (empty($topic)) {
34+
return null;
35+
}
36+
37+
$message->topic($topic);
38+
39+
$client = $message->client ?? $this->client;
40+
41+
try {
42+
return $client->send($message);
43+
} catch (MessagingException $e) {
44+
$this->dispatchFailedNotification($notifiable, $notification, $e);
45+
46+
return null;
47+
}
48+
}
49+
50+
/**
51+
* Dispatch failed event.
52+
*/
53+
protected function dispatchFailedNotification(mixed $notifiable, Notification $notification, MessagingException $exception): void
54+
{
55+
$this->events->dispatch(new NotificationFailed($notifiable, $notification, self::class, [
56+
'exception' => $exception,
57+
]));
58+
}
59+
}

src/Resources/FcmResource.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
abstract class FcmResource
66
{
7-
/**
8-
* @return static
9-
*/
107
public static function create(...$args): static
118
{
129
return new static(...$args);

tests/FcmChannelTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818

1919
class FcmChannelTest extends TestCase
2020
{
21-
public function tearDown(): void
21+
protected function tearDown(): void
2222
{
2323
Mockery::close();
2424
}
2525

26-
public function test_it_can_be_instantiated()
26+
public function test_it_can_be_instantiated(): void
2727
{
2828
$events = Mockery::mock(Dispatcher::class);
2929
$firebase = Mockery::mock(Messaging::class);
@@ -33,7 +33,7 @@ public function test_it_can_be_instantiated()
3333
$this->assertInstanceOf(FcmChannel::class, $channel);
3434
}
3535

36-
public function test_it_can_send_notifications()
36+
public function test_it_can_send_notifications(): void
3737
{
3838
$events = Mockery::mock(Dispatcher::class);
3939
$events->shouldNotReceive('dispatch');
@@ -53,7 +53,7 @@ public function test_it_can_send_notifications()
5353
$this->assertInstanceOf(MulticastSendReport::class, $result->first());
5454
}
5555

56-
public function test_it_can_send_notifications_with_custom_client()
56+
public function test_it_can_send_notifications_with_custom_client(): void
5757
{
5858
$events = Mockery::mock(Dispatcher::class);
5959
$events->shouldNotReceive('dispatch');
@@ -75,7 +75,7 @@ public function test_it_can_send_notifications_with_custom_client()
7575
$this->assertInstanceOf(Collection::class, $result);
7676
}
7777

78-
public function test_it_can_dispatch_events()
78+
public function test_it_can_dispatch_events(): void
7979
{
8080
$events = Mockery::mock(Dispatcher::class);
8181
$events->shouldReceive('dispatch')->once();

tests/FcmMessageTest.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class FcmMessageTest extends TestCase
1212
{
13-
public function test_it_can_be_instantiated()
13+
public function test_it_can_be_instantiated(): void
1414
{
1515
$message = new FcmMessage(name: 'name');
1616

@@ -19,7 +19,7 @@ public function test_it_can_be_instantiated()
1919
$this->assertEquals('name', $message->name);
2020
}
2121

22-
public function test_it_can_be_created()
22+
public function test_it_can_be_created(): void
2323
{
2424
$message = FcmMessage::create(name: 'name');
2525

@@ -28,49 +28,49 @@ public function test_it_can_be_created()
2828
$this->assertEquals('name', $message->name);
2929
}
3030

31-
public function test_it_can_set_name()
31+
public function test_it_can_set_name(): void
3232
{
3333
$message = FcmMessage::create()->name('name');
3434

3535
$this->assertEquals(['name' => 'name'], $message->toArray());
3636
}
3737

38-
public function test_it_can_set_token()
38+
public function test_it_can_set_token(): void
3939
{
4040
$message = FcmMessage::create()->token('token');
4141

4242
$this->assertEquals(['token' => 'token'], $message->toArray());
4343
}
4444

45-
public function test_it_can_set_topic()
45+
public function test_it_can_set_topic(): void
4646
{
4747
$message = FcmMessage::create()->topic('topic');
4848

4949
$this->assertEquals(['topic' => 'topic'], $message->toArray());
5050
}
5151

52-
public function test_it_can_set_condition()
52+
public function test_it_can_set_condition(): void
5353
{
5454
$message = FcmMessage::create()->condition('condition');
5555

5656
$this->assertEquals(['condition' => 'condition'], $message->toArray());
5757
}
5858

59-
public function test_it_can_set_data()
59+
public function test_it_can_set_data(): void
6060
{
6161
$message = FcmMessage::create()->data(['a' => 'b']);
6262

6363
$this->assertEquals(['data' => ['a' => 'b']], $message->toArray());
6464
}
6565

66-
public function test_it_throws_exception_on_invalid_data()
66+
public function test_it_throws_exception_on_invalid_data(): void
6767
{
6868
$this->expectException(\InvalidArgumentException::class);
6969

7070
FcmMessage::create()->data(['a' => 1]);
7171
}
7272

73-
public function test_it_can_set_custom_attributes()
73+
public function test_it_can_set_custom_attributes(): void
7474
{
7575
$message = FcmMessage::create()
7676
->name('name')
@@ -90,7 +90,7 @@ public function test_it_can_set_custom_attributes()
9090
$this->assertEquals($expected, $message->toArray());
9191
}
9292

93-
public function test_it_can_set_notification()
93+
public function test_it_can_set_notification(): void
9494
{
9595
$notification = Notification::create()->title('title');
9696

@@ -101,7 +101,7 @@ public function test_it_can_set_notification()
101101
], $message->toArray());
102102
}
103103

104-
public function test_it_can_set_client()
104+
public function test_it_can_set_client(): void
105105
{
106106
$client = Mockery::mock(Messaging::class);
107107

@@ -110,7 +110,7 @@ public function test_it_can_set_client()
110110
$this->assertSame($client, $message->client);
111111
}
112112

113-
public function test_appends_android_options_into_custom()
113+
public function test_appends_android_options_into_custom(): void
114114
{
115115
$message = FcmMessage::create()
116116
->notification(new Notification(title: 'title', body: 'body'))
@@ -123,7 +123,7 @@ public function test_appends_android_options_into_custom()
123123
$this->assertEquals('channel_id', $payload['android']['notification']['channel_id']);
124124
}
125125

126-
public function test_appends_ios_options_into_custom()
126+
public function test_appends_ios_options_into_custom(): void
127127
{
128128
$message = FcmMessage::create()
129129
->ios(['payload' => ['aps' => ['sound' => 'sound']]]);
@@ -135,7 +135,7 @@ public function test_appends_ios_options_into_custom()
135135
$this->assertEquals('sound', $payload['apns']['payload']['aps']['sound']);
136136
}
137137

138-
public function test_preserves_existing_custom_keys_when_using_helpers()
138+
public function test_preserves_existing_custom_keys_when_using_helpers(): void
139139
{
140140
$message = FcmMessage::create()
141141
->custom(['meta' => ['a' => 1]])

0 commit comments

Comments
 (0)