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.
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.
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.https://php-standard-library.dev — the canonical docs, but they document the whole PSL monorepo. Filter to the interoperability namespace.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.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:
Psl\DateTime\DateTime.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.
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.
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:
App\Support\Interop) so a grep finds every conversion in the app.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.
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.
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.
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.
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.
| 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 | — |
composer.json and be prepared to fork if upstream abandons it — it is MIT and interfaces-only, so forking is genuinely cheap.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.release/*.md files are literally "see the main repository".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 DateTimeInterfaceThe sharpest edge in the whole ecosystem. Psl\DateTime\DateTime does not implement PHP's DateTimeInterface, so it explodes in:
Carbon/Illuminate\Support\Carbon helpersDateTimeInterface (Laravel's RateLimiter, Cache TTLs, signed URLs)datetime castsInterop 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.
DateTimeImmutable truncates to microseconds and setTimezone() mutates the calendar time. A round-trip is not always identity.A→B→A === A, not just A→B formatting.Y-m-d H:i:s.u vs second precision) and document it in the cast.ext-intl is optionalext-intl, which is not guaranteed on minimal Docker images (php:*-cli-alpine without intl-extra).ext-intl to composer.json require or keep intl adapters behind a service-provider if (extension_loaded('intl')) check.__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.InvalidArgumentException from the adapter so the boundary is enforced, not implied.App\Contracts\Interop and mirror the package's naming convention. Do not wait upstream.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.ConverterInterface. It keeps implementations trivially testable and mockable.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.
How can I help you explore Laravel packages today?