Введение
Функциональное программирование появилось не вчера. Но оно так и не приобрело какой-либо дикой популярности, и, вероятно, не просто так. Иногда оно может быть довольно сложным с точки зрения понимания и использования. Но у него есть много преимуществ. Одним из них является возможность избежать проверок на null и исключений.
В этой статье мы рассмотрим монаду Maybe и то, как ее можно использовать в Symfony.
Что из себя представляет монада Maybe?
Начать следует с определения самой концепции монады. Монада (monad) — это структура, предназначенная для представления вычислений в виде императивной последовательности шагов. Она является обобщением концепции функции, которая принимает аргумент и возвращает результат. Монады в функциональном программировании не является чем-то новым. Они существуют с 1960-х годов.
Монада Maybe — это монада, которая инкапсулирует необязательное значение. Значение типа Maybe a содержит либо значение типа а (представленное как Just a), либо вообще ничего (представленное как Nothing). Используя монаду Maybe, мы можем избежать null и исключений.
Как использовать монаду Maybe в Symfony?
Для начала давайте создадим класс-монаду, который будет реализовывать монаду Maybe.
// src/Utils/Maybe.php
<?php
namespace App\Utils;
/**
* @template T
*/
class Maybe
{
/**
* @var T|null
*/
private $value;
/**
* @param T|null $value
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* @param T|null $value
* @return Maybe<T>
*/
public static function just($value): Maybe
{
return new self($value);
}
/**
* @return Maybe<T>
*/
public static function nothing(): Maybe
{
return new self(null);
}
/**
* @template U
* @param callable(T):U $fn
* @return Maybe<U>
*/
public function map(callable $fn): Maybe
{
if ($this->value === null) {
return self::nothing();
}
return self::just($fn($this->value));
}
/**
* @param T $defaultValue
* @return T
*/
public function getOrElse($defaultValue)
{
return $this->value ?? $defaultValue;
}
}
Класс Maybe
содержит два статических метода: just
и nothing. just
метод создает объект Maybe
со значением.
Метод nothing
создает объект Maybe
без значения. Метод map
принимает в качестве аргумента функцию и применяет ее к значению внутри объекта Maybe
. Если значение внутри объекта Maybe
null, метод map
возвращает nothing
. Метод getOrElse
возвращает значение из объекта Maybe
или значение по умолчанию, если значение внутри объекта Maybe
– null.
Давайте посмотрим, как его можно использовать в Symfony-приложении.
// src/Controller/DefaultController.php
<?php
namespace App\Controller;
use App\Entity\User;
use App\Service\UserSrvice;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends AbstractController
{
#[Route('', name: 'default')]
public function getUserData(Request $request, UserSrvice $userSrvice): JsonResponse
{
$email = $request->get('email');
$maybeUser = $userSrvice->getUserByEmail($email);
$userData = $maybeUser
->map(fn(User $user) => [
'name' => $user->getName(),
'email' => $user->getEmail(),
])
->getOrElse([
'name' => 'Unknown',
'email' => 'Unavailable',
])
;
return $this->json($userData);
}
}
// src/Service/UserSrvice.php
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepository;
use App\Utils\Maybe;
class UserSrvice
{
public function __construct(private readonly UserRepository $userRepository)
{
}
public function getUserByEmail(string $email): Maybe
{
return Maybe::just($this->userRepository->getUserByEmail($email));
}
}
В классе DefaultController
мы получаем электронное письмо из запроса. Затем мы определяем пользователя по этому электронному письму, используя класс UserSrvice
.
Класс UserSrvice
возвращает объект Maybe
. Для получения пользовательских данных мы используем метод map
. Если пользователь не найден, метод map
возвращает nothing.
Затем мы используем метод getOrElse
для получения пользовательских данных или значения по умолчанию, если пользователь не найден.
Заключение
В этой статье мы рассмотрели монаду Maybe и то, как ее можно использовать в Symfony. Мы создали класс Maybe
, реализующий монаду Maybe. Мы использовали класс Maybe
в классе DefaultController
, чтобы избежать проверки на null и необходимости использовать исключения. Используя этот подход, мы можем избежать проверки на null и исключений почти во всем нашем Symfony-приложении, что сделает код более читабельным.
Полный код вы можете найти на GitHub
На днях пройдет открытый урок «Twig и Symfony forms: создаем полноценное веб-приложение без погружения во frontend». На нем разработаем быструю и простую административную панель штатными средствами фреймворка.
Занятие пройдет в преддверии старта курса "Symfony Framework". Записаться на открытый урок можно по ссылке.
Комментарии (5)
slavcopost
25.05.2023 09:57public function map(callable $fn): Maybe { return $this->value === null ? $this : self::just($fn($this->value)); }
Если уже "nothing", нового не надо же?!
Пример, настолько "выдуманный" что так и не показывает зачем это надо, а наборот доказывает что это код без монады будет проще и быстрее.
public function getUserData(Request $request, UserRepository $userRepository): JsonResponse { $email = $request->get('email'); $maybeUser = $userRepository->getUserByEmail($email); $userData = $maybeUser ? call_user_func(fn(User $user) => [ 'name' => $user->getName(), 'email' => $user->getEmail(), ], $maybeUser) : [ 'name' => 'Unknown', 'email' => 'Unavailable', ]; return $this->json($userData); }
SerafimArts
25.05.2023 09:57Скорее уж лучше написать:
$user = $userRepository->getUserByEmail($email); return new JsonResponse([ 'name' => $user?->getName() ?? 'Unknown', 'email' => $user?->getName() ?? 'Unavailable', ]);
Как минимум читаемее будет
kubk
25.05.2023 09:57Не проще PHPStan настроить построже чтобы он заставлял обрабатывать nullable значения? Это предотвратит исключения и не повысит порог входа надуманными абстракциями.
Epsiloncool
25.05.2023 09:57-1Я конечно мало что понимаю в этом вашем Symfony, хотя и работаю с PHP с 2001 года и чего только и на чём только не писал. Заинтересовался словом "монада" применительно к PHP. Это базовая конструкция в изначально функциональных языках программирования, тут же она выглядит как сова, натянутая на глобус.
Вам действительно нравится эмулировать на PHP новомодные подходы в программировании или кто-то сверху заставляет? Вот чего я понять не могу. Почему надо создать миллиард мелких хитропродуманных классов, вместо того, чтобы написать простой лаконичный, быстрый и понятный код, возможно даже вообще без использования классов?
Понимаю, это старый холивар. Вопрос больше риторический, чем вопрос.
Daemonis
Пример выглядит странным :)
Фронт/внешний сервис должен понимать, что юзера нет по имени Unknown? Такое себе. Разумнее вернуть им 404 и пусть там разбираются.
Но если прям очень хочется базировать логику на этом имени, тогда его надо приделывать в сервисе, а не в контроллере.