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

Interoperability Laravel Package

php-standard-library/interoperability

Interfaces for converting between PSL types and native PHP stdlib and intl equivalents, providing a consistent bridge so your code can move smoothly between PSL collections, strings, and other types and their built-in PHP counterparts.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal setup

composer require php-standard-library/interoperability

That is the whole install. There is no service provider, no config file, no facade, no publish step. The package is a leaf set of interfaces — it ships contracts, not behaviour. Every conversion you get out of it is code you write.

Where to look first

  1. vendor/php-standard-library/interoperability/src/ — read this before the docs. The interfaces are small and flat; ten minutes of ls + cat is worth more than any blog post. Enumerate them once and keep the list in your team notes.
  2. https://php-standard-library.dev — the canonical docs, but they document the whole PSL monorepo. Filter to the interoperability namespace.
  3. php-standard-library/php-standard-library (the monorepo) — the split repo you installed is a read-only subtree split. Releases here are one-line stubs pointing back to the monorepo, and issues/PRs filed on the split repo go nowhere. File upstream.

Choosing your first use case

The package earns its keep at exactly one place in an application: the boundary. Pick the seam with the most traffic and the most friction. In a Laravel codebase that is almost always dates:

  • Domain/jobs/services speak Psl\DateTime\DateTime.
  • Eloquent columns, DateTimeInterface type hints, Carbon, queues, and third-party SDKs speak DateTimeImmutable.

A second common first case is handing a Psl\Collection\CollectionInterface to something that only accepts iterable/array.

Concretely, your first day looks like:

ls vendor/php-standard-library/interoperability/src
grep -rn "interface " vendor/php-standard-library/interoperability/src

Then write one adapter class in app/Support/Interop/, an Eloquent cast or a small ServiceProvider binding, and a round-trip test. Do not convert anywhere except that boundary.

Rule of thumb: if you cannot name the two systems being bridged (e.g. "PSL DateTime ↔ Eloquent datetime column"), you do not need this package yet.


Implementation Patterns

Every interface in this package is one direction of one conversion. Two shapes dominate in practice: to*() (PSL → stdlib/intl) and from*()/static factories (stdlib/intl → PSL). Your job is to implement the interface on a small class that does the translation and nothing else.

Pattern 1 — Adapter at the boundary (ports & adapters)

Keep PSL types inside the domain, convert at the edge. Never let a conversion leak mid-service.

namespace App\Support\Interop;

use Psl\DateTime\DateTime;

final class PslDateAdapter implements /* the package's DateTime interop interface */ 
{
    public function __construct(private readonly DateTime $value) {}

    // implement the interface's toStdlib-equivalent method by returning
    // a DateTimeImmutable built from PSL's unix timestamp + nanoseconds
}

Two hard rules that keep this maintainable:

  • Adapters are stateless and final. No caching, no logging side effects, no business logic.
  • Adapters live in one namespace (App\Support\Interop) so a grep finds every conversion in the app.

Pattern 2 — Eloquent attribute casts

The single highest-value integration in a Laravel app. Convert in the cast so models, resources, and controllers never see the mismatch.

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Psl\DateTime\DateTime;

final class PslDateTimeCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ?DateTime
    {
        if ($value === null) {
            return null;
        }

        // stdlib -> PSL via the package's from* interop contract
        return DateTime::fromString($value);
    }

    public function set($model, string $key, $value, array $attributes): ?string
    {
        if ($value === null) {
            return null;
        }

        // PSL -> stdlib via the package's to* interop contract
        return $value->format('Y-m-d H:i:s.u');
    }
}

Because casts are resolved per model instance, keep the adapter cheap. If the interface exposes a static factory, use it — it usually avoids constructing an intermediate object.

Pattern 3 — Contract-first container binding

When several implementations of the same concept exist (Redis ↔ Memcached, Stripe ↔ PayPal, Guzzle ↔ Symfony HttpClient), bind the interop interface once rather than sprinkling instanceof checks.

// app/Providers/InteropServiceProvider.php
public function register(): void
{
    $this->app->singleton(SomeInteropInterface::class, PslBackedAdapter::class);
    $this->app->alias(SomeInteropInterface::class, 'interop.some');
}

Then type-hint the interface in constructors and let Laravel resolve it. This is the only place where the package gives you leverage without you writing conversion logic — the DI graph stays framework-agnostic.

Pattern 4 — Testing seam

The best reason to accept this abstraction: it makes the boundary mockable without pulling in PSL internals.

$fake = new class implements SomeInteropInterface {
    public bool $called = false;
    public function toStdlib(mixed $value): mixed
    {
        $this->called = true;
        return $value;
    }
};

Bind the anonymous implementation in a test container and assert the conversion happened. Your unit tests then never depend on ext-intl, ICU versions, or PSL internals.

