Skip to content

Commit 229287c

Browse files
SebastiaanKloosstayallivecleptric
authored
Add support for Livewire 3 (#798)
Co-authored-by: Alex Bouma <[email protected]> Co-authored-by: Michi Hoffmann <[email protected]>
1 parent f132ad5 commit 229287c

File tree

4 files changed

+126
-55
lines changed

4 files changed

+126
-55
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,9 @@ jobs:
134134
- name: Install Composer dependencies
135135
run: |
136136
# friendsofphp/php-cs-fixer: No need for this package to run phpunit and it conflicts with older Laravel versions
137+
# livewire/livewire: Only supported on Laravel 7.0 and above
137138
# laravel/folio: Only supported on PHP 8.1 + Laravel 10.0 and above
138-
composer remove friendsofphp/php-cs-fixer laravel/folio --dev --no-interaction --no-update
139+
composer remove friendsofphp/php-cs-fixer livewire/livewire laravel/folio --dev --no-interaction --no-update
139140
140141
# Require the correct versions we want to run phpunit for
141142
composer require \

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"require-dev": {
3838
"phpunit/phpunit": "^8.4 | ^9.3 | ^10.4",
3939
"laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
40+
"livewire/livewire": "^2.0 | ^3.0",
4041
"orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0",
4142
"friendsofphp/php-cs-fixer": "^3.11",
4243
"mockery/mockery": "^1.3",

phpstan-baseline.neon

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,6 @@ parameters:
9090
count: 1
9191
path: src/Sentry/Laravel/EventHandler.php
9292

93-
-
94-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBoot\\(\\) has invalid type Livewire\\\\Component\\.$#"
95-
count: 1
96-
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php
97-
98-
-
99-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Component\\.$#"
100-
count: 1
101-
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php
102-
103-
-
104-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentDehydrate\\(\\) has invalid type Livewire\\\\Component\\.$#"
105-
count: 1
106-
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php
107-
108-
-
109-
message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentMount\\(\\) has invalid type Livewire\\\\Component\\.$#"
110-
count: 1
111-
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php
112-
113-
-
114-
message: "#^Parameter \\$livewireManager of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:onBoot\\(\\) has invalid type Livewire\\\\LivewireManager\\.$#"
115-
count: 1
116-
path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php
117-
11893
-
11994
message: "#^Parameter \\$request of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Request\\.$#"
12095
count: 1

src/Sentry/Laravel/Features/LivewirePackageIntegration.php

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,21 @@
33
namespace Sentry\Laravel\Features;
44

55
use Livewire\Component;
6+
use Livewire\EventBus;
67
use Livewire\LivewireManager;
78
use Livewire\Request;
89
use Sentry\Breadcrumb;
10+
use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans;
911
use Sentry\Laravel\Integration;
1012
use Sentry\SentrySdk;
11-
use Sentry\Tracing\Span;
1213
use Sentry\Tracing\SpanContext;
1314
use Sentry\Tracing\TransactionSource;
1415

1516
class LivewirePackageIntegration extends Feature
1617
{
17-
private const FEATURE_KEY = 'livewire';
18-
19-
private const COMPONENT_SPAN_OP = 'ui.livewire.component';
18+
use TracksPushedScopesAndSpans;
2019

21-
/** @var array<Span> */
22-
private $spanStack = [];
20+
private const FEATURE_KEY = 'livewire';
2321

2422
public function isApplicable(): bool
2523
{
@@ -32,11 +30,56 @@ public function isApplicable(): bool
3230
}
3331

3432
public function onBoot(LivewireManager $livewireManager): void
33+
{
34+
if (class_exists(EventBus::class)) {
35+
$this->registerLivewireThreeEventListeners($livewireManager);
36+
37+
return;
38+
}
39+
40+
$this->registerLivewireTwoEventListeners($livewireManager);
41+
}
42+
43+
private function registerLivewireThreeEventListeners(LivewireManager $livewireManager): void
44+
{
45+
$livewireManager->listen('mount', function (Component $component, array $data) {
46+
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
47+
$this->handleComponentBoot($component);
48+
}
49+
50+
if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
51+
$this->handleComponentMount($component, $data);
52+
}
53+
});
54+
55+
$livewireManager->listen('hydrate', function (Component $component) {
56+
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
57+
$this->handleComponentBoot($component);
58+
}
59+
60+
if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
61+
$this->handleComponentHydrate($component);
62+
}
63+
});
64+
65+
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
66+
$livewireManager->listen('dehydrate', [$this, 'handleComponentDehydrate']);
67+
}
68+
69+
if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) {
70+
$livewireManager->listen('call', [$this, 'handleComponentCall']);
71+
}
72+
}
73+
74+
private function registerLivewireTwoEventListeners(LivewireManager $livewireManager): void
3575
{
3676
$livewireManager->listen('component.booted', [$this, 'handleComponentBooted']);
3777

3878
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
39-
$livewireManager->listen('component.boot', [$this, 'handleComponentBoot']);
79+
$livewireManager->listen('component.boot', function ($component) {
80+
$this->handleComponentBoot($component);
81+
});
82+
4083
$livewireManager->listen('component.dehydrate', [$this, 'handleComponentDehydrate']);
4184
}
4285

@@ -45,23 +88,38 @@ public function onBoot(LivewireManager $livewireManager): void
4588
}
4689
}
4790

48-
public function handleComponentBoot(Component $component): void
91+
public function handleComponentCall(Component $component, string $method, array $arguments): void
92+
{
93+
Integration::addBreadcrumb(new Breadcrumb(
94+
Breadcrumb::LEVEL_INFO,
95+
Breadcrumb::TYPE_DEFAULT,
96+
'livewire',
97+
"Component call: {$component->getName()}::{$method}",
98+
$this->mapCallArgumentsToMethodParameters($component, $method, $arguments) ?? ['arguments' => $arguments]
99+
));
100+
}
101+
102+
public function handleComponentBoot(Component $component, ?string $method = null): void
49103
{
50-
$currentSpan = SentrySdk::getCurrentHub()->getSpan();
104+
if ($this->isLivewireRequest()) {
105+
$this->updateTransactionName($component->getName());
106+
}
51107

52-
if ($currentSpan === null) {
108+
$parentSpan = SentrySdk::getCurrentHub()->getSpan();
109+
110+
if ($parentSpan === null) {
53111
return;
54112
}
55113

56-
$this->spanStack[] = $currentSpan;
57-
58114
$context = new SpanContext;
59-
$context->setOp(self::COMPONENT_SPAN_OP);
60-
$context->setDescription($component->getName());
61-
62-
$componentSpan = $currentSpan->startChild($context);
63-
64-
SentrySdk::getCurrentHub()->setSpan($componentSpan);
115+
$context->setOp('ui.livewire.component');
116+
$context->setDescription(
117+
empty($method)
118+
? $component->getName()
119+
: "{$component->getName()}::{$method}"
120+
);
121+
122+
$this->pushSpan($parentSpan->startChild($context));
65123
}
66124

67125
public function handleComponentMount(Component $component, array $data): void
@@ -92,23 +150,28 @@ public function handleComponentBooted(Component $component, Request $request): v
92150
}
93151

94152
if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) {
95-
$this->updateTransactionName($component::getName());
153+
$this->updateTransactionName($component->getName());
96154
}
97155
}
98156

157+
public function handleComponentHydrate(Component $component): void
158+
{
159+
Integration::addBreadcrumb(new Breadcrumb(
160+
Breadcrumb::LEVEL_INFO,
161+
Breadcrumb::TYPE_DEFAULT,
162+
'livewire',
163+
"Component hydrate: {$component->getName()}",
164+
$component->all()
165+
));
166+
}
167+
99168
public function handleComponentDehydrate(Component $component): void
100169
{
101-
$currentSpan = SentrySdk::getCurrentHub()->getSpan();
170+
$span = $this->maybePopSpan();
102171

103-
if ($currentSpan === null || empty($this->spanStack)) {
104-
return;
172+
if ($span !== null) {
173+
$span->finish();
105174
}
106-
107-
$currentSpan->finish();
108-
109-
$previousSpan = array_pop($this->spanStack);
110-
111-
SentrySdk::getCurrentHub()->setSpan($previousSpan);
112175
}
113176

114177
private function updateTransactionName(string $componentName): void
@@ -137,10 +200,41 @@ private function isLivewireRequest(): bool
137200
return false;
138201
}
139202

140-
return $request->header('x-livewire') === 'true';
203+
return $request->hasHeader('x-livewire');
141204
} catch (\Throwable $e) {
142205
// If the request cannot be resolved, it's probably not a Livewire request.
143206
return false;
144207
}
145208
}
209+
210+
private function mapCallArgumentsToMethodParameters(Component $component, string $method, array $data): ?array
211+
{
212+
// If the data is empty there is nothing to do and we can return early
213+
// We also do a quick sanity check the method exists to prevent doing more expensive reflection to come to the same conclusion
214+
if (empty($data) || !method_exists($component, $method)) {
215+
return null;
216+
}
217+
218+
try {
219+
$reflection = new \ReflectionMethod($component, $method);
220+
$parameters = [];
221+
222+
foreach ($reflection->getParameters() as $parameter) {
223+
$defaultValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : '<missing>';
224+
225+
$parameters["\${$parameter->getName()}"] = $data[$parameter->getPosition()] ?? $defaultValue;
226+
227+
unset($data[$parameter->getPosition()]);
228+
}
229+
230+
if (!empty($data)) {
231+
$parameters['additionalArguments'] = $data;
232+
}
233+
234+
return $parameters;
235+
} catch (\ReflectionException $e) {
236+
// If reflection fails, fail the mapping instead of crashing
237+
return null;
238+
}
239+
}
146240
}

0 commit comments

Comments
 (0)