Redux в 30 строк на PHP
Перевод статьи https://sorin.live/redux-in-50-lines-of-php/
Официальный сайт называет Redux "предсказуемый контейнер состояний для JS приложений". Основная польза от Redux в том, что он проливает свет на глобальное состояние приложения, позволяя тебе определить: когда, где, почему и как состояние твоего приложения было изменено.
Мы убеждены, что работа с глобальным состоянием - это плохо. Среди всех проблем работы с глобальным состоянием наиболее серьёзные заключаются в том, что кто-то со стороны может изменить состояние приложения, и вы не сможете определить, кто это сделал.
Если вы первый раз встретились с Redux в контексте React Redux приложения, Redux будет казаться чем-то магическим.
Так как я обычно стараюсь понять инструмент, который я использую, нижеизложенное - это попытка реализации Redux в PHP, в надежде приобрести глубокое понимание концепции, лежащей внутри Redux. Важно понимать, что мы будем реализовывать абстрактный Redux, просто чтобы понять логику его работы.
Давайте начнём и объявим простое состояние, которым мы будем управлять:
$initialState = [
'count' => [
'count' => 1
],
];Достаточно просто. Как говорится в Redux соглашении, мы не можем изменять состояние напрямую:
$initialState['count']['count'] +=1 // так делать нельзя
Но мы можем взаимодействовать с состоянием используя действия:
const INCREMENTACTION = 'INCREMENT';
const DECREMENTACTION = 'DECREMENT';
$actions = [
'increment' => [
'type' => INCREMENTACTION
],
'decrement' => [
'type' => DECREMENTACTION
],
];Состояние и действия в нашем случае - это просто PHP массивы, никакой магии.
Нам так же нужен редьюсер - функция, которая берёт наше состояние, выполняет над ним действие и генерирует новое состояние:
function countReducer(array $state, $action) {
switch($action['type']) {
case INCREMENTACTION:
return arrayreplace([], $state, ['count' => $state['count'] + 1]);
case DECREMENT_AACTION:
return arrayplace([], $state, ['count' => $state['count'] - 1]);
default:
return $state;
}
}Важно понимать, что менять состояние могут только функции-редьюсеры.
Редьюсер генерирует массив с новым состоянием посредством инекремента/декремента предыдущего значения. Пока мы не будем разбираться, как редьюсеры получают состояние, сейчас просто примем, что countReducer получает значение в $initialState['count']:
[ 'count' => 1 ]
Закончим с абстрактыми примерами и приступим к практике.
Если мы посмотрим на API Redux, то начать следует с метода createStore. После его реализации мы сможем реализовать методы dispatch и subscribe, для отправки и подписки на изменение состояния хранилища.
Транслируя это в PHP, давайте создадим класс Store:
class Store
{
protected array $state;
protected Closure $reducer;
public function __construct(callable $reducer, array $initialState)
{
$this->state = $initialState;
$this->reducer = Closure::fromCallable($reducer);
}
}Класс принимает начальное состояние и функцию редьюсер, которая может это состояние изменять. В Redux вы можете комбинировать множество редьюсеров в одном и передавать комбинированный результат в метод createStore, позже мы обсудим, как это можно сделать.
Отлично, как быть с отправкой и подпиской на события? Даайте начнём с добавления метода для получения состояния хранилища, а затем реализуем метод подписки:
protected array $listeners = [];
public function getState()
{
return $this->state;
}
public function subscribe(callable $listener)
{
$this->listeners[] = Closure::fromCallable($listener);
}
Подписка на хранилище подразумевает добавление callback-функции, которая будет вызвана когда состояние обновится. Как использовать этот метод?
$store->subscribe(function($state) {
print_n($state);
});
Теперь о том, как инициировать событие:
public function dispatch(array $action)
{
$this->state = ($this->reducer)($this->getState(), $action);
foreach($this->listeners as $listener) {
$listener($this->getState());
}
}Для отправки события нам нужно сгенерировать новое состояние, вызвав редьюсер, а затем уведомить всех подписанных слушателей.
Круто. Нам нужно сделать что-то ещё? Нет, этого достаточно. Мы реализовали базовую версию Redux в PHP. Теперь о том, как это использовать:
<?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']);
Вывод будет следующим:
Array
(
[count] => 2
)
Array
(
[count] => 3
)
Array
(
[count] => 4
)
Array
(
[count] => 3
)
Вы так же можете добавлять кастомные данные к действию и изменить редьюсер так, чтобы учитывать эти данные:
const INCREMENTBYACTION = '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' => INCREMENTBYACTION, 'value' => 5]);Прекрасно. Но есть одна проблема - наше хранилище сейчас может работать только с одним редьюсером. Это также относится и к функции createStore в Redux. Способ, которым можно решить эту проблему - реализовать метод combReducers:
rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer});
Этот метод принимает список редьюсеров и возвращает редьюсер, который комбинирует все полученные. Как это сделать на 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;
};
}
Использовать это можно так:
$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);
Вот мы и написали нашу реализацию Redux. Я надеюсь, что несмотря на язык реализации, эта статья помогла вам пролить свет на то, как Redux работает под капотом.