paint-brush
Say Goodbye to Null-Checking and Exceptions: Using the Maybe Monad in Symfonyby@akankov
2,592 reads
2,592 reads

Say Goodbye to Null-Checking and Exceptions: Using the Maybe Monad in Symfony

by Aleksei KankovMarch 27th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The Maybe monad is a monad that encapsulates an optional value. A value of type `Maybe a` contains either a value oftype `a` (represented as `Just a`) or nothing at all (representing as `Nothing`) Using the Maybe Monad, we can avoid null values and exceptions.
featured image - Say Goodbye to Null-Checking and Exceptions: Using the Maybe Monad in Symfony
Aleksei Kankov HackerNoon profile picture


Introduction

Functional programming is old. But it did not become popular, and probably for a reason. It is sometimes quite hard to understand and use it. But it has a lot of advantages. One of them is the ability to avoid null-checking and exceptions.


In this article, we will look at the Maybe monad and how to use it in Symfony.

What is the Maybe monad?

Let's start with the definition of monad itself. A monad is a structure that represents computations defined as sequences of steps. It is a generalization of the concept of a function that takes an argument and returns a result. A monad in functional programming is not something new. It has been around since the 1960s.


The Maybe monad is a monad that encapsulates an optional value. A value of type Maybe a contains either a value of type a (represented as Just a) or nothing at all (represented as Nothing). Using the Maybe monad, we can avoid null values and exceptions.

How to use the Maybe monad in Symfony?

Let's create a Monad class that will implement the Maybe monad.


// 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;
    }
}


The Maybe class has two static methods: just and nothing. The just method creates a Maybe object with a value.


The nothing method creates a Maybe object without a value. The map method takes a function as an argument and applies it to the value inside the Maybe object. If the value inside the Maybe object is null, the map method returns nothing. The getOrElse method returns the value inside the Maybe object or a default value if the value inside the Maybe object is null.


Let's check how to use it in the Symfony application.


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


In the DefaultController class, we get the email from the request. Then we get the user by email using the UserSrvice class.


The UserSrvice class returns a Maybe object. We use the map method to get the user data. If the user is not found, the map method returns nothing.


Then we use the getOrElse method to get the user data or a default value if the user is not found.

Conclusion

In this article, we looked at the Maybe monad and how to use it in Symfony. We created a Maybe class that implements the Maybe monad. We used the Maybe class in the DefaultController class to avoid null-checking and exceptions. Using this approach, we can avoid null-checking and exceptions in our Symfony application, and the code will be more readable.


The full code is available on GitHub.