gehrisandro/tailwind-merge-php
TailwindMerge for PHP merges multiple Tailwind CSS classes and resolves conflicts automatically, keeping only the class defined later. A PHP port of tailwind-merge supporting Tailwind v3.0 through v3.4, requiring PHP 8.1+, with caching and configurable options.
Architecture fit. tailwind-merge-php is a stateless, framework-agnostic pure-PHP port of the JavaScript tailwind-merge library. Its contract is narrow and unambiguous: accept one or more class strings/arrays, return a single string with earlier classes removed where a later class wins within the same Tailwind class group. It owns no state, no request lifecycle, no rendering, and no persistence — which is exactly the profile a TPM wants when inserting a dependency into an established Laravel codebase. v1.0.0 deliberately removed the illuminate/support dependency, so there is no hidden coupling to a Laravel version line and no risk of it dragging framework internals into a domain or service layer. It can sit inside Blade components, Livewire actions, API resource transformers, mail/PDF builders, or a queue worker without violating any boundary we've drawn.
Integration feasibility. High for a Tailwind v3 estate. One Composer require, one call (TailwindMerge::instance()->merge(...)), plus an optional factory for configuration and a PSR-16 cache. The input flexibility — string, array, or variadic mix — matches idioms Laravel developers already use ($attributes->merge(), conditional class arrays, component prop bags), so adoption is a refactor of existing call sites rather than a new pattern to learn. The PSR-16 cache hook maps directly onto Laravel's cache repository, giving us a performance lever without custom plumbing. For Laravel specifically there is a sibling package (tailwind-merge-laravel) that supplies the service provider, facade, and Blade directive; that wrapper should be the default entry point rather than hand-wiring the core.
Technical risk. Moderate, and unusually well-localized — four items rather than a diffuse cloud:
tailwind.config.js must be mirrored into PHP configuration. Two sources of truth for design tokens is a permanent maintenance tax, and the failure mode is silent: merges succeed, output is wrong.Key questions.
tailwind-merge have a v4 story this port could follow?tailwind.config.js is non-default, and who is named owner of keeping the PHP config in sync?tailwind-merge-laravel wrapper, and does the wrapper's release cadence actually track the core?Stack fit. PHP 8.1+ only. No framework dependency, no PHP extensions, no Node or build-time coupling. Composer install, PSR-4 autoload, done. It coexists with any Laravel 9/10/11 line and with non-Laravel PHP services (queue workers generating mail, PDF renderers). Because it is stateless and pure, it is safe in long-running processes — Octane, Swoole, queue workers — provided the configured instance is constructed once as a singleton and not mutated per request. That is a one-line container binding and a code-review rule, not an architectural commitment.
Migration path. Position this as an additive capability, never a replacement. Realistic sequencing:
ClassMerger interface in the container; write the internal interface so call sites never touch the vendor namespace.tailwind.config.js and translate it into the PHP configuration. This is where schedule risk lives — it is discovery-heavy, not development-heavy.!important, and responsive/dark stacking. This suite is the acceptance gate for every upgrade.Compatibility. Two axes to pin explicitly in the ticket: Tailwind version on the front end (must stay within 3.0–3.4) and PHP version on the back end (8.1 floor). Both should be asserted in CI, and a Tailwind upgrade should be blocked on a corresponding merger upgrade. Treat the Tailwind version as a shared constraint between front-end and back-end tracks — this is a coordination item, not a build item.
Sequencing relative to other work. Lowest-friction window is during an existing component-library refactor, when class-building call sites are already being touched. It should not be sequenced ahead of a design-token consolidation effort; if we are about to change the token model, doing the PHP config mirror first means doing it twice. That dependency is worth flagging early.
Maintenance. Low code churn, non-trivial configuration churn. The package itself is a commodity; the ongoing cost is the design-token mirror and periodic upgrades tracking upstream JS releases. Allocate a fractional owner — not a project, a standing responsibility. Budget one upgrade cycle per Tailwind minor bump and expect the fixture suite to catch regressions.
Support. Single maintainer, MIT, thin changelog. Assume no SLA and no vendor escalation path. Mitigate with three cheap hedges: pin the version in composer.json with a conservative constraint, keep the internal ClassMerger interface so swapping or forking is a one-file change, and verify the license file is retained in any fork. A forked-copy contingency is realistic here precisely because the surface area is small.
Scaling. Computation is cheap; caching is optional but recommended anywhere merging runs per-request at volume or inside repeated render loops (mail batches, PDF generation, table rendering with hundreds of rows). The PSR-16 hook makes this a configuration decision rather than an engineering one. Note the documented caveat: cache keys must be invalidated when configuration changes, so a config change and a cache clear must ship as one unit — put that in the runbook.
Failure modes to instrument and document.
withConfiguration() on the injected instance and leaking configuration into subsequent requests in a long-running worker.Ramp-up. Effectively zero for developers — the API is one method and the mental model ("last class wins, conflicts resolved") is already how Tailwind developers think. The ramp-up cost sits with whoever owns the configuration translation and the fixture corpus, and with the reviewer who must enforce the "always go through ClassMerger" convention. Estimate the engineering effort as small and the configuration/discovery effort as the dominant line item; plan accordingly rather than sizing this as a pure dependency addition.
How can I help you explore Laravel packages today?