Skip to content

Latest commit

 

History

History
101 lines (83 loc) · 2.98 KB

optional.md

File metadata and controls

101 lines (83 loc) · 2.98 KB

Optional

Almost the same as Maybe. But Maybe is more about technical layer, and Optional is about business cases. The NULL in the context of Maybe is just nothing, no value. But in the context of Optional the NULL is a valid value, operation will be performed.

Let's take CRUD operation as an example. Does a null value of $description mean "remove the description", or "skip setting the description"?

class EditArticle {
    private function __construct(
        public readonly int $id,
        public readonly string|null $title,
        public readonly string|null $description,
    ) {}

    public static function fromPost(array $post): self
    {
        Assert::keyExists($post, 'id');
        Assert::positiveInt(prop('id', $post));
                
        return new self(
            prop('id', $post), 
            prop('title', $post), 
            prop('description', $post)
        );
    }
}

The usage side.

class HandleEditArticle
{
    public function __construct(private readonly Articles $articles) {}
    
    public function __invoke(EditArticle $command): void {
        $article = $this->articles->get($command->id);
        
        if ($command->title !== null) {
            // update title only when provided
            $article->setTitle($command->title);
        }
        
        // Description always updating. Maybe we just forgot extract it from the payload?
        // Who is responsible for deciding "optional field" vs "remove description when not provided": the command,
        // or the command handler?
        // Is this a bug, or correct behavior?
        $article->setDescription($command->description);
        
        $this->articles->save($article);
    }
}

Now let's use Optional.

class EditArticle {
    private function __construct(
        public readonly int $id,
        public readonly Optional $title,
        public readonly Optional $description,
    ) {}

    public static function fromPost(array $post): self
    {
        Assert::keyExists($post, 'id');
        Assert::positiveInt(prop('id', $post));
                
        return new self(
            prop('id', $post), 
            Optional::fromProp('title', $post), 
            Optional::fromProp('description', $post)
        );
    }
}

The handler.

class HandleEditArticle
{
    public function __construct(private readonly Articles $articles) {}
    
    public function __invoke(EditArticle $command): void {
        $article = $this->articles->get($command->id);
        
        // Only called if a fields has a provided value.
        $command->title->match([$article, 'setTitle'], N);
        $command->description->match([$article, 'setDescription'], N);
        
        $this->articles->save($article);
    }
}

What we have here:

  • Better clarity about the optional nature of specific fields
  • No conditional logic (less static analysis and testing efforts)
  • Strict behavior: field provided — will be updated, not provided — nothing happen