joelbutcher/php-optional

An opinionated implementation of Java's Optional class for PHP.

v1.0.0 2022-12-02 12:09 UTC

This package is auto-updated.

Last update: 2024-12-21 15:13:55 UTC


README

Build Status Total Downloads Latest Stable Version License

Introduction

Inspired by Java's Optional class, this package aims to provide a comprehensive API for optional field values.

Examples

You can use the Optional class in a variety of ways, see below for a few examples:

Updating a user's profile

The below class indicates how this package may be used to update a users profile information. All fields are optional, because our user may not want to update all fields.

<?php

namespace App\DataObjects;

use JoelButcher\PhpOptional\Optional;
use Psl\Type;
use Webmozart\Assert\Assert;

class UpdateProfile
{
    public function __construct(
        private readonly int $id
        private readonly Optional $name
        private readonly Optional $email
        private readonly Options $bio
    ) {
        //
    }

    public static function fromFormRequest(int $id UpdateProfileRequest $request): self
    {
        return new self(
            id: $id,
            name: Optional::forNullable($request->get('name')),
            email: Optional::forNullable($request->get('email')),
            bio: Optional::forNullable($request->get('bio')),
        );
    }

    public static function fromArray(array $data): self
    {
        Assert::keyExists($post, 'id');
        Assert::positiveInteger($post['id']);


        return new self(
            id: $data['id'],
            name: Optional::forOptionalArrayKey($data, 'name', Type\non_empty_string()),
            email: Optional::forOptionalArrayKey($data, 'email', Type\non_empty_string()),
            bio: Optional::forOptionalArrayKey($data, 'bio', Type\non_empty_string()),
        );
    }

    public function handle(UserRepository $users): void
    {
        $user = $users->findOneById($this->id);

        // These callbacks are only called if the value for each optional field is present.
        $this->name->apply(fn (string $name) => $user->updateName($name));
        $this->email->apply(fn (string $email) => $user->updateEmail($email));
        $this->bio->apply(fn (string $bio) => $user->updateBio($bio));

        $users->save($user);
    }
}

Noticed the \Psl\Type\non_empty_string() call? That's an abstraction coming from azjezz/psl, which allows for having a type declared both at runtime and at static analysis level. We use it to parse inputs into valid values, or to produce crashes, if something is malformed.

The azjezz/psl, Psl\Type tooling gives us both type safety and runtime validation by implicitly validating our values:

OptionalField::forPossiblyMissingArrayKey(
    ['foo' => 'bar'],
    'foo',
    \Psl\Type\positive_int()
); // crashes: `foo` does not contain a `positive-int`!