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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php-version: ['8.1', '8.2', '8.3', '8.4']
php-version: ['8.2', '8.3', '8.4']
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -44,11 +44,11 @@ jobs:
vendor/bin/phpunit --configuration=phpunit.xml --display-deprecations --display-incomplete --display-skipped --coverage-clover=coverage.xml

- name: Run PHPUnit without coverage
if: matrix.php-version != '8.2' || matrix.os != 'ubuntu-latest'
if: matrix.php-version != '8.3' || matrix.os != 'ubuntu-latest'
run: vendor/bin/phpunit --configuration=phpunit.xml

- name: Submit code coverage
if: matrix.php-version == '8.2' && matrix.os == 'ubuntu-latest'
if: matrix.php-version == '8.3' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3

cs-stan:
Expand All @@ -61,7 +61,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: '8.3'
extensions: mbstring, intl, pdo_sqlite
coverage: none
tools: phive
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ 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.1.0]

### Added

- `NotifiableTrait` for providing notification methods on table classes

### Changed

- **BREAKING**: Refactored `NotifiableBehavior` - notification methods moved to `NotifiableTrait`
- Behavior now only creates the `Notifications` association
- Table classes must use both `NotifiableTrait` and `NotifiableBehavior`
- See updated documentation for migration guide

## [1.0.1]

