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

Nihilus Laravel Package

wynd/nihilus

A lightweight Laravel package that lets you cast Eloquent attributes to custom value objects, keeping domain logic strongly typed, reusable, and out of your models for cleaner, more expressive code.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require wynd/nihilus

Open src/ first. That is the whole package: a flat set of interfaces — Command, Query, CommandBus, QueryBus, CommandHandler, QueryHandler, CommandMiddleware, QueryMiddleware. There is no container wiring, no autoloading of handlers, no concrete bus, no exception hierarchy. "Zero dependencies" means zero implementation as well as zero third-party packages. Read composer.json for the PHP constraint before adding it to a modern project; the last tag is from November 2019.

First use case: take one existing service method and re-express it as a command + handler, plus the smallest bus you can get away with.

use Wynd\Nihilus\{Command, CommandHandler};

final class RegisterUser implements Command
{
    public function __construct(public readonly string $email) {}
}

final class RegisterUserHandler implements CommandHandler
{
    public function __construct(private UserRepository $users) {}

    public function handle(Command $command): void
    {
        assert($command instanceof RegisterUser);
        $this->users->save(new User($command->email));
    }
}

Then write the adapter the package deliberately does not ship:

final class MapCommandBus implements CommandBus
{
    /** @var array<class-string<Command>, callable(Command): CommandHandler> */
    private array $handlers = [];

    public function register(string $commandClass, callable $resolver): void
    {
        if (isset($this->handlers[$commandClass])) {
            throw new LogicException("Handler already registered for $commandClass");
        }
        $this->handlers[$commandClass] = $resolver;
    }

    public function dispatch(Command $command): void
    {
        $handler = ($this->handlers[$command::class])($command);
        $handler->handle($command);
    }
}

That twenty-line class is the actual "getting started" cost. Everything after it is your own architecture.

Implementation Patterns

Handler map, not autowiring. Nihilus gives you no convention for finding a handler from a command. In Symfony, write a compiler pass that collects nihilus.command_handler tagged services and builds the class-string map; in Laravel, do the same in a service provider with $this->app->tag(...). Either way the resolver should be a closure over the container, so handler construction stays lazy — a bus that instantiates every handler on boot is the fastest way to make a cold request slow.

Middleware is the only cross-cutting hook. Every real use of this package ends up with three or four middlewares: TransactionMiddleware (wraps $next in beginTransaction/commit/rollBack), LoggingMiddleware, ValidationMiddleware, and a TraceMiddleware for debugging. Keep the pipeline explicit and ordered — the outermost middleware sees every dispatch, so put logging and tracing there and the transaction one level in.

final class TransactionMiddleware implements CommandMiddleware
{
    public function __construct(private Connection $db) {}

    public function handle(Command $command, callable $next): void
    {
        $this->db->beginTransaction();
        try {
            $next($command);
            $this->db->commit();
        } catch (\Throwable $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}

Queries return read models, not entities. QueryBus::ask() has no return type, so PHPStan cannot help you. Type your result at the handler's @return annotation and treat the calling code as the validation boundary. A common pattern is one query handler per screen/composition rather than per entity — that is where CQRS pays off and where a generic findById query handler wastes the abstraction.

Commands return void. The interface enforces this, which is a feature: generate IDs at the call site (Uuid::uuid4() before dispatch) or emit a domain event. If you find yourself wanting the handler's return value, you wanted a query.

Framework bridge over framework replacement. If the app already uses Tactician or Symfony Messenger, keep Nihilus as the domain-facing contract and implement CommandBus/QueryBus as thin delegates onto the framework bus. Handlers stay framework-agnostic; you keep the option to swap buses without touching domain code.

Testing is trivial and that is the point. Handlers are plain classes — instantiate with fakes and call handle(), no bus in the test. For code that dispatches, inject a RecordingCommandBus test double that appends to an array, and assert on the recorded commands.

Gotchas and Tips

The interface signature is wider than your handler. handle(Command $command): void means you cannot declare handle(RegisterUser $command) — PHP will reject the narrower parameter. Every handler narrows internally with assert() or an instanceof guard. This surprises people on day one and is the single most common source of "why won't this implement" errors. Verify the exact signatures in src/ before writing your first handler; they are the contract.

Forgetting return $next($command) silently truncates the chain. The middleware callable $next requires you to return its value, and for commands that value is void, so a missing return looks harmless and passes tests until a middleware three levels deep never runs. Add an integration test that asserts a marker middleware was reached.

Zero dependents and a 2019 tag means you are the maintainer. No security patches, no PHP 8.2+ deprecation fixes, no upstream answers to GitHub issues. MIT plus ~200 lines of interfaces makes forking or vendoring essentially free — if you adopt this, copy the interfaces into your own namespace rather than depending on an unmaintained package. That also removes the "why is our CQRS vocabulary a third-party dep?" question from every architecture review.

No reflection-based handler resolution. Unlike Messenger or Tactician, nothing scans your handler for its command type. You must maintain the map yourself; if you build it by reflecting on handle()'s parameter, note the parameter is always Command, so reflection gives you nothing. Registration must be explicit, per command class.

Duplicate registration should throw. The example above does. Silently overwriting the handler map turns a copy-paste bug into a production routing error that is very hard to diagnose.

Nested dispatch and transactions. A command handler that dispatches another command inside the same transaction middleware will, depending on your implementation, either join the outer transaction (usually what you want) or begin a nested one (which most drivers fake with savepoints). Decide deliberately and document it; don't discover it during an incident.

Debugging deep stacks. Bus → resolver closure → container → handler → repository means stack traces are long and the interesting frame is at the bottom. The TraceMiddleware tip above — log $command::class at the outermost layer — saves more time than any debugger configuration.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager