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

Comparison Laravel Package

php-standard-library/comparison

Interfaces and functions for type-safe, consistent value comparison in PHP, part of the PHP Standard Library. Provides predictable equality and ordering semantics across values, helping you write reliable, uniform comparison logic in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm what you actually got

composer require php-standard-library/comparison

There is no service provider, no config file, no facade. It is a plain library: classes, interfaces, and namespaced functions autoloaded from Composer. If composer require succeeds and vendor/php-standard-library/comparison/src exists, you are done.

Because this package is a split of the larger php-standard-library monorepo, the first thing to do is read vendor/php-standard-library/comparison/src — the README is essentially a link stub, so the source and its tests are the real documentation. Expect something along these lines (names confirmed against your installed version):

  • Psl\Comparison\Order — an enum of Less / Equal / Greater (the return type of a comparison)
  • Psl\Comparison\Comparable — interface for objects that know how to compare themselves
  • Psl\Comparison\Equatable — interface for objects that know how to test equality
  • Psl\Comparison\compare(), is_equal(), is_less(), is_greater(), sort(), min(), max() — free functions wrapping the <=> operator consistently

Run a 30-second smoke test before committing to it:

use function Psl\Comparison\compare;
use Psl\Comparison\Order;

var_dump(compare(1, 2) === Order::Less);   // true
var_dump(compare('10', 9));                // whatever this version does — verify!

That second line matters. The whole value proposition of this package is that comparison behavior is defined once and consistently rather than being whatever PHP's loose operators happen to do. Confirm the semantics before you build on them.

First real use case in a Laravel app

Don't start by replacing if ($a > $b) everywhere. Start with the place where comparison rules are actually business rules: money and prices.

use Psl\Comparison\Comparable;
use Psl\Comparison\Order;

final readonly class Money implements Comparable
{
    public function __construct(
        public int $amount,       // minor units, e.g. cents
        public string $currency,
    ) {}

    public function compareTo(mixed $other): Order
    {
        if (!$other instanceof self) {
            throw new \InvalidArgumentException('Cannot compare Money to ' . get_debug_type($other));
        }

        if ($this->currency !== $other->currency) {
            throw new \DomainException("Cannot compare {$this->currency} to {$other->currency}");
        }

        return $this->amount <=> $other->amount;
    }
}

Now pricing tiers, discount thresholds, "cheapest plan", and sortBy callbacks in checkout code all route through one method, and cross-currency comparisons fail loudly instead of silently comparing integers. That is the day-to-day payoff: a single, testable comparison rule instead of a dozen scattered $a->amount < $b->amount checks.

Implementation Patterns

1. Value objects own their own comparison

This is the highest-leverage pattern. Anything you already treat as a value object — Money, DateRange, PhoneNumber, SemanticVersion — gets a compareTo() and stops leaking its internals into controllers and services.

final readonly class DateRange implements Comparable
{
    public function __construct(public CarbonImmutable $from, public CarbonImmutable $to) {}

    public function compareTo(mixed $other): Order
    {
        // ranges sort by start, then by end (shorter range first on ties)
        return [$this->from, $this->to] <=> [$other->from, $other->to];
    }
}

The multi-field tie-break is the key trick: PHP array comparison uses <=> element-wise, so [...] <=> [...] gives you deterministic multi-criteria ordering for free. Wrap it in the interface and every consumer gets the same ordering.

2. Sorting collections with an explicit comparator

Illuminate\Support\Collection::sort() takes a two-argument callable, which is exactly the shape of a comparator function:

$plans = Plan::all()->sort(
    fn (Plan $a, Plan $b) => $a->price->compareTo($b->price)
)->values();

For multi-criteria sorting, chain inside the closure rather than nesting sortBy calls:

$orders = $orders->sort(function (Order $a, Order $b) {
    return $a->total->compareTo($b->total)
        ?: $a->createdAt <=> $b->createdAt
        ?: $a->id <=> $b->id;   // final tie-break for determinism
});

The trailing $a->id <=> $b->id is not optional if the result is user-visible or paginated. PHP 8 sorting is stable, but "stable" only preserves the input order, and the input order from Model::all() is not guaranteed. Always end with a unique field.

