Redux en 30 lignes de PHP

Redux en 30 lignes de PHP

Le site officiel appelle Redux "un conteneur d'état prévisible pour les JS Apps". Le principal avantage de Redux est qu'il met en lumière l'état global de votre application, vous permettant de retracer quand, où, pourquoi et comment l'état de votre application a changé.

L'idée que l'état global est mauvais est ancrée chez les programmeurs depuis le début de leur carrière. Une chose qui n'est pas clairement expliquée, cependant, est le pourquoi.

L'état global pose plusieurs problèmes, mais le plus important est que non seulement n'importe qui peut changer l'état d'une application, mais aussi qu'il n'y a aucun moyen de savoir qui a changé l'état.

Si votre premier contact avec Redux se fait dans le cadre d'une application React Redux, le côté Redux vous semblera magique.

Comme j'essaie généralement de comprendre les outils que j'utilise, ce qui suit se veut une implémentation ludique de Redux en PHP, dans l'espoir de mieux comprendre les concepts qui se cachent derrière Redux.

La réalisation la plus importante a été que Redux est surtout conventionnel, avec un peu de code de bibliothèque pour lier le tout. Comme vous pouvez probablement le voir dans le titre, le langage que nous allons utiliser est PHP, alors considérez-vous comme averti.

Très bien. Commençons par l'état simple que nous allons gérer :

$initialState = [
    'count' => [
        'count' => 1
    ],
];

C'est assez simple. Conformément à la convention Redux, nous ne modifions pas directement l'État :

$initialState['count']['count'] +=1

Mais nous pouvons interagir avec l'État par le biais d'actions :

const INCREMENT_ACTION = 'INCREMENT';
const DECREMENT_ACTION = 'DECREMENT';

$actions = [
    'increment' => [
        'type' =>  INCREMENT_ACTION
    ],
    'decrement' => [
        'type' => DECREMENT_ACTION
    ],
];

L'état et les actions sont des tableaux de PHP simples, sans magie.

Nous avons également besoin d'un réducteur, une fonction qui prend notre état, traite l'action et génère le nouvel état :

function countReducer(array $state, $action) {
    switch($action['type']) {
        case INCREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] + 1]);
        case DECREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] - 1]);
        default:
            return $state;
    }
}

Nous générons simplement un nouveau tableau d'états en incrémentant / décrémentant la valeur précédente. Les réducteurs reçoivent une partie du tableau d'états initial, en fonction de leur clé, mais nous en reparlerons plus tard, lorsque nous introduirons les combinateurs. Pour l'instant, imaginons que countReducer reçoive la valeur dans $initialState [count] :

[
    'count' => 1
]

Nous n'avons pas encore écrit de code de bibliothèque, jusqu'à présent tout est conventionnel.

Si nous regardons l'API Redux, nous commençons par utiliser la méthode createStore. Nous pouvons ensuite utiliser les méthodes de répartition et d'abonnement pour répartir les actions et nous abonner aux mises à jour des changements d'état.

En traduisant cela en PHP, créons une classe Store. De quoi ça aurait l'air :

class Store 
{
    protected array $state;
    protected Closure $reducer;
    public function __construct(callable $reducer, array $initialState)
    {
        $this->state = $initialState;
        $this->reducer = Closure::fromCallable($reducer);
    }
}

La classe accepte maintenant un état initial et une fonction réductrice. Dans Redux, vous pouvez combiner plusieurs réducteurs en un seul et transmettre ce résultat combiné à la méthode createStore. Nous verrons plus tard comment nous pouvons y parvenir.

Super, que diriez-vous d'envoyer et de vous inscrire ? Commençons par ajouter une méthode pour récupérer l'état du store, puis la méthode d'abonnement :

protected array $listeners = [];

public function getState()
{
    return $this->state;
}

public function subscribe(callable $listener)
{
    $this->listeners[] = Closure::fromCallable($listener);
}

S'abonner au store signifie ajouter une fonction de rappel qui sera appelée lors de la mise à jour de l'état. Comment utiliser la méthode d'abonnement ?

$store->subscribe(function($state) {
    print_r($state);
});

Que diriez-vous d'un envoi :

public function dispatch(array $action)
{
    $this->state = ($this->reducer)($this->getState(), $action);
    foreach($this->listeners as $listener) {
        $listener($this->getState());
    }
}

Pour envoyer une action, nous devons générer le nouvel état en appelant le réducteur et en avertissant ensuite tous les auditeurs abonnés.

Génial. Avons-nous besoin d'autre chose ? Non, c'est à peu près tout. Nous avons maintenant une implémentation Redux de base en PHP. Voici comment vous l'utiliseriez :

<?php

class Store
{
    protected array $state;
    protected Closure $reducer;
    protected array $listeners = [];

    public function __construct( callable $reducer, array $initialState)
    {
        $this->state = $initialState;
        $this->reducer = Closure::fromCallable($reducer);
    }

    public function getState()
    {
        return $this->state;
    }

    public function subscribe(callable $listener)
    {
        $this->listeners[] = Closure::fromCallable($listener);
    }

    public function dispatch(array $action)
    {
        $this->state = ($this->reducer)($this->getState(), $action);
        foreach($this->listeners as $listener) {
            $listener($this->getState());
        }
    }
}

$initialState = [
    'count' => [
        'count' => 1
    ],
];

const INCREMENT_ACTION = 'INCREMENT';
const DECREMENT_ACTION = 'DECREMENT';

$actions = [
    'increment' => [
        'type' =>  INCREMENT_ACTION
    ],
    'decrement' => [
        'type' => DECREMENT_ACTION
    ],
];

function countReducer(array $state, $action) {
    switch($action['type']) {
        case INCREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] + 1]);
        case DECREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] - 1]);
        default:
            return $state;
    }
}

$store = new Store('countReducer', $initialState['count']);

$store->subscribe(function($state) {
    print_r($state);
});

$store->dispatch($actions['increment']);
$store->dispatch($actions['increment']);
$store->dispatch($actions['increment']);
$store->dispatch($actions['decrement']);

La sortie du code ci-dessus est :

Array
(
    [count] => 2
)
Array
(
    [count] => 3
)
Array
(
    [count] => 4
)
Array
(
    [count] => 3
)

Vous pouvez alors ajouter des données personnalisées sur l'action et modifier le réducteur pour prendre en compte les données personnalisées :

const INCREMENT_BY_ACTION = 'INCREMENT_BY';

function countReducer(array $state, $action) {
    switch($action['type']) {
        case INCREMENT_BY_ACTION;
            $value = $action['value'] ?? 0;
            return array_replace([], $state, ['count' => $state['count'] + $value]);
        case INCREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] + 1]);
        case DECREMENT_ACTION:
            return array_replace([], $state, ['count' => $state['count'] - 1]);
        default:
            return $state;
    }
}

$store->dispatch(['type' => INCREMENT_BY_ACTION, 'value' => 5]);

Génial. Le seul problème maintenant est que notre store n'accepte qu'un seul réducteur. C'est aussi le cas pour la fonction createStore Redux. La façon dont ils résolvent ce problème est de fournir une méthode combinant les réducteurs :

rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer});

La méthode prend donc une liste de réducteurs et finit par fournir un réducteur qui les combine. Qu'est-ce que cela donnerait en PHP ?

function combineReducers(array $reducers) {
    return function(array $state, $action) use ($reducers) {
        $newState = $state;
        foreach($reducers as $stateKey => $reducer) {
            if(!isset($newState[$stateKey])) {
                $newState[$stateKey] = [];
            }
            $newState[$stateKey] = $reducer($newState[$stateKey], $action);
        }

        return $newState;
    };
}

Nous commençons par accepter un ensemble de réducteurs et nous devons produire un rappel qui colle à la signature du réducteur (prend en compte un état et une action, et produit l'état résultant).

Dans ce rappel, nous passons ensuite en revue tous les réducteurs enregistrés, nous les appelons un par un avec leur morceau d'état approprié (basé sur la clé du réducteur) et nous retournons l'état résultant.

Nous appellerions alors cela comme suit :

$initialState = [
    'count' => [
        'count' => 1
    ],
    'sum' => 0
];

function sumReducer($state, $action) {
    switch($action['type']) {
        case ADD_SUM:
            return $state + 1;
        default:
            return $state;
    }

$store = new Store(combineReducers([
    'count' => 'countReducer',
    'sum' => 'sumReducer'
]), $initialState);

Merci à @sdnunca pour la permission de traduction de cette article