June 18, 2020

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