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 работает под капотом.