Skip to content

Commit

Permalink
Documentation v1
Browse files Browse the repository at this point in the history
  • Loading branch information
b-viguier committed Aug 26, 2024
1 parent 3f215c5 commit abb8974
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 28 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ build-jekyll: ## Build the Jekyll container only (useful to update dependencies)

jekyll-clean: ## Clean all Jekyll content
$(DOCKER_COMPOSE) exec jekyll $(EXEC_SHELL) -c "bundle exec jekyll clean"
$(DOCKER_COMPOSE) exec jekyll $(EXEC_SHELL) -c "bundle exec jekyll build"
$(DOCKER_COMPOSE) down -v jekyll
$(DOCKER_COMPOSE) up jekyll -d
.PHONY: jekyll-clean

up: ## Start the docker containers
Expand Down
82 changes: 82 additions & 0 deletions docs/00_index.md
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!
```


102 changes: 102 additions & 0 deletions docs/01_signals_slots.md
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)).
101 changes: 101 additions & 0 deletions docs/02_connections.md
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.
}
```
58 changes: 58 additions & 0 deletions docs/03_advanced.md
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.
12 changes: 10 additions & 2 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ plugins:
# - vendor/ruby/

# Just The Docs settings
callouts_level: quiet
callouts:
good_to_know:
title: 💡Good to know
color: green
warning:
title: Warning
title: ⚠️Warning
color: red
color_scheme: dark
color_scheme: light
# Footer last edited timestamp
last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter
last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html
Expand All @@ -52,6 +56,10 @@ aux_links:
- https://github.com/b-viguier/Siglot
back_to_top: true
back_to_top_text: "Back to top"
mermaid:
# Version of mermaid library
# Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/
version: "10.9.1"



Expand Down
Loading

0 comments on commit abb8974

Please sign in to comment.