### Added
Expand Down
12 changes: 10 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"crustum/plugin-manifest": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.5.0",
"ratchet/pawl": "^0.4",
"cakephp/bake": "^3.0",
"react/http": "^1.7",
Expand All @@ -45,13 +45,21 @@
}
},
"scripts": {
"check": [
"@cs-check",
"@test",
"@analyse"
],
"analyse": [
"@stan"
],
"cs-check": "phpcs -p --standard=phpcs.xml src/ tests/",
"cs-fix": "phpcbf --standard=phpcs.xml src/ tests/",
"test": "phpunit",
"test:unit": "phpunit --testsuite=Unit",
"test:integration": "phpunit --testsuite=Integration",
"test:coverage": "phpunit --coverage-html coverage",
"stan": "tools/phpstan analyse src/",
"stan": "tools/phpstan analyse src/ tests/",
"psalm": "tools/psalm --show-info=false",
"stan-tests": "tools/phpstan analyze -c tests/phpstan.neon",
"stan-baseline": "tools/phpstan --generate-baseline"
Expand Down
9 changes: 6 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,19 @@ bin/cake bake notification InvoicePaid --channels database,mail,slack
<a name="using-the-notifiable-behavior"></a>
### Using the Notifiable Behavior

Notifications may be sent in two ways: using the `notify()` method provided by the `NotifiableBehavior` or using the `NotificationManager`. The `NotifiableBehavior` must be added to your Table class:
Notifications may be sent in two ways: using the `notify()` method provided by the `NotifiableTrait` or using the `NotificationManager`. To enable notifications on a table, you must use both the `NotifiableTrait` and add the `NotifiableBehavior`:

```php
<?php
namespace App\Model\Table;

use Cake\ORM\Table;
use Crustum\Notification\Model\Trait\NotifiableTrait;

class UsersTable extends Table
{
use NotifiableTrait;

public function initialize(array $config): void
{
parent::initialize($config);
Expand All @@ -111,7 +114,7 @@ class UsersTable extends Table
}
```

The `notify()` method that is provided by this behavior is called on the table and receives the entity and notification instance:
The `notify()` method that is provided by the trait is called on the table and receives the entity and notification instance:

```php
use App\Notification\InvoicePaid;
Expand All @@ -123,7 +126,7 @@ $usersTable->notify($user, new InvoicePaid($invoice));
```

> [!NOTE]
> You may add the `Notifiable` behavior to any of your tables. You are not limited to only including it on your `Users` table.
> You may add the `NotifiableTrait` and `NotifiableBehavior` to any of your tables. You are not limited to only including it on your `Users` table.

<a name="using-the-notificationmanager"></a>
### Using the NotificationManager
Expand Down
3 changes: 0 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,5 @@ parameters:
ignoreErrors:
-
identifier: missingType.generics
-
identifier: trait.unused
path: src/TestSuite/NotificationTrait.php

services:
173 changes: 12 additions & 161 deletions src/Model/Behavior/NotifiableBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@

namespace Crustum\Notification\Model\Behavior;

use Cake\Datasource\EntityInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Query\SelectQuery;
use Crustum\Notification\Notification;
use Crustum\Notification\NotificationManager;

/**
* Notifiable Behavior
*
* Makes an entity/model notifiable by automatically creating association to Notifications table
* and providing methods to send notifications.
* Creates hasMany association to Notifications table. For methods, use NotifiableTrait.
*
* Usage:
* ```
* // In your UsersTable
* $this->addBehavior('Crustum/Notification.Notifiable');
* use Crustum\Notification\Model\Trait\NotifiableTrait;
*
* // Then use on entities
* $user->notify(new WelcomeNotification());
* class UsersTable extends Table
* {
* use NotifiableTrait;
*
* public function initialize(array $config): void
* {
* $this->addBehavior('Crustum/Notification.Notifiable');
* }
* }
* ```
*
* @property \Cake\ORM\Table $_table
Expand All @@ -31,22 +32,9 @@ class NotifiableBehavior extends Behavior
/**
* Default configuration
*
* Configuration options:
* - implementedMethods: Methods to expose on the table/entity
*
* @var array<string, mixed>
*/
protected array $_defaultConfig = [
'implementedMethods' => [
'notify' => 'notify',
'notifyNow' => 'notifyNow',
'routeNotificationFor' => 'routeNotificationFor',
'markNotificationAsRead' => 'markNotificationAsRead',
'markAllNotificationsAsRead' => 'markAllNotificationsAsRead',
'unreadNotifications' => 'unreadNotifications',
'readNotifications' => 'readNotifications',
],
];
protected array $_defaultConfig = [];

/**
* Initialize hook
Expand All @@ -71,141 +59,4 @@ public function initialize(array $config): void
'cascadeCallbacks' => true,
]);
}

/**
* Send a notification to the given entity
*
* The notification will be sent through all channels defined in the notification's via() method.
* If the notification implements ShouldQueueInterface, it will be queued for async processing.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to notify
* @param \Crustum\Notification\Notification $notification The notification instance
* @return void
*/
public function notify(EntityInterface $entity, Notification $notification): void
{
NotificationManager::send($entity, $notification);
}

/**
* Send a notification immediately, bypassing the queue
*
* The notification will be sent immediately through the specified channels,
* even if it implements ShouldQueueInterface.
*
* @param \Cake\Datasource\EntityInterface $entity The entity to notify
* @param \Crustum\Notification\Notification $notification The notification instance
* @param array<string>|null $channels Optional array of channel names to send through
* @return void
*/
public function notifyNow(EntityInterface $entity, Notification $notification, ?array $channels = null): void
{
NotificationManager::sendNow($entity, $notification, $channels);
}

/**
* Get the routing information for a given notification channel
*
* This method checks for a specific routing method on the entity first,
* then falls back to default routing for known channels.
*
* @param object $entity The entity to get routing info for
* @param string $channel The channel name (e.g., 'database', 'mail', 'slack')
* @param \Crustum\Notification\Notification|null $notification The notification instance
* @return mixed Routing information for the channel
*/
public function routeNotificationFor(object $entity, string $channel, ?Notification $notification = null): mixed
{
$method = 'routeNotificationFor' . ucfirst($channel);

if (method_exists($entity, $method)) {
return $entity->{$method}($notification);
}

if ($channel === 'database') {
return $this->_table->getAssociation('Notifications');
}

return null;
}

/**
* Mark a notification as read for this entity
*
* @param object $entity The entity
* @param string $notificationId The notification ID
* @return bool True if marked as read
*/
public function markNotificationAsRead(object $entity, string $notificationId): bool
{
/** @var \Crustum\Notification\Model\Table\NotificationsTable $notificationsTable */
$notificationsTable = $this->_table->getAssociation('Notifications')->getTarget();

return $notificationsTable->markAsRead($notificationId);
}

/**
* Mark all notifications as read for this entity
*
* @param \Cake\Datasource\EntityInterface $entity The entity
* @return int Number of notifications marked as read
*/
public function markAllNotificationsAsRead(EntityInterface $entity): int
{
/** @var \Crustum\Notification\Model\Table\NotificationsTable $notificationsTable */
$notificationsTable = $this->_table->getAssociation('Notifications')->getTarget();
$primaryKey = $this->_table->getPrimaryKey();
if (is_array($primaryKey)) {
$primaryKey = $primaryKey[0];
}

return $notificationsTable->markAllAsRead(
$this->_table->getAlias(),
(string)$entity->get($primaryKey),
);
}

/**
* Get unread notifications query for this entity
*
* @param \Cake\Datasource\EntityInterface $entity The entity
* @return \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification>
* @phpstan-return \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification>
*/
public function unreadNotifications(EntityInterface $entity): SelectQuery
{
/** @var \Crustum\Notification\Model\Table\NotificationsTable $notificationsTable */
$notificationsTable = $this->_table->getAssociation('Notifications')->getTarget();
$primaryKey = $this->_table->getPrimaryKey();
if (is_array($primaryKey)) {
$primaryKey = $primaryKey[0];
}

/** @phpstan-var \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification> */
return $notificationsTable
->find('forModel', model: $this->_table->getAlias(), foreign_key: (string)$entity->get($primaryKey))
->find('unread');
}

/**
* Get read notifications query for this entity
*
* @param \Cake\Datasource\EntityInterface $entity The entity
* @return \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification>
* @phpstan-return \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification>
*/
public function readNotifications(EntityInterface $entity): SelectQuery
{
/** @var \Crustum\Notification\Model\Table\NotificationsTable $notificationsTable */
$notificationsTable = $this->_table->getAssociation('Notifications')->getTarget();
$primaryKey = $this->_table->getPrimaryKey();
if (is_array($primaryKey)) {
$primaryKey = $primaryKey[0];
}

/** @phpstan-var \Cake\ORM\Query\SelectQuery<\Crustum\Notification\Model\Entity\Notification> */
return $notificationsTable
->find('forModel', model: $this->_table->getAlias(), foreign_key: (string)$entity->get($primaryKey))
->find('read');
}
}
Loading