Введение

Функциональное программирование появилось не вчера. Но оно так и не приобрело какой-либо дикой популярности, и, вероятно, не просто так. Иногда оно может быть довольно сложным с точки зрения понимания и использования. Но у него есть много преимуществ. Одним из них является возможность избежать проверок на 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)


  1. Daemonis
    25.05.2023 09:57

    Пример выглядит странным :)

    Фронт/внешний сервис должен понимать, что юзера нет по имени Unknown? Такое себе. Разумнее вернуть им 404 и пусть там разбираются.

    Но если прям очень хочется базировать логику на этом имени, тогда его надо приделывать в сервисе, а не в контроллере.


  1. slavcopost
    25.05.2023 09:57

    public 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);
    }


    1. SerafimArts
      25.05.2023 09:57

      Скорее уж лучше написать:


      $user = $userRepository->getUserByEmail($email);
      
      return new JsonResponse([
          'name' => $user?->getName() ?? 'Unknown',
          'email' => $user?->getName() ?? 'Unavailable',
      ]);

      Как минимум читаемее будет


  1. kubk
    25.05.2023 09:57

    Не проще PHPStan настроить построже чтобы он заставлял обрабатывать nullable значения? Это предотвратит исключения и не повысит порог входа надуманными абстракциями.


  1. Epsiloncool
    25.05.2023 09:57
    -1

    Я конечно мало что понимаю в этом вашем Symfony, хотя и работаю с PHP с 2001 года и чего только и на чём только не писал. Заинтересовался словом "монада" применительно к PHP. Это базовая конструкция в изначально функциональных языках программирования, тут же она выглядит как сова, натянутая на глобус.

    Вам действительно нравится эмулировать на PHP новомодные подходы в программировании или кто-то сверху заставляет? Вот чего я понять не могу. Почему надо создать миллиард мелких хитропродуманных классов, вместо того, чтобы написать простой лаконичный, быстрый и понятный код, возможно даже вообще без использования классов?

    Понимаю, это старый холивар. Вопрос больше риторический, чем вопрос.