Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.2, 8.3, 8.4]
laravel: ['11.*', '12.*']
php: [8.3, 8.4]
laravel: ["12.*"]
stability: [prefer-lowest, prefer-stable]

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This package makes it easy to send notifications using [Firebase Cloud Messaging
- [Setting up the FCM service](#setting-up-the-fcm-service)
- [Usage](#usage)
- [Available message methods](#available-message-methods)
- [Topic notifications](#topic-notifications)
- [Custom clients](#custom-clients)
- [Handling errors](#handling-errors)
- [Changelog](#changelog)
Expand Down Expand Up @@ -156,6 +157,17 @@ FcmMessage::create()
->custom(['notification' => []]);
```

## Topic notifications

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.

```php
use NotificationChannels\Fcm\FcmTopicChannel;

Notification::route(FcmTopicChannel::class, 'news')
->notify(new BlogCreated($blog));
```

## Custom clients

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.
Expand Down
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@
}
],
"require": {
"php": "^8.2",
"php": "^8.3",
"guzzlehttp/guzzle": "^7.0",
"illuminate/notifications": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"illuminate/notifications": "^12.0",
"illuminate/support": "^12.0",
"kreait/laravel-firebase": "^6.0"
},
"require-dev": {
"laravel/pint": "^1.25",
"mockery/mockery": "^1.6.0",
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": {
Expand Down
12 changes: 6 additions & 6 deletions src/FcmChannel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@
class FcmChannel
{
/**
* The maximum number of tokens we can use in a single request
*
* @var int
* The maximum number of tokens we can use in a single request.
*/
const TOKENS_PER_REQUEST = 500;
const int TOKENS_PER_REQUEST = 500;

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

$fcmMessage = $notification->toFcm($notifiable);
$message = $notification->toFcm($notifiable);

$client = $message->client ?? $this->client;

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

Expand Down
21 changes: 7 additions & 14 deletions src/FcmMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(
public ?string $topic = null,
public ?string $condition = null,
public ?array $data = [],
public array $custom = [],
public ?array $custom = [],
public ?Notification $notification = null,
public ?Messaging $client = null,
) {
Expand Down Expand Up @@ -81,7 +81,7 @@ public function condition(?string $condition): self
*/
public function data(?array $data): self
{
if (! empty(array_filter($data, fn($value) => ! is_string($value)))) {
if (! empty(array_filter($data, fn ($value) => ! is_string($value)))) {
throw new InvalidArgumentException('Data values must be strings.');
}

Expand All @@ -103,26 +103,19 @@ public function custom(?array $custom = []): self
/**
* Set Aandroid specific custom options.
*/
public function android(array $options = []): self
public function android(?array $options = []): self
{
$this->custom([
...$this->custom,
'android' => $options,
]);
$this->custom([...$this->custom, 'android' => $options]);

return $this;
}

/**
* Set APNs-specific custom options.
*/
public function ios(array $options = []): self
public function ios(?array $options = []): self
{
$this->custom([
...$this->custom,
'apns' => $options,
'apns' => $options,
]);
$this->custom([...$this->custom, 'apns' => $options]);

return $this;
}
Expand Down Expand Up @@ -150,7 +143,7 @@ public function usingClient(Messaging $client): self
/**
* Get the array represenation of the message.
*/
public function toArray()
public function toArray(): array
{
return array_filter([
'name' => $this->name,
Expand Down
59 changes: 59 additions & 0 deletions src/FcmTopicChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace NotificationChannels\Fcm;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Notifications\Events\NotificationFailed;
use Illuminate\Notifications\Notification;
use Kreait\Firebase\Contract\Messaging;
use Kreait\Firebase\Exception\MessagingException;

class FcmTopicChannel
{
/**
* Create a new channel instance.
*/
public function __construct(protected Dispatcher $events, protected Messaging $client)
{
//
}

/**
* Send the given notification.
*/
public function send(mixed $notifiable, Notification $notification): ?array
{
$message = $notification->toFcm($notifiable);

$topic = $notifiable instanceof AnonymousNotifiable
? $notifiable->routeNotificationFor('fcm-topic')
: $message->topic;

if (empty($topic)) {
return null;
}

$message->topic($topic);

$client = $message->client ?? $this->client;

try {
return $client->send($message);
} catch (MessagingException $e) {
$this->dispatchFailedNotification($notifiable, $notification, $e);

return null;
}
}

/**
* Dispatch failed event.
*/
protected function dispatchFailedNotification(mixed $notifiable, Notification $notification, MessagingException $exception): void
{
$this->events->dispatch(new NotificationFailed($notifiable, $notification, self::class, [
'exception' => $exception,
]));
}
}
3 changes: 0 additions & 3 deletions src/Resources/FcmResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

abstract class FcmResource
{
/**
* @return static
*/
public static function create(...$args): static
{
return new static(...$args);
Expand Down
10 changes: 5 additions & 5 deletions tests/FcmChannelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@

class FcmChannelTest extends TestCase
{
public function tearDown(): void
protected function tearDown(): void
{
Mockery::close();
}

public function test_it_can_be_instantiated()
public function test_it_can_be_instantiated(): void
{
$events = Mockery::mock(Dispatcher::class);
$firebase = Mockery::mock(Messaging::class);
Expand All @@ -33,7 +33,7 @@ public function test_it_can_be_instantiated()
$this->assertInstanceOf(FcmChannel::class, $channel);
}

public function test_it_can_send_notifications()
public function test_it_can_send_notifications(): void
{
$events = Mockery::mock(Dispatcher::class);
$events->shouldNotReceive('dispatch');
Expand All @@ -53,7 +53,7 @@ public function test_it_can_send_notifications()
$this->assertInstanceOf(MulticastSendReport::class, $result->first());
}

public function test_it_can_send_notifications_with_custom_client()
public function test_it_can_send_notifications_with_custom_client(): void
{
$events = Mockery::mock(Dispatcher::class);
$events->shouldNotReceive('dispatch');
Expand All @@ -75,7 +75,7 @@ public function test_it_can_send_notifications_with_custom_client()
$this->assertInstanceOf(Collection::class, $result);
}

public function test_it_can_dispatch_events()
public function test_it_can_dispatch_events(): void
{
$events = Mockery::mock(Dispatcher::class);
$events->shouldReceive('dispatch')->once();
Expand Down
28 changes: 14 additions & 14 deletions tests/FcmMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class FcmMessageTest extends TestCase
{
public function test_it_can_be_instantiated()
public function test_it_can_be_instantiated(): void
{
$message = new FcmMessage(name: 'name');

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

public function test_it_can_be_created()
public function test_it_can_be_created(): void
{
$message = FcmMessage::create(name: 'name');

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

public function test_it_can_set_name()
public function test_it_can_set_name(): void
{
$message = FcmMessage::create()->name('name');

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

public function test_it_can_set_token()
public function test_it_can_set_token(): void
{
$message = FcmMessage::create()->token('token');

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

public function test_it_can_set_topic()
public function test_it_can_set_topic(): void
{
$message = FcmMessage::create()->topic('topic');

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

public function test_it_can_set_condition()
public function test_it_can_set_condition(): void
{
$message = FcmMessage::create()->condition('condition');

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

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

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

public function test_it_throws_exception_on_invalid_data()
public function test_it_throws_exception_on_invalid_data(): void
{
$this->expectException(\InvalidArgumentException::class);

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

public function test_it_can_set_custom_attributes()
public function test_it_can_set_custom_attributes(): void
{
$message = FcmMessage::create()
->name('name')
Expand All @@ -90,7 +90,7 @@ public function test_it_can_set_custom_attributes()
$this->assertEquals($expected, $message->toArray());
}

public function test_it_can_set_notification()
public function test_it_can_set_notification(): void
{
$notification = Notification::create()->title('title');

Expand All @@ -101,7 +101,7 @@ public function test_it_can_set_notification()
], $message->toArray());
}

public function test_it_can_set_client()
public function test_it_can_set_client(): void
{
$client = Mockery::mock(Messaging::class);

Expand All @@ -110,7 +110,7 @@ public function test_it_can_set_client()
$this->assertSame($client, $message->client);
}

public function test_appends_android_options_into_custom()
public function test_appends_android_options_into_custom(): void
{
$message = FcmMessage::create()
->notification(new Notification(title: 'title', body: 'body'))
Expand All @@ -123,7 +123,7 @@ public function test_appends_android_options_into_custom()
$this->assertEquals('channel_id', $payload['android']['notification']['channel_id']);
}

public function test_appends_ios_options_into_custom()
public function test_appends_ios_options_into_custom(): void
{
$message = FcmMessage::create()
->ios(['payload' => ['aps' => ['sound' => 'sound']]]);
Expand All @@ -135,7 +135,7 @@ public function test_appends_ios_options_into_custom()
$this->assertEquals('sound', $payload['apns']['payload']['aps']['sound']);
}

public function test_preserves_existing_custom_keys_when_using_helpers()
public function test_preserves_existing_custom_keys_when_using_helpers(): void
{
$message = FcmMessage::create()
->custom(['meta' => ['a' => 1]])
Expand Down
Loading