-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
515 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
--- | ||
title: Getting Started | ||
nav_order: 0 | ||
permalink: / | ||
--- | ||
|
||
# Getting Started | ||
{: .no_toc } | ||
|
||
* TOC | ||
{:toc} | ||
|
||
## What is _Siglot_ | ||
Signals and slots is a mechanism introduced in [Qt](https://doc.qt.io/qt-6/signalsandslots.html) | ||
for communication between objects. | ||
It makes it easy to implement the observer pattern while avoiding boilerplate code. | ||
_Siglot_ aims to provide similar features for the PHP language, with particular attention to Developer Experience (DX): | ||
* Easy to write, utilizing the [first-class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php) | ||
* Compatible with existing auto-completion | ||
* Compatible with static analysis tools | ||
* No side effects with references | ||
|
||
It can be regarded as an alternative to callbacks or event dispatchers, and it's particularly suited for | ||
[event-driven programming](https://en.wikipedia.org/wiki/Event-driven_programming), | ||
to decouple events' sender and receiver. | ||
|
||
## Installation | ||
|
||
To install Siglot, you can use [Composer](https://getcomposer.org/): | ||
|
||
```bash | ||
composer require b-viguier/siglot | ||
``` | ||
|
||
|
||
## Example | ||
|
||
Let's define a `$button` object, with a `$button->clicked()` _signal_ function that can be triggered with the `$button->click()` function. | ||
```php | ||
$button = new class() implements Siglot\Emitter { | ||
use Siglot\EmitterHelper; | ||
|
||
// This is our signal | ||
public function clicked(): Siglot\SignalEvent | ||
{ | ||
return Siglot\SignalEvent::auto(); | ||
} | ||
|
||
// This function triggers the signal above | ||
public function click(): void | ||
{ | ||
$this->emit($this->clicked()); | ||
} | ||
}; | ||
``` | ||
|
||
Now let's create a `$receiver` object, with a `$receiver->onClick()` method that will be used as a _slot_. | ||
```php | ||
$receiver = new class() { | ||
// This is our slot | ||
public function onClick(): void | ||
{ | ||
echo "Button clicked!\n"; | ||
} | ||
}; | ||
``` | ||
|
||
We can now connect the `$button->clicked()` _signal_ to the `$receiver->onClick()` _slot_. | ||
```php | ||
Siglot\Siglot::connect0( | ||
$button->clicked(...), | ||
$receiver->onClick(...), | ||
); | ||
``` | ||
|
||
Now, each time the _signal_ is triggered with the `$button->click()` method, the connected slot will be called. | ||
```php | ||
$button->click(); | ||
// Displays: Button clicked! | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
--- | ||
title: Signals & Slots | ||
nav_order: 1 | ||
permalink: /signals_slots | ||
--- | ||
|
||
# Signals and Slots | ||
{: .no_toc } | ||
|
||
* TOC | ||
{:toc} | ||
|
||
## Signals | ||
|
||
To define _signals_, a class must implement the | ||
[`Emitter`](https://github.com/b-viguier/Siglot/blob/main/src/Emitter.php) interface. | ||
All non-static methods that return a [`SignalEvent`](https://github.com/b-viguier/Siglot/blob/main/src/SignalEvent.php) | ||
instance are considered as _signals_. | ||
|
||
```php | ||
// A class able to emit signals | ||
class MyEmitter implements Emitter | ||
{ | ||
// A valid signal function | ||
public function signal(string $param1, int $param2): SignalEvent | ||
{ | ||
return SignalEvent::auto(); | ||
} | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
The goal of a [`SignalEvent`](https://github.com/b-viguier/Siglot/blob/main/src/SignalEvent.php) instance | ||
is to encapsulate all input parameters of the signal in a single object. | ||
The static method `SignalEvent::auto()` uses reflection to facilitate parameter forwarding, thus preventing misordering of parameters. | ||
|
||
{: .warning } | ||
A signal function SHOULD only return a [`SignalEvent`](https://github.com/b-viguier/Siglot/blob/main/src/SignalEvent.php) | ||
instance and SHOULD NOT perform any other actions. | ||
|
||
We recommend using the [`EmitterHelper`](https://github.com/b-viguier/Siglot/blob/main/src/EmitterHelper.php) | ||
trait to implement the [`Emitter`](https://github.com/b-viguier/Siglot/blob/main/src/Emitter.php) interface. | ||
This trait provides the useful `emit(SignalEvent $signalEvent): void` method. | ||
The `emit` function is `protected`, | ||
by design, as it's best to only emit signals from the class that defines them and its subclasses. | ||
You can find more details in the [Advanced](/advanced) section. | ||
|
||
```php | ||
class MyEmitter implements Emitter | ||
{ | ||
// ... | ||
public function processing(): void | ||
{ | ||
// ... | ||
$this->emit( | ||
$this->signal('my string', 123) | ||
); | ||
// ... | ||
} | ||
} | ||
``` | ||
|
||
|
||
{: .warning } | ||
Calling a signal function is not sufficient to actually trigger the signal; | ||
the returned [`SignalEvent`](https://github.com/b-viguier/Siglot/blob/main/src/SignalEvent.php) | ||
instance SHOULD be **immediately** _emitted_ using the `emit` method. | ||
|
||
{: .good_to_know } | ||
> * A class can define multiple signals. | ||
> * There are no restrictions on the type of parameters a signal can have. | ||
> * Although there are also no restrictions on the number of parameters, | ||
> it is recommended to keep it low in order to be compatible with existing connection functions (see [Connections](/connections)). | ||
> * The visibility of a signal function only affects its capability to be called or connected, | ||
following the usual rules of visibility in PHP (see [Connections Visibility](/connections#visibility)). | ||
|
||
## Slots | ||
Any non-static object method can be considered a slot. | ||
In most cases, it makes sense to call a slot as a regular method, without being triggered by a signal. | ||
|
||
```php | ||
class MyReceiver | ||
{ | ||
// A valid slot function | ||
public function slot(string $param1, int $param2): void | ||
{ | ||
// ... | ||
} | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
{: .good_to_know } | ||
> * There are no restrictions on the type of parameters a slot can have. | ||
> * Although there are also no restrictions on the number of parameters, | ||
> it is recommended to keep it low in order to be compatible with existing connection functions (see [Connections](/connections)). | ||
> * The visibility of a slot function only affects its capability to be called or connected, | ||
> following the usual rules of visibility in PHP (see [Connections Visibility](/connections#visibility)). | ||
> * A slot SHOULD NOT return a value, as there is no way to retrieve it from the signal emitter (see [Connections](/connections)). | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
--- | ||
title: Connections | ||
nav_order: 2 | ||
permalink: /connections | ||
--- | ||
|
||
# Connections | ||
{: .no_toc } | ||
|
||
* TOC | ||
{:toc} | ||
|
||
## Connecting signals to slots | ||
|
||
The [`Siglot`](https://github.com/b-viguier/Siglot/blob/main/src/Siglot.php) | ||
class offers various static methods for connecting a _signal_ to a _slot_. | ||
These methods are named `connect<N>`, where `<N>` represents the number of parameters of the _signal_. | ||
The numbering starts from `connect0` for signals without parameters and goes up to `connect5`. | ||
In all cases, the first parameter is the _signal_ and the second is the _slot_. | ||
Both are referenced using [first-class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php). | ||
|
||
```php | ||
Siglot::connect0($button->clicked(...), $receiver->onClick(...)); | ||
Siglot::connect1($editor->nameChanged(...), $receiver->onNewName(...)); | ||
``` | ||
|
||
{: .warning } | ||
It is **highly** discouraged to use something else than | ||
[first-class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php) | ||
to connect _signals_ and _slots_, even if it is technically possible. | ||
Refer to the [Advanced](/advanced) section for more details. | ||
|
||
|
||
{: .good_to_know } | ||
> * The number of parameters expected by a slot may be less than the number of parameters provided by the signal. | ||
> Any extra parameters are ignored, similar to regular PHP functions. | ||
> * A signal can be linked to multiple slots, but the order in which they are called is not guaranteed. | ||
> * A slot can be connected to multiple signals. | ||
## Chaining signals | ||
It is also possible to _chain_ signals together, using the `Siglot::chain<N>` methods, | ||
where `<N>` represents the number of parameters of the input _signal_. | ||
When a _signal_ is triggered, chained _signals_ will also be triggered with the same parameters. | ||
|
||
```php | ||
Siglot::chain0($button->clicked(...), $component->onSaveButtonClicked(...)); | ||
Siglot::chain1($text->changed(...), $component->onTextChanged(...)); | ||
``` | ||
|
||
{: .good_to_know } | ||
> * It is possible for a destination signal to expect fewer parameters than the input signal provides. | ||
Any extra parameters are ignored, similar to regular PHP functions. | ||
> * A signal can be chained to multiple signals, but the order in which they are called is not guaranteed. | ||
> * Chaining is compatible with regular slot connections. | ||
|
||
## Visibility | ||
Signals and slots are regular PHP methods that you can define with any visibility. | ||
However, the visibility affects the scope from which signals and slots can be connected. | ||
For example, a class can connect a `public` _signal_ to one of its `private` _slots_, | ||
or it can connect one of its `private` _signals_ to a `public` _slot_ of another class. | ||
This is because accessibility is determined when the | ||
[first-class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php) | ||
is used, not at execution. | ||
|
||
```php | ||
class MyReceiver { | ||
public function __construct(MyEmitter $emitter) { | ||
Siglot::connect0($emitter->signal(...), $this->myPrivateSlot(...)); | ||
} | ||
|
||
private function myPrivateSlot(): void { | ||
// ... | ||
} | ||
} | ||
``` | ||
|
||
{: .warning } | ||
If you are not using [first-class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php), | ||
scope resolution may be performed in Siglot's code, which will most likely result in failure. | ||
|
||
|
||
|
||
## Connection lifetime | ||
Once an emitter or a receiver is destroyed, all connections involving it are automatically removed. | ||
It’s important to note that Siglot does not retain any references to connected objects in order to avoid interfering with PHP’s garbage collector. | ||
This means you cannot depend on the existence of a connection to keep an object alive. | ||
|
||
```php | ||
function attachSignalLogger(MyEmitter $emitter): void { | ||
$logger = new class() { | ||
public function log(): void { | ||
echo "Signal received!\n"; | ||
} | ||
}; | ||
|
||
Siglot::connect0($emitter->signal(...), $logger->log(...)); | ||
// ⚠️ $logger is destroyed here !!! | ||
// Nothing will be printed when the signal is emitted outside of this function. | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
--- | ||
title: Advanced | ||
nav_order: 3 | ||
permalink: /advanced | ||
--- | ||
|
||
# Advanced | ||
{: .no_toc } | ||
|
||
Here are some optional details if you're curious about how Siglot works. | ||
|
||
* TOC | ||
{:toc} | ||
|
||
## Implementing [`Emitter`](https://github.com/b-viguier/Siglot/blob/main/src/Emitter.php) interface | ||
|
||
Siglot stores all connections in an [`Internal\SignalManager`](https://github.com/b-viguier/Siglot/blob/main/src/Internal/SignalManager.php) | ||
that is stored in the _emitter_ instance. | ||
The goal of the [`Emitter`](https://github.com/b-viguier/Siglot/blob/main/src/Emitter.php) | ||
interface is to expose signals of the [`Internal\SignalManager`](https://github.com/b-viguier/Siglot/blob/main/src/Internal/SignalManager.php) | ||
without exposing a way to _emit_ them from outside the class. | ||
This design ensures that signals are only emitted from the class that defines them and its subclasses. | ||
The [`EmitterHelper`](https://github.com/b-viguier/Siglot/blob/main/src/EmitterHelper.php) | ||
trait already includes everything needed to implement the [`Emitter`](https://github.com/b-viguier/Siglot/blob/main/src/Emitter.php) interface. | ||
However, if more control is needed, one can refer to the trait's implementation to manually implement the interface. | ||
|
||
|
||
{: .warning } | ||
Classes in the `Internal` namespace are not meant to be used directly | ||
and may be removed or changed without notice. | ||
|
||
## Storage of connections | ||
|
||
All connections must be stored in the _emitter_ instance in order to share its lifetime. | ||
This is transparently achieved by the [`EmitterHelper`](https://github.com/b-viguier/Siglot/blob/main/src/EmitterHelper.php) trait, | ||
but you may need to keep this in mind when dealing with some serialization functions for your _emitter_ class. | ||
|
||
## Performance | ||
|
||
In theory, there is a certain amount of overhead associated with calling a slot function from a signal. | ||
This overhead depends on the number of signals in the object and the number of slots connected to the called signal. | ||
In practice, this overhead should be less than 10 microseconds and is usually negligible. | ||
You can see the [benchmark](https://github.com/b-viguier/Siglot/tree/main/examples/benchmark.php) example to try it yourself. | ||
|
||
{: .good_to_know } | ||
When a slot is called directly, there is no overhead even if it is connected to several signals. | ||
|
||
|
||
## Connecting closures | ||
|
||
In order to connect signals to slots, Siglot utilizes closure objects to access related instances and methods through reflection. | ||
It is best to use [first class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php), | ||
as it ensures that the method exists and is visible, resulting in more readable code. | ||
Additionally, your favorite IDE will be able to offer autocompletion and static analysis. | ||
While it is possible to use the `\Closure::fromCallable([$emitter, 'signal'])` syntax, it is discouraged due to being less readable. | ||
|
||
{: .warning } | ||
Providing a closure that does not match a signal or slot function will result in a runtime error. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.