Pattern 5 — Authoring a reusable package

If you publish an internal package consumed by both Laravel and Symfony services, depend on interoperability rather than a concrete psl/* implementation package. You get the contract without forcing the whole PSL runtime on every consumer — that is the entire point of an interfaces-only package.

Pattern 6 — Anti-corruption layer for third-party SDKs

Wrap an SDK's native types behind an interop interface so upgrades to the SDK touch one file. This is a manual adapter, but it aligns with the package's vocabulary and stops the SDK's types spreading through your domain.

Workflow summary

Step Action Where
1 Enumerate available contracts vendor/.../src
2 Pick one seam (usually DateTime) —
3 Write one stateless adapter app/Support/Interop
4 Bind / cast at the edge AppServiceProvider or casts
5 Round-trip test both directions tests/Unit/Interop
6 Repeat only for the next real friction point —

Gotchas and Tips

Maturity and risk

  • 1 star, 0 dependents, one-line release notes. Treat the interfaces as a specification you have adopted, not a dependency you can lean on. Pin the exact version in composer.json and be prepared to fork if upstream abandons it — it is MIT and interfaces-only, so forking is genuinely cheap.
  • Because it ships no implementations, "it broke" almost always means "your adapter is wrong". Don't spend an hour debugging the package before reading your own class.

Versioning

  • psl/interoperability is versioned in lockstep with the monorepo (6.1.x, 6.2.x). Keep every psl/* package on the same minor. Mixing 6.1 and 6.2 contracts produces signature mismatches that the split repo's release notes will never explain to you.
  • Read the monorepo release page for change detection; the split's release/*.md files are literally "see the main repository".

PSR confusion

  • This is not PSR-3 (logging), PSR-7 (HTTP messages), PSR-11 (containers), or PSR-15 (middleware). Those already have standard interfaces. Do not introduce a PSL interface to satisfy a PSR-shaped need, and vice versa.
  • Laravel's Illuminate\Contracts\* already covers logs, queues, and buses. Adopt interoperability only where the overlap is types (dates, collections, streams, intl), not architecture.

Psl\DateTime is not a DateTimeInterface

The sharpest edge in the whole ecosystem. Psl\DateTime\DateTime does not implement PHP's DateTimeInterface, so it explodes in:

  • Carbon/Illuminate\Support\Carbon helpers
  • Doctrine DBAL datetime types
  • Anything type-hinted DateTimeInterface (Laravel's RateLimiter, Cache TTLs, signed URLs)
  • Default Eloquent datetime casts

Interop is exactly the fix — but convert at the boundary, never deep in a service. If you find a toStdlib() call three layers into your domain, the design has already failed.

Lossy round-trips

  • PSL preserves nanoseconds and an explicit timezone; DateTimeImmutable truncates to microseconds and setTimezone() mutates the calendar time. A round-trip is not always identity.
  • Write bidirectional tests with a fixed instant in a non-UTC zone. Assert A→B→A === A, not just A→B formatting.
  • Pick your storage precision deliberately (Y-m-d H:i:s.u vs second precision) and document it in the cast.

ext-intl is optional

  • Every intl-flavoured conversion needs ext-intl, which is not guaranteed on minimal Docker images (php:*-cli-alpine without intl-extra).
  • Guard it: add ext-intl to composer.json require or keep intl adapters behind a service-provider if (extension_loaded('intl')) check.
  • ICU version drift: collator/transliterator/number-formatter output varies across ICU versions. CI running a different base image than prod can produce different strings. Pin the base image, and never assert on formatted intl output without normalizing first.

Debugging

  • Stack traces now cross an adapter boundary. Add __debugInfo() returning the wrapped value so dd()/dump() is legible, and re-throw with context: adapter exceptions should name the source type and target type.
  • Fail loudly. Never silently coerce an invalid input — throw InvalidArgumentException from the adapter so the boundary is enforced, not implied.
  • Eloquent cast caches are per-model; a stateful adapter will bite you in queue workers that reuse model instances.

Extension points

  • The package is intentionally thin. If the interface you need does not exist, define it in App\Contracts\Interop and mirror the package's naming convention. Do not wait upstream.
  • A local App\Contracts\Interop namespace also protects you on the day you decide the package isn't worth keeping — a composer remove then costs you a regex, not a refactor.
  • Prefer one interface per direction over a fat bidirectional ConverterInterface. It keeps implementations trivially testable and mockable.

When not to adopt

Skip it if the stack is homogeneous Laravel (first-party contracts already cover it), if there's a single one-off integration, or if the "abstraction" would exist for one implementation. The package's own value proposition — dependency-agnostic contracts across frameworks — evaporates the moment you only have one framework.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums