How PHP Attributes Changed the Way I Write Livewire
I've been writing Livewire components for years now. I love the framework. It does for full-stack PHP what nothing else really has: it lets me build genuinely interactive UIs without leaving the language I actually enjoy writing.
But I'll be honest, for a long time, Livewire components had a smell. Not a deal-breaker, just an itch. You'd open a class and find a wall of metadata at the top: a $rules property here, a $listeners array there, a $queryString somewhere, plus a mount() method doing three different things. The logic of the component was buried under its own configuration.
Then PHP attributes happened. And then Livewire leaned into them. And honestly? I can't go back.
The Before Times
Here's what a typical Livewire component looked like in my codebase:
class CreatePost extends Component
{
public $title = '';
public $content = '';
protected $rules = [
'title' => 'required|min:3|max:255',
'content' => 'required|min:10',
];
protected $listeners = ['post-saved' => 'refresh'];
protected $queryString = ['search'];
public $search = '';
public function mount()
{
$this->search = request('search', '');
}
public function save() { /* ... */ }
public function refresh() { /* ... */ }
}
There are five separate concerns floating around the top of this class:
$titleand$contentare form fields$rulesare validation rules belonging to those fields$listenersconnectsrefresh()to an event$queryStringsyncs$searchwith the URLmount()initialises$searchfrom the request
And they're all completely disconnected from each other in the code. The validation rules for $title live ten lines below $title itself. The listener for refresh() is declared as a string in an array, far away from the actual method. Nothing tells your IDE that any of these things relate.
I never thought much about it. This was just how Livewire worked, and I'd happily mentally jumped around the file when I needed to.
The After Times
Same component, written the way I write it now:
class CreatePost extends Component
{
#[Validate('required|min:3|max:255')]
public string $title = '';
#[Validate('required|min:10')]
public string $content = '';
#[Url]
public string $search = '';
public function save() { /* ... */ }
#[On('post-saved')]
public function refresh() { /* ... */ }
}
That's it. Same functionality. Half the code.
But more importantly, and this is the part that genuinely changed how I think about components, every piece of metadata is now attached to the thing it's actually about. The validation rules sit right above the property they validate. The event listener sits right above the method it triggers. The URL sync sits right above the property it syncs.
There's no more mental map to keep in your head. You read the code top to bottom and the meaning is right there.
It's Not Just About Cleanliness
I'd be lying if I said this is just about aesthetics. It's also about how my brain works when I'm reading code I haven't touched for months.
When I open an attribute-heavy component now, I can answer "what does this property do?" in one glance. Before, I had to scroll up to check $rules, scroll down to check mount(), and search for the property name in $queryString. Three separate cognitive jumps to understand one property.
With attributes, it's all there. #[Validate], #[Url], #[Locked], #[Session], each one tells me exactly what's special about that property the moment I look at it. The class becomes self-documenting in a way it just wasn't before.
My Favourite Discovery: Computed Properties
If I had to pick the one attribute I use most, it's #[Computed]. And it's the one I wish I'd been using from day one.
class UserProfile extends Component
{
public int $userId;
#[Computed]
public function user(): User
{
return User::with('profile')->findOrFail($this->userId);
}
}
<h1>{{ $this->user->name }}</h1>
<p>{{ $this->user->profile->bio }}</p>
Two database hits would be the obvious worry here. But #[Computed] caches the result for the duration of the request, so accessing $this->user ten times in your view still only hits the database once.
This used to be the kind of thing I'd build manually with private properties and lazy initialisation. Now it's three letters above the method. I genuinely smile every time I write it.
The IDE Win Nobody Talks About
One last thing that doesn't get enough credit: PHPStorm understands attributes.
I can cmd+click on #[Validate] and jump to the attribute class. I can hover over #[Locked] and read the docblock. My static analyser catches mistakes in attribute arguments. Refactoring rename works across them.
The old $listeners = ['post-saved' => 'refresh'] was just a string in an array. The IDE had no idea those characters meant anything special. With #[On('post-saved')] directly above refresh(), the relationship is structural, not coincidental.
That's the thing about attributes that I think people underestimate. They're not just nicer to read, they're metadata the language itself understands. Tools can reason about them. Frameworks can introspect them properly via reflection. You're no longer writing configuration that pretends to be code; you're writing actual code that happens to be configuration.
Wrapping Up
I'm not someone who thinks every new language feature is automatically a win. PHP has had a few of those over the years, looking at you, named arguments combined with positional arguments, that's a mess waiting to happen. But attributes? Attributes are different. They solved a real problem I'd just learned to live with.
Six months from now I'll probably look at my current code and find something else to improve. That's the job. But the attribute-based Livewire I write today is genuinely the cleanest interactive PHP I've ever shipped, and I have a hard time imagining what would have to change for me to go back.
If you're still writing $rules arrays and protected $listeners, do yourself a favour: pick one component, refactor it to attributes, and just see how it feels. I think you'll have the same reaction I did.
#livewire, #attributes, #clean-code