roflcopter24/symfony-livewire-bundle
A Symfony bundle that brings Livewire-style reactive components to Symfony apps, enabling dynamic, server-rendered interfaces built primarily in PHP with minimal JavaScript and seamless component updates.
Before you type a single command, read the bundle's composer.json. The livewire major version it pins determines almost everything downstream: Livewire 2 dispatches updates to one route per component (/livewire/message/{name}), Livewire 3 uses a single /livewire/update POST, and the JS/Alpine story differs between them. A 0-star, 0-dependent bundle like this one is a bridge between two frameworks whose internals both move fast — the pinned version is your contract.
# If it isn't on Packagist (likely), pull it from VCS:
composer config repositories.livewire vcs https://gitlab.com/RoflCopter24/symfony-livewire-bundle
composer require roflcopter24/symfony-livewire-bundle:dev-main
Then:
config/bundles.php:
RoflCopter24\SymfonyLivewireBundle\SymfonyLivewireBundle::class => ['all' => true],
bin/console debug:container --tag=kernel.event_subscriber | grep -i livewire
bin/console debug:router | grep -i livewire
If there's no route, there is no Livewire — the update endpoint is the whole product. Register it yourself as a fallback:
#[Route('/livewire/update', name: 'livewire.update', methods: ['POST'])]
public function update(Request $request): Response { /* forward to Livewire's handler */ }
templates/livewire/ and src/Livewire/).vendor/livewire/livewire/dist/livewire.js served through your normal asset pipeline.First use case: a counter. It exercises the full loop — initial render, snapshot, POST, re-render — with nothing else to blame.
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $count = 0;
public function increment(): void { $this->count++; }
public function render() { return view('livewire.counter'); }
}
{{-- templates/livewire/counter.blade.php --}}
<div>
<button wire:click="increment">+</button>
<span>{{ $count }}</span>
</div>
The initial mount is the part least likely to be standardized. Check for a Twig extension ({{ livewire('counter') }}); if none exists, mount from a controller with Livewire::mount('counter')->html() and pass the HTML into Twig with |raw.
Components as thin presentation shells. Livewire components get resolved from the Illuminate container, not Symfony's. Constructor-injecting a Symfony service usually fails or yields an uninitialized stub. The reliable pattern is to keep components dumb — properties in, actions out — and reach for Symfony services explicitly:
public function search(MyRepository $repo) // method injection is the safer seam
{
$this->results = $repo->find($this->query);
}
If method injection isn't honored either, bind the service into the bridge container in a compiler pass or a bundle bootstrap, and resolve it via the facade. Treat every constructor dependency as something to verify with a dd() early.
Embedded vs. full-page. Embedded components (mounted inside a Twig template) are the day-to-day workhorse. Full-page components need the Symfony route to render the component's initial HTML server-side and the Livewire route to handle updates — two entry points, one component. Keep full-page components on routes you own so Symfony's firewall and access control still apply.
Symfony Security stays outside. Livewire's authorize() and policies are Laravel constructs and won't see your voters. Put access checks in the Symfony controller that mounts the component, and re-check inside actions with an injected Security:
public function delete(int $id): void
{
$this->security->denyAccessUnlessGranted('ROLE_ADMIN');
// ...
}
Validation is a fork in the road. Livewire runs Illuminate's validator, so #[Assert\...] attributes are invisible to $this->validate(). Either write Laravel-style rules ('email' => 'required|email') and accept the duplicate schema, or validate in a Symfony Form/Validator inside the action and map errors back onto public properties. Don't half-do both.
Forms and file uploads. Uploads go through Livewire's temporary-upload endpoint, which writes to a disk configured on the Laravel side, not Symfony's Flysystem bundle. Point it at var/uploads and handle the move yourself in the action.
Testing. Livewire::test() needs the Illuminate app booted; under KernelTestCase it usually isn't. The pragmatic suite is HTTP-level: boot the kernel, render the mount route, scrape the snapshot, POST to /livewire/update with a forged payload, assert on the returned JSON effects.html. Slower, but it tests the seam that actually breaks.
debug:container shows Symfony's bindings only. When a class "can't be resolved," you are almost certainly looking in the wrong container. Keep a tiny command that dumps the bridge container's bindings for sanity._token, Livewire posts _token from its own session guard. The /livewire/update route will 419 until you either exempt it in security.yaml (safe-ish: Livewire has a payload checksum) or bridge the token into the Blade snapshot. Symptom: component renders fine, every action returns 419 in the network tab and nothing on the page.livewire.js carry matching checksums. A stale asset produces "corrupt component snapshot" errors that look like serialization bugs. Never upgrade one without the other.var/cache. Illuminate wants storage/framework/views. Point view.compiled at var/cache/blade, create it, and add it to your deploy cache:clear script — Symfony's clear won't touch it, and stale Blade is a classic "my change didn't deploy."asset(), path(), encore_entry_link_tags(), and trans don't exist there. Either register Blade directives for them in the bundle bootstrap or hardcode paths and accept the debt.wire:navigate will double-render or drop listeners. Scope Turbo off for Livewire-bearing pages before you debug anything else.x-data will behave erratically. Load exactly one.effects and, on failure, an error bag — exceptions surface as a generic toast. Flip the bundle's debug/log config and check the Network tab's response body before touching the PHP.kernel.debug ≠ APP_DEBUG. The two frameworks read different flags. Set both in .env.local or you'll chase ghost caching.repositories: {type: path}) or fork it the moment you depend on it for production, and pin the commit SHA. Budget a day to read the bundle's bootstrap — it's small, and it's the only real documentation.Extension points worth knowing: the bundle's compiler pass (where Symfony services get exposed to the Illuminate container), the facade/Livewire static entry point for out-of-Blade rendering, and the update controller — overriding that controller is the cleanest way to add tenancy, locale switching, or a custom auth guard to every component at once.
How can I help you explore Laravel packages today?