Livewire 4 a Dependency Injection: kde to funguje a kde ne
Kde v Livewire 4 selhává dependency injection a proč — pohled do zdrojáku a praktické řešení.
Dokumentace Livewire 4 u sekce lifecycle hooks říká jednu uklidňující větu: „You can use dependency injection with all hook methods.“ Je to pravda — ale jen pro lifecycle hooks. V komponentě je několik dalších míst, kde DI nefunguje a kde tě Livewire potichu nechá narazit do ArgumentCountError. Tenhle článek je o tom, kde a proč.
Krátké připomenutí: co je DI v Laravelu
Dependency Injection v Laravelu znamená, že místo manuálního vytváření závislostí (new SomeService()) jen type-hintneš parametr a service container ti instanci podstrčí sám. Funguje to ve třech podobách:
Konstruktor controlleru:
class InvoiceController extends Controller
{
public function __construct(
private InvoiceRepository $repository,
) {}
}
Method injection v controllerech a route closurech:
public function show(Request $request, InvoiceRepository $repository, int $id)
{
return $repository->find($id);
}
Manuální resolve přes container:
$repository = app(InvoiceRepository::class);
// nebo
$repository = resolve(InvoiceRepository::class);
Pod kapotou to dělá Illuminate\Container\Container::call(), který přes reflection projde parametry metody, podívá se na type-hinty a každý si vyresolvuje.
Jak to funguje v Livewire komponentě
V Livewire komponentě nepíšeš __construct — Livewire se komponentu rekonstruuje při každém subsequent requestu a konstruktor by se ti volal pokaždé bez tvých dat. Místo toho jsou tu lifecycle hooks (mount, boot, hydrate, …) a public metody jako akce.
Když se Livewire chystá zavolat metodu na komponentě, většinou ji obalí helperem wrap(), který vnitřně použije ImplicitlyBoundMethod::call() z Laravel kontejneru — a to je přesně ta cesta, která dělá DI. Příklad ze zdrojáku Livewire (SupportLifecycleHooks::callHook):
public function callHook($name, $params = [])
{
if (method_exists($this->component, $name)) {
wrap($this->component)->__call($name, $params);
}
}
A Wrapped::__call():
function __call($method, $params)
{
if (! method_exists($this->target, $method)) return value($this->fallback);
return ImplicitlyBoundMethod::call(app(), [$this->target, $method], $params);
}
Pokud volání metody projde tudy, DI funguje. Když ne, máš smůlu. A v Livewire 4 jsou minimálně dvě místa, kde Livewire wrap() nepoužívá a metodu zavolá rovnou jako $this->method() nebo přes invade().
Kde DI v Livewire 4 nefunguje
1. Computed properties (#[Computed])
Tohle je nejčastější past. Computed properties z dokumentace vypadají jako normální metoda na komponentě, ale Livewire je volá z BaseComputed::evaluateComputed():
protected function evaluateComputed()
{
return invade($this->component)->{parent::getName()}();
}
Žádný wrap(), žádný kontejner. Jakmile dáš metodě parametr s type-hintem, dostaneš:
ArgumentCountError: Too few arguments to function App\Livewire\Dashboard::stats(),
0 passed and exactly 1 expected
Tohle je známá regresse z přechodu Livewire v2 → v3 (existuje k tomu discussion #6967) a v Livewire 4 se to stále nezměnilo.
2. Validační metody: rules(), messages(), validationAttributes()
Trait HandlesValidation volá tyhle tři metody:
if (method_exists($this, 'rules')) $rulesFromComponent = $this->rules();
if (method_exists($this, 'messages')) $messages = $this->messages();
if (method_exists($this, 'validationAttributes')) $validationAttributes = $this->validationAttributes();
Takže pokud bys do nich chtěl přes type-hint dostat třeba RuleFactory nebo Translator, narazíš na stejný ArgumentCountError.
3. Pár dalších míst (méně časté)
Pro úplnost — getListeners() (v SupportEvents) a queryString() (v SupportQueryString) jsou volané přes invade(), takže taky bez DI. V praxi je to ale jen málokdy problém, protože do nich obvykle nic injektovat nepotřebuješ.
Jak to vyřešit
Možnosti jsou tři, seřazené od nejlepší po nejhorší.
Best practice: inject v boot(), ulož do protected property
boot() běží na každém requestu (initial i subsequent), DI v něm funguje, a protected properties jsou ideální úložiště — Livewire je neserializuje na klienta, takže nemusíš řešit hydrataci.
<?php
namespace App\Livewire;
use App\Services\StatsService;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Dashboard extends Component
{
public string $apiKey;
protected StatsService $stats;
public function boot(StatsService $stats): void
{
$this->stats = $stats;
}
#[Computed]
public function dailyRequests(): array
{
// $this->stats je dostupné, žádný app() helper není potřeba
return $this->stats->forApiKey($this->apiKey);
}
public function render()
{
return view('livewire.dashboard');
}
}
Proč ne mount()? Protože mount() se volá jen při prvním vytvoření komponenty. Při kliknutí na tlačítko (subsequent request) se komponenta zrekonstruuje, mount() se nezavolá a tvoje protected property bude neinicializovaná → must not be accessed before initialization. boot() tenhle problém nemá.
Druhá varianta: app() přímo v metodě
Jednodušší, ale méně testovatelné a porušuje to inversion of control. Hodí se na ad-hoc případy:
#[Computed]
public function dailyRequests(): array
{
return app(StatsService::class)->forApiKey($this->apiKey);
}
Nepoužívej: __construct v Livewire komponentě
Někdy se v starších tutoriálech objevuje řešení s konstruktorem. Nedělej to. Livewire komponenty se neinstanciují přes new, ale přes service container s reflection logikou, a vlastní konstruktor ti tu logiku rozbije.
Shrnutí
| Místo | DI funguje? | Důvod |
|---|---|---|
mount(), boot(), booted() |
✅ | volání přes wrap() |
hydrate(), dehydrate(), jejich *Foo varianty |
✅ | volání přes wrap() |
updating(), updated(), *Foo varianty |
✅ | volání přes wrap() |
rendering(), rendered(), render() |
✅ | volání přes wrap() |
exception() |
✅ | volání přes wrap() |
| Akce (public metody volané z front-endu) | ✅ | volání přes wrap() |
Event listenery (#[On], getListeners) |
✅ | volání přes wrap() |
with(), placeholder() |
✅ | volání přes wrap() |
#[Computed] metody |
❌ | invade() |
rules(), messages(), validationAttributes() |
❌ | přímé $this->...() |
getListeners(), queryString() |
❌ | invade() |
Pokud potřebuješ službu v computed property nebo ve validation rules, injektuj ji v boot() do protected property. Funguje to, je to čisté a nemusíš si pamatovat, kde Livewire interně používá wrap() a kde ne.