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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.1]

### Added

- Plugin manifest integration for automated configuration installation
- `manifest()` method to `NotificationPlugin` implementing `ManifestInterface`
- Automatic installation of `config/notification.php` configuration file
- Automatic installation of database migrations
- Automatic bootstrap file configuration loading
- GitHub star repository prompt support

### Changed

- Plugin now uses manifest system for configuration setup
- Configuration files are installed via `bin/cake manifest install --plugin Crustum/Notification`

## [1.0.0]

### Added

- Multi-channel notification system supporting email, database, and extensible custom channels
- NotificationManager and NotificationSender for sending notifications via NotifiableBehavior or manager
- Database notification storage with migrations, entity, and table for displaying notifications in web interface
- Queue support with ShouldQueueInterface for async notification delivery via CakePHP Queue
- Testing utilities with NotificationTrait for comprehensive test assertions and custom channel registry
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"cakephp/cakephp": "^5.1",
"cakephp/authentication": "*",
"cakephp/authorization": "*",
"cakephp/queue": "^2.2"
"cakephp/queue": "^2.2",
"crustum/plugin-manifest": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
Expand Down
24 changes: 24 additions & 0 deletions config/notification.php.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);

/**
* Notification Plugin Configuration
*
* This file contains the default configuration for the Notification plugin.
* You can override these settings in your application's config/app.php file.
*/

return [
'Notification' => [
'channels' => [
'database' => [
'className' => 'Crustum/Notification.Database',
],
'mail' => [
'className' => 'Crustum/Notification.Mail',
'profile' => env('NOTIFICATION_MAIL_PROFILE', 'default'),
],
],
],
];

29 changes: 29 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Notifications

- [Introduction](#introduction)
- [Quickstart](#quickstart)
- [Generating Notifications](#generating-notifications)
- [Sending Notifications](#sending-notifications)
- [Using the Notifiable Behavior](#using-the-notifiable-behavior)
Expand Down Expand Up @@ -38,6 +39,34 @@ The CakePHP Notification plugin provides support for sending notifications acros

Typically, notifications should be short, informational messages that notify users of something that occurred in your application. For example, if you are writing a billing application, you might send an "Invoice Paid" notification to your users via the email and SMS channels.


<a name="quickstart"></a>
## Quickstart

### Installing the Plugin

Install via Composer:

```bash
composer require crustum/notification
```

> [!NOTE]
> This plugin should be registered in your `config/plugins.php` file.

```bash
bin/cake plugin load Crustum/Notification
```

> [!TIP]
> **After the plugin registers itself**, it's recommended to install the configuration with the manifest system:

```bash
bin/cake manifest install --plugin Crustum/Notification
```

The Notification plugin will create the `config/notification.php` configuration file where you may register your application's notification channels. Additionally, it will copy the migrations to the application's migrations directory and append the loading of the `config/notification.php` file to the `config/bootstrap.php` file.

<a name="generating-notifications"></a>
## Generating Notifications

Expand Down
8 changes: 7 additions & 1 deletion src/Channel/DatabaseChannel.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ public function send(EntityInterface|AnonymousNotifiable $notifiable, Notificati

$entity = $notificationsTable->newEntity($this->buildPayload($notifiable, $notification));

return $notificationsTable->save($entity);
$saved = $notificationsTable->save($entity);

if ($saved && $saved->id && $saved->id !== $notification->getId()) {
$notification->setId($saved->id);
}

return $saved;
}

/**
Expand Down
34 changes: 33 additions & 1 deletion src/NotificationPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
use Cake\Core\BasePlugin;
use Cake\Core\PluginApplicationInterface;
use Crustum\Notification\Command\NotificationCommand;
use Crustum\PluginManifest\Manifest\ManifestInterface;
use Crustum\PluginManifest\Manifest\ManifestTrait;

/**
* Plugin for Notification
*
* @uses \Crustum\PluginManifest\Manifest\ManifestTrait
*/
class NotificationPlugin extends BasePlugin
class NotificationPlugin extends BasePlugin implements ManifestInterface
{
use ManifestTrait;

/**
* Load all the plugin configuration and bootstrap logic.
*
Expand All @@ -39,4 +45,30 @@ public function console(CommandCollection $commands): CommandCollection

return $commands;
}

/**
* Get the manifest for the plugin.
*
* @return array<int, array<string, mixed>>
*/
public static function manifest(): array
{
$pluginPath = dirname(__DIR__);

return array_merge(
static::manifestMigrations(
$pluginPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Migrations',
),
static::manifestConfig(
$pluginPath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'notification.php.example',
CONFIG . 'notification.php',
false,
),
static::manifestBootstrapAppend(
"if (file_exists(CONFIG . 'notification.php')) {\n Configure::load('notification', 'default');\n}",
'// Notification Plugin Configuration',
),
static::manifestStarRepo('Crustum/Notification'),
);
}
}
17 changes: 13 additions & 4 deletions src/NotificationSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Cake\Utility\Inflector;
use Cake\Utility\Text;
use Crustum\Notification\Job\SendQueuedNotificationJob;
use Crustum\Notification\Model\Entity\Notification as NotificationEntity;
use Throwable;

/**
Expand Down Expand Up @@ -91,9 +92,15 @@ public function sendNow(EntityInterface|AnonymousNotifiable|iterable $notifiable
$this->preferredLocale($notifiable, $notification),
function () use ($viaChannels, $notifiable, $original): void {
$notificationId = Text::uuid();
$actualNotificationId = $notificationId;

foreach ((array)$viaChannels as $channel) {
$this->sendToNotifiable($notifiable, $notificationId, clone $original, $channel);
$notificationClone = clone $original;
$response = $this->sendToNotifiable($notifiable, $actualNotificationId, $notificationClone, $channel);

if ($channel === 'database' && $response instanceof NotificationEntity && $response->id) {
$actualNotificationId = $response->id;
}
}
},
);
Expand All @@ -107,16 +114,16 @@ function () use ($viaChannels, $notifiable, $original): void {
* @param string $id Unique notification ID
* @param \Crustum\Notification\Notification $notification The notification instance
* @param string $channel The channel name
* @return void
* @return \Crustum\Notification\Model\Entity\Notification|null The saved notification entity, or null if not saved
*/
protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifiable, string $id, Notification $notification, string $channel): void
protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifiable, string $id, Notification $notification, string $channel): mixed
{
if (!$notification->getId()) {
$notification->setId($id);
}

if (!$this->shouldSendNotification($notifiable, $notification, $channel)) {
return;
return null;
}

try {
Expand All @@ -129,6 +136,8 @@ protected function sendToNotifiable(EntityInterface|AnonymousNotifiable $notifia
'channel' => $channel,
'response' => $response,
],);

return $response;
} catch (Throwable $exception) {
$this->dispatchEvent('Model.Notification.failed', [
'notifiable' => $notifiable,
Expand Down