Skip to content

Commit b5010b1

Browse files
committed
[Notify] Add Notify library
1 parent 57e0217 commit b5010b1

23 files changed

+1004
-0
lines changed

.github/workflows/test.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ jobs:
5454
cd src/LazyImage
5555
composer update --prefer-lowest --prefer-dist --no-interaction --no-ansi --no-progress
5656
php vendor/bin/simple-phpunit
57+
- name: Notify
58+
run: |
59+
cd src/Notify
60+
composer update --prefer-lowest --prefer-dist --no-interaction --no-ansi --no-progress
61+
php vendor/bin/simple-phpunit
5762
5863
tests-php-high-deps:
5964
runs-on: ubuntu-latest
@@ -86,6 +91,12 @@ jobs:
8691
composer config platform.php 7.4.99
8792
composer update --prefer-dist --no-interaction --no-ansi --no-progress
8893
php vendor/bin/simple-phpunit
94+
- name: Notify
95+
run: |
96+
cd src/Notify
97+
composer config platform.php 7.4.99
98+
composer update --prefer-dist --no-interaction --no-ansi --no-progress
99+
php vendor/bin/simple-phpunit
89100
90101
tests-js:
91102
runs-on: ubuntu-latest

src/Notify/.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.gitattributes export-ignore
2+
/.gitignore export-ignore
3+
/phpunit.xml.dist export-ignore
4+
/assets/test export-ignore
5+
/tests export-ignore

src/Notify/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/vendor/
2+
.phpunit.result.cache
3+
.php_cs.cache
4+
composer.lock
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Notify\DependencyInjection;
13+
14+
use Symfony\Component\Config\FileLocator;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
17+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
18+
19+
/**
20+
* @author Mathias Arlaud <[email protected]>
21+
*
22+
* @internal
23+
*/
24+
class NotifyExtension extends Extension
25+
{
26+
public function load(array $configs, ContainerBuilder $container)
27+
{
28+
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
29+
$loader->load('services.php');
30+
}
31+
}

src/Notify/NotifyBundle.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Notify;
13+
14+
use Symfony\Component\HttpKernel\Bundle\Bundle;
15+
16+
/**
17+
* @author Mathias Arlaud <[email protected]>
18+
*
19+
* @final
20+
* @experimental
21+
*/
22+
class NotifyBundle extends Bundle
23+
{
24+
}

src/Notify/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Symfony UX Notify
2+
3+
Symfony UX Notify is a Symfony bundle integrating realtime notifications in Symfony applications
4+
using [Mercure](https://mercure.rocks/).
5+
It is part of [the Symfony UX initiative](https://symfony.com/ux).
6+
7+
## Installation
8+
9+
Symfony UX Notify requires PHP 7.2+ and Symfony 5.3+.
10+
11+
Install this bundle using Composer and Symfony Flex:
12+
13+
```sh
14+
composer require symfony/ux-notify
15+
16+
# Don't forget to install the JavaScript dependencies as well and compile
17+
yarn install --force
18+
yarn encore dev
19+
```
20+
21+
## Usage
22+
23+
To use Symfony UX Notify you must have a [running Mercure server](https://symfony.com/doc/current/mercure.html#running-a-mercure-hub).
24+
25+
Then, inject the `NotifierInterface` service and send messages on the `chat/mercure` channel.
26+
27+
```php
28+
// ...
29+
use Symfony\Component\Notifier\Notification\Notification;
30+
use Symfony\Component\Notifier\NotifierInterface;
31+
32+
class AnnounceFlashSalesCommand extends Command
33+
{
34+
protected static $defaultName = 'app:flash-sales:announce';
35+
private $notifier;
36+
37+
public function __construct(NotifierInterface $notifier)
38+
{
39+
parent::__construct();
40+
41+
$this->notifier = $notifier;
42+
}
43+
44+
protected function execute(InputInterface $input, OutputInterface $output): int
45+
{
46+
$this->notifier->send(new Notification('Flash sales has been started!', ['chat/mercure']));
47+
48+
return 0;
49+
}
50+
}
51+
```
52+
53+
Finally, HTML5 notifications could be displayed using the `notify` Twig function:
54+
55+
```twig
56+
{{ stream_notifications(['/my/topic/1', '/my/topic/2'], 'https://my-mercure-server:9090/mercure') }}
57+
{{ stream_notifications() }}
58+
59+
{#
60+
Calling notify without parameters will fallback to these values:
61+
- 'https://symfony.com/notifier' as a single topic
62+
- and mercure.default_hub configuration parameter as hub url
63+
#}
64+
```
65+
66+
### Extend the default behavior
67+
68+
Symfony UX Notify allows you to extend its default behavior using a custom Stimulus controller:
69+
70+
```js
71+
// notify_controller.js
72+
73+
import { Controller } from 'stimulus';
74+
75+
export default class extends Controller {
76+
connect() {
77+
this.element.addEventListener('notify:connect', this._onConnect);
78+
}
79+
80+
disconnect() {
81+
// You should always remove listeners when the controller is disconnected to avoid side effects
82+
this.element.removeEventListener('notify:connect', this._onConnect);
83+
}
84+
85+
_onConnect(event) {
86+
// Event sources have just been created
87+
console.log(event.eventSources);
88+
89+
event.eventSources.forEach((eventSource) => {
90+
eventSource.addEventListener('message', (event) => {
91+
console.log(event); // You can add custom behavior on each event source
92+
});
93+
});
94+
}
95+
}
96+
```
97+
98+
Then in your render call, add your controller as an HTML attribute:
99+
100+
```twig
101+
{{ stream_notifications() }}
102+
```
103+
104+
## Backward Compatibility promise
105+
106+
This bundle aims at following the same Backward Compatibility promise as the Symfony framework:
107+
[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html)
108+
109+
However it is currently considered
110+
[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html),
111+
meaning it is not bound to Symfony's BC policy for the moment.
112+
113+
## Run tests
114+
115+
### PHP tests
116+
117+
```sh
118+
php vendor/bin/phpunit
119+
```
120+
121+
### JavaScript tests
122+
123+
```sh
124+
cd Resources/assets
125+
yarn test
126+
```

src/Notify/Resources/assets/.babelrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"presets": ["@babel/env"],
3+
"plugins": ["@babel/plugin-proposal-class-properties"]
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
yarn.lock
3+
yarn-error.log
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
'use strict';
10+
11+
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
12+
13+
Object.defineProperty(exports, "__esModule", {
14+
value: true
15+
});
16+
exports["default"] = void 0;
17+
18+
var _stimulus = require("stimulus");
19+
20+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
21+
22+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
23+
24+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
25+
26+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
27+
28+
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
29+
30+
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
31+
32+
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
33+
34+
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
35+
36+
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
37+
38+
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
39+
40+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
41+
42+
/**
43+
* @author Mathias Arlaud <[email protected]>
44+
*/
45+
var _default = /*#__PURE__*/function (_Controller) {
46+
_inherits(_default, _Controller);
47+
48+
var _super = _createSuper(_default);
49+
50+
function _default() {
51+
var _this;
52+
53+
_classCallCheck(this, _default);
54+
55+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
56+
args[_key] = arguments[_key];
57+
}
58+
59+
_this = _super.call.apply(_super, [this].concat(args));
60+
61+
_defineProperty(_assertThisInitialized(_this), "eventSources", []);
62+
63+
return _this;
64+
}
65+
66+
_createClass(_default, [{
67+
key: "connect",
68+
value: function connect() {
69+
var _this2 = this;
70+
71+
if (!('Notification' in window) || !this.hubValue || !this.topicsValue) {
72+
return;
73+
}
74+
75+
this.eventSources = this.topicsValue.map(function (topic) {
76+
return new EventSource("".concat(_this2.hubValue, "?topic=").concat(encodeURIComponent(topic)));
77+
});
78+
this.eventSources.forEach(function (eventSource) {
79+
return eventSource.addEventListener('message', function (event) {
80+
return _this2._notify(JSON.parse(event.data).summary);
81+
});
82+
});
83+
84+
this._dispatchEvent('notify:connect', {
85+
eventSources: this.eventSources
86+
});
87+
}
88+
}, {
89+
key: "disconnect",
90+
value: function disconnect() {
91+
var _this3 = this;
92+
93+
this.eventSources.forEach(function (eventSource) {
94+
eventSource.removeEventListener('message', _this3._notify);
95+
eventSource.close();
96+
});
97+
this.eventSources = [];
98+
}
99+
}, {
100+
key: "_notify",
101+
value: function _notify(content) {
102+
if (!content) {
103+
return;
104+
}
105+
106+
if ('granted' === Notification.permission) {
107+
new Notification(content);
108+
return;
109+
}
110+
111+
if ('denied' !== Notification.permission) {
112+
Notification.requestPermission().then(function (permission) {
113+
if ('granted' === permission) {
114+
new Notification(content);
115+
}
116+
});
117+
}
118+
}
119+
}, {
120+
key: "_dispatchEvent",
121+
value: function _dispatchEvent(name) {
122+
var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
123+
var canBubble = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
124+
var cancelable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
125+
var userEvent = document.createEvent('CustomEvent');
126+
userEvent.initCustomEvent(name, canBubble, cancelable, payload);
127+
this.element.dispatchEvent(userEvent);
128+
}
129+
}]);
130+
131+
return _default;
132+
}(_stimulus.Controller);
133+
134+
exports["default"] = _default;
135+
136+
_defineProperty(_default, "values", {
137+
hub: String,
138+
topics: Array
139+
});

0 commit comments

Comments
 (0)