3. Request-driven sorting (?sort=price:desc)

A common Laravel pattern: map a validated request param to a comparator. Keep the map explicit and allowlist the fields.

final class SortRequest
{
    private const ALLOWED = ['price', 'name', 'created_at'];

    public function apply(Collection $items): Collection
    {
        $raw = $this->validated('sort', 'created_at:asc');
        [$field, $dir] = array_pad(explode(':', $raw, 2), 2, 'asc');

        abort_unless(in_array($field, self::ALLOWED, true), 422);

        $cmp = match ($field) {
            'price'      => fn ($a, $b) => $a->price->compareTo($b->price),
            'name'       => fn ($a, $b) => $a->name <=> $b->name,
            'created_at' => fn ($a, $b) => $a->createdAt <=> $b->createdAt,
        };

        $sorted = $items->sort($cmp)->values();

        return $dir === 'desc' ? $sorted->reverse()->values() : $sorted;
    }
}

Note reverse() on a comparator-sorted collection is correct only if you also reverse the tie-break intent — reverse of a stable ascending sort is not the same as a stable descending sort. For anything user-facing, build the descending comparator directly (fn ($a, $b) => $cmp($b, $a)) instead of reversing.

4. Equality for DTOs, dedupe, and "has this changed?"

Implement Equatable (or just an equals() method) on request/response DTOs so idempotency checks and dirty-tracking stop relying on ==:

if ($incoming->equals($existing)) {
    return; // no-op, don't fire events
}

// Or dedupe a collection without array_unique's string-casting surprises
$unique = $items->unique(fn ($item) => $item->sku);

The win here is avoiding PHP's loose-comparison footguns ("0" == false, "1e3" == "1000") in code paths that decide whether to write to the database or dispatch a job.

5. Business rules: ranges and thresholds

$this->assertTrue($cart->total->compareTo($freeShippingThreshold) !== Order::Less);

For "between" checks on date ranges — booking overlaps, promotional windows, subscription validity — express the rule once in the value object:

public function overlaps(self $other): bool
{
    return $this->from <= $other->to && $other->from <= $this->to;
}

Then $booking->range->overlaps($request->range) reads as the business rule and is unit-testable without a database.

6. Container binding for the comparator (if you go that far)

If you adopt a Comparator-style object rather than static functions, bind it once in a service provider and inject it, so you can swap it in tests:

$this->app->singleton(Comparator::class, fn () => new Comparator());

Most Laravel codebases will get more value from interfaces on value objects than from injecting a comparator service. Prefer the former; it keeps comparison logic next to the invariants it enforces.

7. Testing pattern

Comparators are pure functions, so they deserve pure unit tests — no RefreshDatabase, no factories:

it('orders money by amount within a currency', function () {
    $a = new Money(100, 'USD');
    $b = new Money(200, 'USD');

    expect($a->compareTo($b))->toBe(Order::Less)
        ->and($b->compareTo($a))->toBe(Order::Greater)
        ->and($a->compareTo(new Money(100, 'USD')))->toBe(Order::Equal);
});

Add the three properties as invariants: antisymmetry ($a→$b is the inverse of $b→$a), reflexivity ($a→$a is Equal), and transitivity (three-element checks). Bugs in comparators almost always show up as a transitivity violation, and those bugs corrupt sorted output in ways that are very hard to trace in production.

Gotchas and Tips

The README won't help you — budget for source reading

The package README is a set of links to php-standard-library.dev and the monorepo. There is no API reference in the repo, the release notes for 6.1.x/6.2.x just say "see the main repository release," and there are zero dependents on the package as published. Treat the first hour as "read src/ and tests/," and pin the exact version in composer.json rather than floating on ^6.2 — a split package can move underneath you when the monorepo releases.

It cannot reach your database

This is the single biggest day-to-day limitation. A comparator sorts in PHP memory. It cannot translate to ORDER BY.

  • User::orderBy('created_at')->paginate(20) — fine, fast, indexed.
  • User::all()->sort($cmp)->forPage(...) — loads the whole table, then slices. This works in dev with 50 rows and falls over in production.

Rule: if the collection is paginated, sorted on an indexed column, or larger than a few thousand rows, do it in SQL. Use the package for in-memory ordering of already-scoped result sets, for value objects that map to multiple columns, or for business-rule comparisons. If you find yourself hydrating a whole table to sort it with a comparator, stop.

Null handling is your responsibility

Nothing in the package knows your opinion on where null belongs in a sort. Decide explicitly and encode it in the comparator, or you will get null sorted to the front in one endpoint and the back in another:

fn ($a, $b) => [$a->shippedAt === null, $a->shippedAt] <=> [$b->shippedAt === null, $b->shippedAt]

Floats are still floats

If you compare monetary or measured values as float, the comparator faithfully reproduces IEEE-754 problems — 0.1 + 0.2 will not equal 0.3, and NaN breaks transitivity entirely (NaN <=> NaN is not Equal). Store money as integer minor units or a decimal string and compare those. Never build a comparator whose correctness depends on float equality.

Strict vs. loose is a real decision, not a detail

The package's whole reason to exist is consistent semantics, but you still have to confirm which semantics the version you installed implements for mixed types. Write a test asserting the behavior you expect for 1 vs "1", 0 vs null, and true vs 1. If the library's answer differs from your domain's answer, wrap it — don't discover the mismatch through a sorting bug in production.

Function-call overhead is real in hot loops

Sorting 100k items means ~1.7M comparator invocations. Each one crosses into your compareTo(), which may then cross into the library. That's fine for a request-scoped collection; it is not fine inside a queue worker processing millions of rows. If profiling (php artisan tinker + hrtime, or Blackfire) shows the sort dominating, fall back to usort with the raw spaceship:

usort($rows, fn ($a, $b) => $a->amount <=> $b->amount);

Keep <=> as your documented escape hatch. Adopting the package shouldn't mean you're forbidden from ever writing an operator.

Always close with a unique tie-break

Any sort that ends in a user-visible list or a paginated view needs a deterministic final key. Without it, two items with equal prices can swap order between requests, and users see "phantom" reordering on refresh. ?->id at the end of every comparator chain.

Blade: don't put comparators in templates

Views should receive data already ordered and already reduced to booleans/strings. If you catch yourself writing comparison logic in a .blade.php, it belongs in a view model or the controller:

// controller / view model
'showFreeShippingBanner' => $cart->total->compareTo($threshold) !== Order::Less,
@if ($showFreeShippingBanner) … @endif

This keeps comparison rules testable and keeps php artisan view:cache from baking business logic into compiled templates.

Debugging: log both operands, not the comparison

The failure mode of a comparator abstraction is an opaque error. compareTo() throwing InvalidArgumentException deep inside Collection::sort() tells you nothing about which pair of objects failed. Wrap the comparator during development:

$cmp = function ($a, $b) use ($inner) {
    try {
        return $inner($a, $b);
    } catch (\Throwable $e) {
        logger()->error('comparison failed', ['a' => $a, 'b' => $b, 'ex' => $e]);
        throw $e;
    }
};

Also give every value-object comparator a __toString() or a toArray(). Exception messages like "cannot compare USD to EUR" are useless without the amounts.

Extension points worth knowing

  • Implement Comparable / Equatable on your domain objects — this is the primary extension point and where nearly all your value lives.
  • Implement the Comparator interface if you want a configurable, injectable strategy (e.g., a "relevance" comparator whose weights come from config).
  • Use the Order enum, not raw ints, in return types. It makes match exhaustive under PHPStan and makes $result === Order::Less readable at call sites.
  • Adapt, don't rewrite. For Eloquent models, an adapter (OrderComparator reading $order->total) keeps the comparison rule in one place even though the model itself isn't a value object.

Adopt incrementally, and enforce it with tooling

Refactoring every > and < in a mature codebase is a net-negative project. Pick one domain concept per sprint — Money, then DateRange, then the sort-request mapping — and pin the semantics with tests. If you want the rule to stick, add a PHPStan baseline or a Pint/rector rule scoped to the directories you've migrated, rather than trying to enforce it globally on day one.

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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode