Бывают ситуации, когда процесс расчёта или получения неких значений для вывода пользователю, довольно долгий или трудоёмкий. Например, получение какого-то значения через внешнее API или какой-то «тяжёлый» расчёт на основе существующих данных. Как правило, такие значения где-то сохраняют, чтобы иметь к нему быстрый доступ.
Я столкнулся с такой ситуацией и решил написать универсальную обвязку для работы с такими значениями.
Исходя из условий можно выделить следующие поля, которые позволят хранить всю необходимую информацию для значения:
key
— идентификатор значения;status
— статус значения (в процессе получения, успешно получено, ошибка);status_date
— дата сохранения статуса;value_date
(опционально) — дата получения значения;value
(опционально) — значение;error
(опционально) — текст ошибки.Статус значения зададим перечислением:
enum Status: string
{
case IN_PROCESS = 'in-process';
case SUCCESS = 'success';
case FAIL = 'fail';
}
Напишем интерфейс для хранилища:
interface StorageInterface
{
/**
* @param array{
* status: Status,
* status_date: DateTimeImmutable,
* value_date?: DateTimeImmutable|null,
* value?: string|null,
* error?: string|null,
* } $data
*/
public function save(string $key, array $data): void;
/**
* @return array{
* status: Status,
* status_date: DateTimeImmutable,
* value_date: DateTimeImmutable|null,
* value: string|null,
* error: string|null
* }|null
*/
public function get(string $key): ?array;
}
Реализация хранилища зависит от конкретной ситуации. Хранение может быть организовано в файлах, базе данных, кэше и так далее.
Теперь перейдём к основному классу, с которым будет взаимодействовать код приложения для получение/сохранения информации.
final readonly class CalculatedValues
{
public function __construct(
private StorageInterface $storage,
) {
}
/**
* Запомнить, что начат расчёт значения:
* - меняем статус,
* - запоминаем время смены статуса,
* - сбрасываем текст ошибки,
* - предыдущее значение не трогаем.
*/
public function start(string $key): void
{
$this->storage->save($key, [
'status' => Status::IN_PROCESS,
'status_date' => new DateTimeImmutable(),
'error' => null,
]);
}
/**
* Запомнить значение:
* - меняем статус,
* - запоминаем время смены статуса,
* - запоминаем значение и дату его получения,
* - сбрасываем текст ошибки.
*/
public function success(string $key, string $value): void
{
$date = new DateTimeImmutable();
$this->storage->save($key, [
'status' => Status::SUCCESS,
'status_date' => $date,
'value_date' => $date,
'value' => $value,
'error' => null,
]);
}
/**
* Запомнить, что расчёт значения завершился с ошибкой:
* - меняем статус,
* - запоминаем время смены статуса,
* - запоминаем текст ошибки,
* - предыдущее значение не трогаем.
*/
public function fail(string $key, string $error): void
{
$this->storage->save($key, [
'status' => Status::FAIL,
'status_date' => new DateTimeImmutable(),
'error' => $error,
]);
}
/**
* Получить значение.
*/
public function get(string $key): Result
{
$data = $this->storage->get($key);
if ($data === null) {
return Result::notExist();
}
return match ($data['status']) {
Status::IN_PROCESS => Result::inProcess(
$data['status_date'],
$data['value'],
$data['value_date'],
),
Status::SUCCESS => ($data['value'] === null || $data['value_date'] === null)
? Result::notExist()
: Result::success($data['value'], $data['value_date']),
Status::FAIL => $data['error'] === null
? Result::notExist()
: Result::fail($data['status_date'], $data['error'], $data['value'], $data['value_date']),
};
}
}
Результатом вызова метода get()
будет объект Result
, предоставляющий доступ как к самому значению, так и к дополнительной информации.
Если информации о значении нет в хранилище или запись в хранилище некорректная, то результатом будет "значения нет". Вообще, кодом обрабатывать некорректные значения считается плохой практикой, но в данном случае, на мой взгляд, такой подход оправдан. Некорректная записать в хранилище при следующем обновлении заменится на верную.
Код класса Result
:
final readonly class Result
{
private const int STATUS_NOT_EXIST = 0;
private const int STATUS_IN_PROCESS = 1;
private const int STATUS_SUCCESS = 2;
private const int STATUS_FAIL = 3;
/**
* @param self::STATUS_* $status
*/
private function __construct(
private int $status,
public ?string $value = null,
public ?DateTimeImmutable $valueDate = null,
private ?DateTimeImmutable $calculationStartDate = null,
private ?DateTimeImmutable $errorDate = null,
private ?string $error = null,
) {
}
public static function notExist(): self
{
return new self(self::STATUS_NOT_EXIST);
}
public static function inProcess(
DateTimeImmutable $calculationStartDate,
?string $value,
?DateTimeImmutable $valueDate,
): self {
return new self(self::STATUS_NOT_EXIST, $value, $valueDate, $calculationStartDate);
}
public static function success(
string $value,
DateTimeImmutable $date,
): self {
return new self(self::STATUS_SUCCESS, $value, $date);
}
public static function fail(
DateTimeImmutable $errorDate,
string $error,
?string $value,
?DateTimeImmutable $valueDate,
): self {
return new self(self::STATUS_FAIL, $value, $valueDate, errorDate: $errorDate, error: $error);
}
public function isNotExist(): bool
{
return $this->status === self::STATUS_NOT_EXIST;
}
public function isInProcess(): bool
{
return $this->status === self::STATUS_IN_PROCESS;
}
/**
* @psalm-assert-if-true !null $this->value
* @psalm-assert-if-true !null $this->valueDate
*/
public function isSuccess(): bool
{
return $this->status === self::STATUS_SUCCESS;
}
public function isFail(): bool
{
return $this->status === self::STATUS_FAIL;
}
/**
* @psalm-assert-if-true !null $this->value
* @psalm-assert-if-true !null $this->valueDate
*/
public function hasValue(): bool
{
return $this->value !== null && $this->valueDate !== null;
}
public function getCalculationStartDate(): DateTimeImmutable
{
if ($this->calculationStartDate === null) {
throw new LogicException('Calculation start date available in "in process" status only.');
}
return $this->calculationStartDate;
}
public function getErrorDate(): DateTimeImmutable
{
if ($this->errorDate === null) {
throw new LogicException('Error date available in "fail" status only.');
}
return $this->errorDate;
}
public function getError(): string
{
if ($this->error === null) {
throw new LogicException('Error available in "fail" status only.');
}
return $this->error;
}
}
В итоге, в коде мы будем работать с классом CalculatedValues
. Выглядеть это будет примерно так:
/**
* Код, запускаемый по крону
*/
$calculatedValues->start('my-data');
// ставим в очередь задачу на расчёт данных
/**
* Очередь
*/
$calcluatedValues->success('my-data', 'xxxxxx');
// или
$calcluatedValues->fail('my-data', 'API не доступно.');
/**
* Чтение данных
*/
$result = $calculatedValues->get('my-data');
if ($result->hasValue()) {
echo '<p>' . $result->value . ' (' . $result->valueDate->format('d.m.Y') . ')</p>';
}
if ($result->isNotExist()) {
echo '<p>Нет данных</p>';
}
if ($result->isFail()) {
echo '<p>Ошибка от ' . $result->getErrorDate()->format('d.m.Y') . ': ' . $result->getError() . '</p>';
}
if ($result->isInProcess()) {
echo '<p>Идёт расчёт (' . $result->getCalculationStartDate()->format('d.m.Y') . '</p>';
}
Не уверен, что этот код стоит выделять в отдельную библиотеку, так как в конкретном приложении возможны нюансы. Думаю будет лучше, если это будет частью пользовательского кода.
Спасибо подписчикам моего телеграм-канала и отдельно Алексндру Борисову за помощь в доработке статьи.