symfony/ux-native
Symfony UX Native lets you build native iOS and Android apps using Symfony, Twig, and Turbo, wrapping your server-rendered web UI in native shells. Reuse existing Symfony skills and components to deliver mobile experiences without a separate frontend stack.
This is a Symfony UX bundle, not a Laravel package. Before anything else, confirm whether you actually need it: symfony/ux-native is the Symfony-side integration for Hotwire Native (the 37signals hybrid mobile app framework), and its value is mostly in (a) detecting native requests, (b) generating the Hotwire Native path configuration JSON, and (c) providing Twig helpers/bridge markup. All three are thin enough to reimplement in a Laravel app in an afternoon.
Minimal steps if you still want to pull it in:
composer require symfony/ux-native and immediately check composer.json for the required symfony/http-kernel, symfony/dependency-injection, and symfony/config version constraints — if they conflict with your Laravel/Symfony transitive deps, stop here and reimplement instead.NativeServiceProvider; Symfony bundles register through config/bundles.php and the framework's kernel. In Laravel you must manually instantiate the runtime classes you need (native detector, path-config builder) inside a small NativeBridgeServiceProvider, or just copy the logic. Do not try to boot NativeBundle — it expects KernelInterface, the Symfony event dispatcher, and bundle config merging.NativeBundle.php (what it registers), the Native runtime class (the UA/context checks), the Twig extension (what helpers you'll need to port to Blade), and the path-configuration generator (the only genuinely non-trivial piece).Hotwire Native / Turbo Native in the User-Agent as a request attribute, plus a GET /native/path-configuration.json route. That is the entire minimum viable integration — everything else is layout branching.A realistic outcome for most Laravel teams: install it once to read the source, then delete it and write ~150 lines of your own code.
Native detection middleware. Hotwire Native apps append a token to the webview UA (e.g. ... Safari ... Hotwire Native iOS; Turbo Native Android). Register a global middleware that checks the UA, sets $request->attributes->set('native', true), and optionally captures the platform (ios/android). Share the flag with views via a view composer so Blade can branch layouts.
Dual layout strategy. Keep one Blade layout per platform ("web" and "native"). The native layout strips your site chrome (header, footer, nav) because the native shell provides the navigation bar. This is the single biggest day-to-day behavior: @extends($native ? 'layouts.native' : 'layouts.app') or a @yield/component swap.
Path configuration endpoint. This is the core Hotwire Native contract. Serve JSON from a route and keep the patterns in config/native.php:
// config/native.php
return [
'settings' => ['screenshots_enabled' => true],
'rules' => [
['patterns' => ['/login$'], 'properties' => ['context' => 'modal']],
['patterns' => ['/checkout/.*'], 'properties' => ['context' => 'modal', 'pull_to_refresh_enabled' => false]],
['patterns' => ['/.*'], 'properties' => ['context' => 'default']],
],
];
Order matters — most specific first. Cache it (Cache-Control: public, max-age=3600) because the native app fetches it once per launch.
Turbo must actually work. ux-native assumes Turbo Drive/Frames/Streams are enabled on the web side. In Laravel that means @hotwired/turbo via Vite, data-turbo-frame attributes in Blade, and honoring Turbo-Frame request headers (Laravel returns the full page by default; you need to return frame-only partials). Without Turbo working, the Symfony package's helpers buy you nothing.
Native bridge components. Hotwire Native exposes bridge components (button, menu, form). The Symfony package likely renders them through Twig components; port the equivalent HTML (the data-* bridge attributes) into Blade components named x-native.button, x-native.menu. Keep these in one directory so upgrades are auditable.
Form and navigation flow. Always POST → redirect → GET. Native navigation treats a 200 HTML response to a form submission as a "replace" and can leave the back stack in a weird state. Flash messages work, but they must render inside the native layout or the user never sees them.
Auth and sessions. The webview shares cookies with the origin, so Laravel session auth works, but you must get the cookie domain right. Set SESSION_DOMAIN to the host the native app loads, use SameSite=Lax (not Strict), and make sure APP_URL matches scheme/host or asset and redirect URLs will point to the wrong place. For a Sanctum-flavored setup, add the native host to SANCTUM_STATEFUL_DOMAINS.
Testing. Add a feature test helper that sets the native UA ($this->withHeader('User-Agent', '... Hotwire Native iOS ...')) and assert on layout selection and the path-config JSON. Also add a "debug override" query string (?native=1) so QA can test in a desktop browser.
NativeBundle requires KernelInterface, Bundle config merging, and the Symfony event dispatcher. Either bootstrap the handful of classes manually or treat the repo as a spec.native_* functions, Twig components) has to be ported. Don't assume you can call them from Blade.Illuminate\Http\Request extends Symfony\Component\HttpFoundation\Request, so runtime classes accepting the Symfony request will usually work. But anything resolving services from a container or reading bundle config (symfony/ux-native's Configuration.php) will throw — there is no native config tree in Laravel.composer.json and vendor-review the diff on every bump. The API surface (particularly attribute names on path-config rules and Twig helper signatures) is the most likely thing to churn.Hotwire Native/Turbo Native token, not the whole string, and always provide a ?native=1 escape hatch for debugging.$/^ behavior differs from PHP's preg_match. Test each rule against the actual URL the app requests (/orders/123, not the full origin).context: modal vs default is the most common source of "double nav bar" and "back button does nothing" bug reports. When in doubt, leave default and add a native close button manually.asset() and route() are fine, but if APP_URL is http://localhost in a staging build, the native shell will fail to load pages. Fail loudly at boot if APP_URL scheme isn't https outside local.SameSite=None; Secure is required if the native shell ever loads from a different host. Verify in the simulator, not just a browser./native/path-configuration.json hits; listen for turbo:before-fetch-request in the webview's remote inspector (Safari → Develop → Simulator, or chrome://inspect for Android). Log the resolved path-config rule per request during development.PathConfiguration interface and the detection behind a single middleware. If you later drop the Symfony dependency, you swap two classes instead of chasing call sites.How can I help you explore Laravel packages today?