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

Leo Laravel Package

peridot-php/leo

Leo is a lightweight assertion library for PHP and the Peridot BDD framework. It provides a fluent expect(...)->to syntax with readable matchers for equality, types, arrays, exceptions, and more, helping you write concise, expressive tests without heavy dependencies.

Deep Wiki
Context7

Getting Started

Leo is a BDD-style assertion layer built around a single global function, expect(). It was written for the Peridot spec runner but works standalone anywhere you can autoload a Composer dependency.

composer require --dev peridot-php/leo

Composer wires the expect() function in through the package's files autoload, so once the installer finishes you can call it immediately — no require, no use statement, no bootstrap:

expect(4)->to->equal(4);
expect([1, 2, 3])->to->have->length(3);
expect('laravel')->to->contain('ravel');
expect(null)->not->to->be->null;

The first place to look is vendor/peridot-php/leo/src/Leo/Matcher/. Every assertion Leo ships with is a small class in there implementing a one-method Matcher interface — read a handful and you will understand the whole library in ten minutes. Second stop is Expectation, which owns the fluent grammar (to, be, have, not) and dispatches method calls to the registered matchers.

The first use case to reach for is a quick, readable sanity assertion in a Peridot spec:

describe('Cart', function () {
    it('adds items', function () {
        $cart = new Cart();
        $cart->add(new Item('book', 12));
        expect($cart->count())->to->equal(1);
        expect($cart->total())->to->equal(12);
    });
});

Leo is a replacement for PHPUnit assertions, not a companion — do not try to mix it into a PHPUnit suite expecting assert* methods; keep it inside a Peridot project or use it as a lightweight standalone check in scripts and CLI tooling.

Implementation Patterns

Chain as grammar, terminate as matcher. The words to, be, have, an, a and not are noise words — they return the same Expectation instance. Only a matcher method actually asserts. That means the meaningful line is always the last segment of the chain, and readers scan for it:

expect($user->roles)->to->have->length(2);
expect($response->status())->to->be->equal(404);
expect($payload)->not->to->have->key('password');

Negation is a chain word, not a matcher variant. not flips the result of the next matcher call. It is sticky for that expectation only — build a new expect() for the next assertion.

Custom matchers are the real extension point. Implement Leo\Matcher\Matcher and register it once in a bootstrap file. This is how you keep domain vocabulary out of your specs:

namespace Spec\Matcher;

use Leo\Matcher\Matcher;

class BeExpired implements Matcher
{
    public function match($expected, $actual)
    {
        // return true on success, throw on failure
        return $actual->expiresAt < new \DateTime();
    }
}
use Leo\Leo;

Leo::registerMatcher('expired', new Spec\Matcher\BeExpired());

Then in specs: expect($token)->to->be->expired();

A matcher should return truthy on success and either return falsy or throw a descriptive exception on failure — check the bundled matchers for which convention each family uses so your custom one behaves consistently in failure output.

Peridot integration is zero-config. Because Leo throws on failed matches, Peridot's it() blocks surface failures as normal spec failures with the matcher's message. Nothing needs to be registered with the runner.

Build a matcher per team vocabulary, not per assertion. Resist writing isValidEmail() style one-offs; instead write matchers that express a shape of expectation (jsonStructure, respondsWith, withinRange) so they compose with not and with have/be chains.

Use it outside tests sparingly. expect() is handy in throwaway scripts and database seeding sanity checks, but for anything production-facing prefer explicit exceptions — Leo failures carry no error codes and are meant for humans reading test output.

Gotchas and Tips

  • The package is dormant. Last release was August 2017 and it is effectively unmaintained. On PHP 8+ you will hit dynamic-property deprecations in Expectation and may need #[\AllowDynamicProperties] shims or an error-suppression layer. Evaluate this before adopting on a modern stack.
  • Silent no-ops from typos. Because the grammar words are resolved via magic __get/__call, a misspelled chain word or matcher name can behave unpredictably instead of throwing a clear "no such matcher" error. If an assertion seems to pass suspiciously, check spelling first and add a deliberately failing expectation to confirm the matcher actually fires.
  • Chains do not short-circuit. expect($x)->to->be->a('string')->and->contain('foo') is not a thing — one expect() per assertion. This is a feature (clear failure attribution) but surprises people coming from fluent PHPUnit/Assert chains.
  • Loose comparison inside matchers. Most bundled matchers use == semantics. If you need strict type checks, write a custom matcher rather than assuming equal() is ===.
  • Failure messages live in the matcher. Debugging a confusing failure usually means opening the matcher class and reading its exception text, not stepping through Expectation. The Matcher contract is where message quality is decided.
  • Autoload ordering matters. Because expect() comes from a Composer files autoload, it is available as soon as vendor/autoload.php runs. If you see "Call to undefined function expect()", you are almost certainly running a script that bypasses Composer's autoloader, or Leo landed in require-dev and your CI installs with --no-dev.
  • Do not upgrade the expect global casually. Leo defines a global expect() function; another library defining the same name will cause a fatal redeclaration. Check dependencies before adding assertion libraries alongside it.
  • Keep custom matchers tiny and pure. They are easiest to trust and test when they take the actual value, do one comparison, and either return truthy or throw with a message that names both the expected and actual value.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings