# Server-side message publisher

## Overview

The server-side message publisher provides a flexible message dispatcher intended to publish messages on various media. For that an interface for a message publisher is provided with two implementations:
* `HttpPublisher` that publishes messages with HTTP POST requests, for instance to locations served by Nchan
* `TouchPublisher` that notifies the OS about messages by touching particular files.

Once constructed, publishers are added to a message dispatcher that from now on is used to publish messages.

The `MessageDispatcher` class supports PSR-3 loggers via the `LoggerAwareTrait`. If a logger is provided, the dispatcher will log all publishing errors.

### Example
```php
use IMATHUZH\Messenger\{MessageDispatcher,HttpPublisher,TouchPublisher};

// Create a dispatcher an add some publishers
$dispatcher = (new MessageDispatcher())
    ->addPublisher(
        // Publishes messages using Nchan
        new HttpPublisher('http://localhost:8080/messenger/mysite', [
            'Content-Type' => 'text/json'
        ])
    )->addPublisher(
        // Touches files watched by an inotify-based script
        new TouchPublisher('/var/messanger', [
            'p' => 'page',
            'c' => 'command',
            't' => ''
        ])
    );

// Dispatch messages: (channels, name, payload=null)
// Note: processing publishing errors are explained later
$dispatcher->publishMessage(['t_Person'],
                            'updated',
                            ['id'=>433, 'name'=>'Joe']);
$dispatcher->publishMessage(['chat','chatadmin'],
                            'control',
                            'User jdoe has joined');
$dispatcher->publishMessage(['chatuserjdoe'],
                            'warning',
                            'Behave or you\'ll be kicked out!');
```


## Classes and interfaces

### Message dispatchers

A message dispatcher implements the interface
```php
interface MessageDispatcherInterface
{
    public function addPublisher(MessagePublisherInterface $publisher): self;

    public function publishMessage(array $channels,
                                   string $name,
                                   mixed $payload): array;
}
```
* `addPublisher` - adds a publisher to the internal collection.
* `publishMessage` - encodes a message and calls all publishers to handle it. Returns an array of `MessagePublisherError` objects that wrap errors returned by publishers.

The package provides the class `MessageDispatcher` with a minimal implementation and no configuration. It can be extended or used as is.

### Token Management

The messenger uses a token-based authorization system to restrict access to channels. A token is an instance of `TokenInterface` and is managed by a `TokenManagerInterface`.

#### AuthorizationToken

This class represents a set of permissions (publisher, listener, controller) for multiple groups of channels.

```php
use IMATHUZH\Messenger\AuthorizationToken;

// Create a token for a group with specific channels
$token = AuthorizationToken::createMessengerToken('mygroup', ['pub1'], ['sub1']);

// Wildcards are supported
$token = AuthorizationToken::createPublisherToken('admin', ['chat.*', 'system.status']);

// Merge multiple tokens
$merged = AuthorizationToken::mergeTokens($token1, $token2);
```

#### SessionTokenManager

This class manages the storage and retrieval of tokens, typically using the PHP `$_SESSION` array.

```php
use IMATHUZH\Messenger\SessionTokenManager;

// Use the QFQ session for storage. Tokens are searched in the stores: SIP, USER, WEB.
// Created tokens are saved in the SIP store.
$tokenMgr = SessionTokenManager::fromQfqSession();

// Save a token and get its ID
$tokenId = $tokenMgr->saveToken($token);

// Load a token by ID
$token = $tokenMgr->loadToken($tokenId);
```


### MessagePublisherError
A data class representing an error returned by a publisher. It has three properties:

|Property   |Type                       |Description                                               |
|-----------|---------------------------|----------------------------------------------------------|
|`publisher`|`MessagePublisherInterface`| A reference to the publisher                             |
|`key`      |`string`                   | Represents the medium to which the message was published |
|`message`  |`string`                   | The error message                                        |

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher($somePublisher);

// Publish a message
$errors = $dispatcher->publishMessage(['ch1','ch2'], 'test', ['value' => 42]);
// Process the errors
foreach($errors as $error) {
    echo $error->publisher::class . " returned an error " . $error->message
                                  . " on " . $error->key;
}
```


### Message publishers

A message publisher must implement the interface
```php
interface MessagePublisherInterface
{
    public function publishMessage(array $channels, string $msg): Iterator;
}
```
where the only function `publishMessage` is called by a `MessageDispatcher` to publish a message. This message must return in key-value iterator that iterates over potential errors:
* the value is interpreted as the error message
* the key is interpreted as the medium on which the message was published.

The package comes with two message publisher classes.

#### HttpPublisher

```php
class HttpPublisher implements MessagePublisherInterface
{
    public function __construct(string $basepath,
                                array $headers = [],
                                array $options = []);

    protected function pathsFromChannels(array $channels): Iterator;

    protected function isValidResponseCode(int $httpCode): bool;
    ...
}
```
This publisher makes an HTTP POST request with the message as the body to all urls `$basepath/$subpath`, where `$subpath` iterates over `pathsFromChannels()`. By default only one subpath is returned: `pub-$csch`, where `$csch` is a comma-separated list of all channels.

Use `$headers` to specify headers of the request, such as
* `Content-Type: text/json`
* `Authorization: <token>`
* ...

Use `$options` to specify additional options of the request, such as `timeout`.

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher(new HttpPublisher('http://localhost/messenger'));
$dispatcher->addPublisher(new HttpPublisher(
    'http://example.com/subdomain/channels/',
    ['Authorization' => 'secretToken']
));

// This results in HTTP requests to
// - http://localhost/messenger/pub-ch1,ch2
// - https://example.com/subdomain/channels/pub-ch1,ch2
$dispatcher->publishMessage(['ch1','ch2'], 'test');
```

**Errors**

An error represented by `MessagePublisherError` has the request URL as its key. The error message can be one of the following.

| Message                          | Meaning                                                                                                       |
|----------------------------------|---------------------------------------------------------------------------------------------------------------|
|*bad request*                     | No response received. Most likely an invalid URL or connection issues.                                        |
|*bad response*                    | The response was not in an HTTP format.                                                                       |
|*invalid HTTP status &lt;code&gt;*| An HTTP response with status code not accepted by `isValidHttpResponse()` (default: anything other than 2xx). |

#### TouchPublisher

```php
class TouchPublisher implements MessagePublisherInterface
{
    public function __construct(string $rootDir,
                                array $directories);

    protected function filesFromChannels(array $channels): Iterator;
}
```
Publishing a message touches (or creates) files associated with channels as follows.
1. A channel has an associated file only if it is of the form `${type}_${name}`, where `$type` is a key of the array `$directories` provided to the constructor.
1. For such channel the associated file is given as
   ```
   $rootDir/$directories[$type]/$name
   ```
1. Other channels are ignored.

Note that the content of the message is ignored by this publisher.

**Example**
```php
$dispatcher = new MessageDispatcher();
$dispatcher->addPublisher(new TouchPublisher(
    '/var/www/signals', [       // trailing slash is not required
        'p' => 'page',
        'pa' => 'page/admin',   // longer paths are allowed
        'cmd' => 'command',
        'tbl' => ''             // directly in the root folder
    ]
));

// This touches
// - /var/www/signals/page/login
// - /var/www/signals/command/refresh
// Channel 'other' is ignored.
$dispatcher->publishMessage(['p_login', 'cmd_refresh', 'other'], 'test');
```

**Errors**

Unsuccessful touches are represented by errors with a generic message (*failed to touch file*) and the file name as the key.
