Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Monads Laravel Package

gosuperscript/monads

gosuperscript/monads is a lightweight functional programming library for PHP and Laravel, offering Option, Result, and related monads to handle nullable values and errors safely through clean, chainable APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, inspect, and wire the static-analysis extension before you commit to it.

composer require gosuperscript/monads-php
composer require --dev gosuperscript/monads-phpstan

This is a single-purpose library (Option/Maybe, Either/Result style wrappers) with a tiny surface area and a small user base. Before writing any application code, spend ten minutes in vendor/gosuperscript/monads-php/src — the whole package is readable in one sitting, and the exact factory/method names (some/none, just/nothing, right/left, ok/err, map/bind/flatMap, getOrElse/fold) are the one thing worth verifying against the source rather than assuming. There is no service provider and no config file to publish, so there is nothing to register in config/app.php.

Where to look first

  1. composer.json — check the required PHP version against your Laravel/PHP matrix. A package released in mid-2026 may assume PHP 8.2+.
  2. The README for the canonical fold/match example — that tells you the intended control-flow idiom.
  3. The interfaces (if any) behind Option and Either. If they're interfaces, you can type-hint them in your own code and swap implementations later; if they're final classes, you're coupled.
  4. phpstan.neon / the PHPStan extension shipped in v1.3.0. Register it in your project's phpstan.neon and confirm it flags panicking unwraps (get(), unwrap(), expect(), etc.) on Option/Either. If it does, treat that as a mandatory part of the install — it's the single cheapest way to stop a monad from re-introducing the null bugs it was meant to eliminate.

First use case

The highest-value, lowest-risk entry point in a Laravel app is a read-only lookup in a service class where the alternative is nested ternaries or optional() chains:

public function findActiveSubscription(User $user): Option
{
    return Option::fromNullable(
        $user->subscriptions()->where('status', 'active')->first()
    );
}

Then at the call site:

$label = $this->subscriptions->findActiveSubscription($user)
    ->map(fn ($sub) => $sub->plan->name)
    ->getOrElse('No active plan');

Because nothing crosses a boundary yet (no controller returns it, no Blade sees it, no JSON encodes it), you get the readability win with zero architectural exposure. Expand from there only if the team likes it.


Implementation Patterns

Boundary conversion — the core discipline. Monads should live in your service/domain layer and be converted at every edge.

// Controller: convert once, then behave conventionally
$result = $this->registerUser->execute($request->validated());

return $result->fold(
    fn (User $user) => redirect()->route('dashboard'),
    fn (RegistrationError $e) => back()->withErrors($e->messages()),
);

Rule: HTTP responses, Eloquent models, Blade views, and queued job payloads never contain a monad. If a monad appears in a toArray(), a JsonResource, or a Livewire public property, you've gone too far.

Either for validation and multi-step workflows. This is where the package earns its keep over exceptions and where Laravel's own idioms are weakest — a pipeline of fallible steps where you want the first failure without a try/catch ladder:

return Either::right($payload)
    ->flatMap(fn ($p) => $this->validateInventory($p))
    ->flatMap(fn ($p) => $this->reserveStock($p))
    ->flatMap(fn ($p) => $this->chargeCard($p))
    ->map(fn ($p) => $this->orderFrom($p));

Use Either/Result for expected failures (out of stock, card declined, coupon invalid) and let genuine exceptions (QueryException, Throwable) bubble to Laravel's handler. Don't wrap the entire request lifecycle; wrap the decision points.

Option for relation traversal. The pattern that saves the most typing is deep optional chains that would otherwise be ?-> soup with null-coalescing at the end. Prefer Option::fromNullable($x)->map(...) for anything more than two levels deep, and use native ?-> for one-liners — mixing them for consistency's sake is not worth the noise.

Never unwrap — the PHPStan extension enforces this for you. v1.3.0 ships a PHPStan extension that bans panicking unwraps (the methods that throw or abort() when you call them on None/Left). Two consequences:

  • Register the extension in phpstan.neon and run it in CI alongside Larastan. Without it, the discipline is on the honor system.
  • Prefer total accessors everywhere: getOrElse($default), fold($onLeft, $onRight), map, flatMap, filter. If you find yourself wanting get() or unwrap(), that's a signal to convert at a boundary instead — which is exactly the rule above.
  • In a request context, translate a "should never happen" empty into a 404/422 explicitly via fold/getOrElse, not via a throwing getter. The extension nudges you toward the correct shape.

Wrap it in your own thin façade. With a small project and modest dependents, treat this as vendored code. Define App\Support\Maybe / App\Support\Result (or a Monadic trait) that your app imports, and have those delegate to the package. If it's abandoned or you need to swap it, the blast radius is a handful of files instead of the whole codebase. This also gives you a place to add Laravel-specific helpers, e.g. Result::fromValidation(Validator $v).

Adapters to framework types. Two short converters pay for themselves immediately: Option → Collection (->map(...)->getOrElse(collect())) for iterating Eloquent relations, and Result → ValidationException for form requests. Keep them in one MonadSupport class.

Testing. Monads are trivially assertable, which is a real benefit: $result->isRight(), ->getOrElse($default), ->fold(...). Prefer asserting on the unwrapped value plus the failure branch over asserting on internal state. For property-style checks, assert that map(f) then flatMap(g) equals flatMap(fn ($x) => g(f($x))) once, in a single test, and trust it thereafter. Add one small test that your CI PHPStan config actually runs the extension (e.g. a @phpstan-baseline entry disappearing when you fix a get() call), so a dependency bump can't silently disable the guard.


Gotchas and Tips

The maintenance risk is still the headline gotcha — but v1.3.0 is a good signal. The release added a first-class PHPStan extension (a real feature, not a doc tweak), and the contributor credit goes to someone outside the original author. That's healthier than the 6-star history alone suggests. It's still a small maintainer base, so pin the exact version ("gosuperscript/monads-php": "1.x" with a committed lock file), vendor the source if your compliance rules demand it, and keep the façade described above so a fork is a one-day job rather than a rewrite. Do not introduce it into a codebase where the team can't read the package source.

Static analysis friction — partially solved, still worth checking. The v1.3.0 extension bans panicking unwraps, which removes the worst footgun. It does not necessarily add generics: generic types in PHP are PHPDoc-only, and @template annotations on map/flatMap are a separate concern. Run PHPStan/Larastan at a decent level and inspect whether your map callbacks infer concrete types or degrade to mixed. If they degrade, write one local stub or wrapper with generics — without them, the ergonomics drop below plain ?-> plus a match, even with the new guard.

The extension doesn't ship with Laravel. There's no service provider, and no auto-registration for the PHPStan rules. You must add the extension file to phpstan.neon (typically includes: - vendor/gosuperscript/monads-phpstan/extension.neon or equivalent — check the released path). If you use Larastan via a shared config package, adding it there is the lowest-friction spot. Forgetting this step is the most common upgrade mistake: PHPStan passes, but nothing is actually checking your unwraps.

Immutability vs Eloquent. These wrappers return new instances; Eloquent models are mutable and shared by reference. $option->map(fn ($u) => $u->update([...])) mutates the model and discards the boolean return, which is exactly the bug you were trying to avoid. Side effects belong outside the chain, or return a new DTO from the map.

Don't chain across side effects. A monad chain looks transaction-shaped but isn't. DB::transaction() does not wrap a flatMap chain — do the transaction inside one flatMap callback, or pass the chain through a single transactional closure. Same for dispatching events and jobs: fire them in a terminal fold/tap, not mid-pipeline.

Serialization. Monads don't survive serialize(), queue payloads, or session storage cleanly, and they won't JSON-encode into anything meaningful. Convert to a plain array/DTO before dispatch(). If you see a monad in a SerializesModels job constructor, that's a refactor.

instanceof and equality. None may be a singleton while Some(null) is a distinct value — verify how the package treats null. ==/=== comparisons on two independently constructed Right(5) instances are unreliable; use equals() if provided, or compare unwrapped values.

Error semantics drift. Packages differ on whether map runs on the failure branch (it shouldn't) and whether flatMap is aliased to bind. Read the implementations; a one-line difference here silently changes your error path. Write one test per monad asserting short-circuit behaviour and treat it as a canary for upgrades.

Upgrading to v1.3.0. Additive, low risk, but it will surface existing get()/unwrap() calls as PHPStan errors the moment you wire the extension in. Either (a) adopt the extension and fix the call sites in the same PR — the right move on a greenfield service — or (b) baseline the existing violations so the guard prevents new ones without blocking the upgrade. Do not skip the upgrade to avoid the churn; the extension is the main reason to trust this package in production code.

Extension points are thin. Expect final classes and no Laravel macros. The PHPStan extension is a read-only consumer — it doesn't give you a hook for custom rules or behaviour. If you need logging on failure, retries, or collection semantics, add it in your façade rather than subclassing.

Debugging. Stack traces end at fold, which obscures where a value went wrong. Keep dd($monad) working — if the class exposes __debugInfo or a toString, use it; otherwise unwrap with ->getOrElse('EMPTY') at the suspect point and re-run. Note this is the one place where calling a non-panicking accessor explicitly is fine; if you're inside a phpstan run, getOrElse won't trip the v1.3.0 rules.

When not to use it. Don't refactor controllers, form requests, Blade templates, or existing ?-> chains that already read fine. The package's payoff is concentrated in service-layer pipelines with three or more fallible steps; everywhere else it adds vocabulary the next developer has to learn for no benefit. The PHPStan extension is the exception: turn it on across the codebase even in files that never build a monad by hand, since it also guards against panicking accessors in helper code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource