📖 Documentation: https://gabrielalmir.github.io/maybe/ — Why Maybe? · Tutorial · API Reference · Español
Maybe is a PHP library for explicit and predictable business logic.
It combines 5 main building blocks:
Option<T>: safe flow for optional valuesResult<T, E>: typed success/error without exceptions as control flowSchema: immutable parsing and validationDTO: validated mapping for input objectsAsync: concurrent execution via processes (proc_open) focused on PHP 7.4 + Windows + CI3
New users in corporate or legacy environments should start with Schema, DTO, and Result before adopting Async. Detailed adoption guidance lives in docs/ so this README can remain a compact API overview.
- Corporate Adoption Guide
- CodeIgniter 3 Guide
- Usage Patterns
- Practical Recipes
- Anti-Patterns
- Incremental Migration
- Async Safety Guide
- Tutorial: Email Queue no CI3 e assinatura de contratos no Laravel
- PHP
>= 7.4 - Composer
composer require gabrielalmir/maybe- Main runtime: no extra mandatory dependencies
Asyncmodule: usesopis/closurefor closure serialization
use Maybe\Option\Option;
$name = Option::fromNullable($payload['name'] ?? null)
->map('trim')
->flatMap(static function (string $value): Option {
return $value === '' ? Option::none() : Option::some($value);
})
->unwrapOr('guest');Main methods:
map(callable $fn): OptionflatMap(callable $fn): Optionfilter(callable $predicate): Optionmatch(callable $onSome, callable $onNone)unwrap(),unwrapOr($default),unwrapOrElse(callable),expect(string)okOr($error): Result,okOrElse(callable): ResultisSome(),isNone()
use Maybe\Result\Result;
function loadUser(int $id): Result
{
if ($id <= 0) {
return Result::err('invalid_id');
}
return Result::ok(['id' => $id, 'name' => 'Ana']);
}
$message = loadUser(10)->match(
static fn (array $user): string => 'User: ' . $user['name'],
static fn (string $error): string => 'Error: ' . $error
);Main methods:
map(callable $fn): ResultmapErr(callable $fn): ResultandThen(callable $fn): ResultorElse(callable $fn): Resultmatch(callable $onOk, callable $onErr)unwrap(),unwrapErr(),unwrapOr($default),unwrapOrElse(callable),expect(string)okOption(): Option,errOption(): OptionisOk(),isErr()
use Maybe\Schema\Schema;
$schema = Schema::shape([
'email' => Schema::string()->trimmed()->min(5),
'age' => Schema::int()->min(18),
]);
$result = $schema->safeParse([
'email' => ' user@example.com ',
'age' => 23,
]);Available builders:
Schema::string(),Schema::int(),Schema::bool(),Schema::date()Schema::enumeration([...])Schema::arrayOf(...)Schema::shape([...])Schema::option(...)
use Maybe\DTO\DTO;
use Maybe\Schema\ObjectSchema;
use Maybe\Schema\Schema;
final class CustomerDTO extends DTO
{
/** @var string */
public $email;
private function __construct(string $email)
{
$this->email = $email;
}
public static function schema(): ObjectSchema
{
return Schema::shape([
'email' => Schema::string()->trimmed()->min(5),
]);
}
protected static function fromValidated(array $validated)
{
return new self($validated['email']);
}
}
$dtoResult = CustomerDTO::fromArray(['email' => 'ana@example.com']);Entry points:
DTO::fromArray($input)returnsResult<DTO, ValidationErrorBag>DTO::parse($input)throws an exception on validation error
$result = await(async(static function (): int {
usleep(100000);
return 42;
}));Features:
async(callable $task, array $args = [], array $options = [])await($futureOrArray)Async::all([...])Async::race([...])Async::pool($tasks, $limit)AsyncFuture::then()->catch()->finally()->resolve()pending(),cancel(), per-task timeout (['timeout' => 2.5])- authenticated IPC with default input/output limits of 16 MiB / 64 MiB
max_input_bytes,max_output_bytes, andinclude_remote_traceoptions
The following functions are auto-loaded:
- Option/Result:
some(),none(),fromNullable(),ok(),err() - Schema:
stringSchema(),intSchema(),boolSchema(),dateSchema(),enumSchema(),arraySchema(),objectSchema(),optionSchema() - Async:
async(),await()
Global aliases are also available for CI3 compatibility:
AsyncAsync_future
With Composer loaded in the project:
$this->load->library('async');
$value = await(async(static function (): int {
return 123;
}));- Processes are isolated (no shared memory)
- Non-serializable resources must be recreated in the child process
- There is process spawn overhead per task
- Task stdout/stderr is discarded to prevent pipe back-pressure deadlocks
Asyncis not a same-user sandbox; callables and process configuration are trusted inputs
composer lint
composer test:asyncNote: the legacy
testrunner uses Pest 1.x. On very new PHP versions, prefertest:asyncto validate the async module.
Run any example directly:
php examples/option-result.php # Option + Result checkout flow
php examples/schema-dto.php # DTO with Schema validation
php examples/recipe-repository-lookup.php # Option-based repository lookup
php examples/recipe-safe-external-call.php # wrapping legacy exceptions into Result
php examples/recipe-batch-import.php # batch validation with per-row errors
php examples/scenario-transactional-email.php # order-confirmation email with SMTP fallback
php examples/scenario-sap-order-integration.php # pushing orders into SAP with retryable vs business errors
php examples/scenario-contract-validation.php # contract validation with cross-field business rules
php examples/async-basic.php
php examples/async-all-race.php
php examples/async-pool.php
php examples/async-chain-timeout-cancel.phpMore recipes with explanations: Recipes guide. For full business context on the scenario examples: Case Studies.