Skip to content

Commit

Permalink
Merge pull request #414 from facade/feature/livewire-support
Browse files Browse the repository at this point in the history
Add better livewire support
  • Loading branch information
rubenvanassche authored Nov 24, 2021
2 parents 29b533f + fd648b4 commit 7d1f1e6
Show file tree
Hide file tree
Showing 21 changed files with 828 additions and 33 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"livewire/livewire": "^2.4",
"mockery/mockery": "^1.3",
"orchestra/testbench": "^5.0|^6.0",
"psalm/plugin-laravel": "^1.2"
Expand Down
8 changes: 4 additions & 4 deletions resources/compiled/ignition.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions resources/js/Ignition.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class Ignition {
registerBuiltinTabs() {
Vue.component('AppTab', require('./components/Tabs/AppTab').default);
Vue.component('ContextTab', require('./components/Tabs/ContextTab').default);
Vue.component('LivewireTab', require('./components/Tabs/LivewireTab').default);
Vue.component('DebugTab', require('./components/Tabs/DebugTab').default);
Vue.component('RequestTab', require('./components/Tabs/RequestTab').default);
Vue.component('StackTab', require('./components/Tabs/StackTab').default);
Expand Down
10 changes: 9 additions & 1 deletion resources/js/components/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import ShareButton from './Shared/ShareButton';
export default {
inject: ['config'],
inject: ['config', 'report'],
components: { ShareButton },
props: {
value: { required: true },
Expand All @@ -40,6 +40,14 @@ export default {
component: 'RequestTab',
title: 'Request',
},
...(this.report.context.livewire
? [
{
component: 'LivewireTab',
title: 'Livewire',
},
]
: []),
{
component: 'AppTab',
title: 'App',
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/Tabs/ContextTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const predefinedContextItemGroups = [
'logs',
'dumps',
'exception',
'livewire',
];
export default {
Expand Down
85 changes: 85 additions & 0 deletions resources/js/components/Tabs/LivewireTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<div class="tab-content">
<div class="layout-col">
<DefinitionList title="Component" class="tab-content-section">
<DefinitionListRow
v-for="(value, key) in livewire"
v-if="key.startsWith('component_')"
:key="key"
:label="lookupKey(key)"
>{{ value }}</DefinitionListRow
>
</DefinitionList>
<DefinitionList title="Updates" class="tab-content-section">
<DefinitionListRow
v-for="(value, key) in livewire.updates"
:key="key"
:label="value['type']"
>
<DefinitionList>
<DefinitionListRow
v-for="(parameter, key) in value['payload'] || []"
:label="key"
:key="key"
>
<code class="code-inline">
<pre>{{ parameter }}</pre>
</code>
</DefinitionListRow>
</DefinitionList>
</DefinitionListRow>
</DefinitionList>
<DefinitionList title="Data" class="tab-content-section">
<DefinitionListRow v-for="(value, key) in livewire.data" :key="key" :label="key">
<template v-if="typeof value === 'string'">
{{ value }}
</template>
<code v-else class="code-inline">
<pre>{{ value }}</pre>
</code>
</DefinitionListRow>
</DefinitionList>
</div>
</div>
</template>
<script>
import DefinitionList from '../Shared/DefinitionList';
import DefinitionListRow from '../Shared/DefinitionListRow.js';
import upperFirst from 'lodash/upperFirst';
const predefinedKeys = {
component_alias: 'Alias',
component_id: 'Id',
component_class: 'Class',
};
export default {
inject: ['report'],
components: { DefinitionListRow, DefinitionList },
filters: {
upperFirst,
},
computed: {
livewire() {
return this.report.context.livewire;
},
data() {
return this.report.context.livewire.data;
},
updates() {
return this.report.context.livewire.updates;
},
},
methods: {
lookupKey(key) {
return predefinedKeys[key] || key;
},
},
};
</script>
14 changes: 13 additions & 1 deletion src/Context/LaravelContextDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Facade\FlareClient\Context\ContextDetectorInterface;
use Facade\FlareClient\Context\ContextInterface;
use Illuminate\Http\Request;
use Livewire\LivewireManager;

class LaravelContextDetector implements ContextDetectorInterface
{
Expand All @@ -14,6 +15,17 @@ public function detectCurrentContext(): ContextInterface
return new LaravelConsoleContext($_SERVER['argv'] ?? []);
}

return new LaravelRequestContext(app(Request::class));
$request = app(Request::class);

if ($this->isRunningLiveWire($request)) {
return new LivewireRequestContext($request, app(LivewireManager::class));
}

return new LaravelRequestContext($request);
}

protected function isRunningLiveWire(Request $request)
{
return $request->hasHeader('x-livewire') && $request->hasHeader('referer');
}
}
12 changes: 0 additions & 12 deletions src/Context/LaravelRequestContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,6 @@ public function getRoute(): array
];
}

public function getRequest(): array
{
$properties = parent::getRequest();


if ($this->request->hasHeader('x-livewire') && $this->request->hasHeader('referer')) {
$properties['url'] = $this->request->header('referer');
}

return $properties;
}

protected function getRouteParameters(): array
{
try {
Expand Down
94 changes: 94 additions & 0 deletions src/Context/LivewireRequestContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

namespace Facade\Ignition\Context;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Livewire\LivewireManager;

class LivewireRequestContext extends LaravelRequestContext
{
/** @var \Livewire\LivewireManager */
protected $livewireManager;

public function __construct(
Request $request,
LivewireManager $livewireManager
) {
parent::__construct($request);

$this->livewireManager = $livewireManager;
}

public function getRequest(): array
{
$properties = parent::getRequest();

$properties['method'] = $this->livewireManager->originalMethod();
$properties['url'] = $this->livewireManager->originalUrl();

return $properties;
}

public function toArray(): array
{
$properties = parent::toArray();

$properties['livewire'] = $this->getLiveWireInformation();

return $properties;
}

protected function getLiveWireInformation(): array
{
$componentId = $this->request->input('fingerprint.id');
$componentAlias = $this->request->input('fingerprint.name');

if ($componentAlias === null) {
return [];
}

try {
$componentClass = $this->livewireManager->getClass($componentAlias);
} catch (Exception $e) {
$componentClass = null;
}

return [
'component_class' => $componentClass,
'component_alias' => $componentAlias,
'component_id' => $componentId,
'data' => $this->resolveData(),
'updates' => $this->resolveUpdates(),
];
}

protected function resolveData(): array
{
$data = $this->request->input('serverMemo.data') ?? [];

$dataMeta = $this->request->input('serverMemo.dataMeta') ?? [];

foreach ($dataMeta['modelCollections'] ?? [] as $key => $value) {
$data[$key] = array_merge($data[$key] ?? [], $value);
}

foreach ($dataMeta['models'] ?? [] as $key => $value) {
$data[$key] = array_merge($data[$key] ?? [], $value);
}

return $data;
}

protected function resolveUpdates()
{
$updates = $this->request->input('updates') ?? [];

return array_map(function (array $update) {
$update['payload'] = Arr::except($update['payload'] ?? [], ['id']);

return $update;
}, $updates);
}
}
4 changes: 4 additions & 0 deletions src/IgnitionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
use Facade\Ignition\SolutionProviders\RunningLaravelDuskInProductionProvider;
use Facade\Ignition\SolutionProviders\SolutionProviderRepository;
use Facade\Ignition\SolutionProviders\TableNotFoundSolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedLivewireMethodSolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedLivewirePropertySolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedPropertySolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedVariableSolutionProvider;
use Facade\Ignition\SolutionProviders\UnknownValidationSolutionProvider;
Expand Down Expand Up @@ -445,6 +447,8 @@ protected function getDefaultSolutions(): array
RunningLaravelDuskInProductionProvider::class,
MissingColumnSolutionProvider::class,
UnknownValidationSolutionProvider::class,
UndefinedLivewireMethodSolutionProvider::class,
UndefinedLivewirePropertySolutionProvider::class,
UndefinedPropertySolutionProvider::class,
MissingMixManifestSolutionProvider::class,
MissingLivewireComponentSolutionProvider::class,
Expand Down
48 changes: 48 additions & 0 deletions src/SolutionProviders/UndefinedLivewireMethodSolutionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Facade\Ignition\SolutionProviders;

use Facade\Ignition\Solutions\SuggestLivewireMethodNameSolution;
use Facade\Ignition\Support\LivewireComponentParser;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Livewire\Exceptions\MethodNotFoundException;
use Throwable;

class UndefinedLivewireMethodSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return $throwable instanceof MethodNotFoundException;
}

public function getSolutions(Throwable $throwable): array
{
['methodName' => $methodName, 'component' => $component] = $this->getMethodAndComponent($throwable);

if ($methodName === null || $component === null) {
return [];
}

$parsed = LivewireComponentParser::create($component);

return $parsed->getMethodNamesLike($methodName)
->map(function (string $suggested) use ($parsed, $methodName) {
return new SuggestLivewireMethodNameSolution(
$methodName,
$parsed->getComponentClass(),
$suggested
);
})
->toArray();
}

protected function getMethodAndComponent(Throwable $throwable): array
{
preg_match_all('/\[([\d\w\-_]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER);

return [
'methodName' => $matches[0][1] ?? null,
'component' => $matches[1][1] ?? null,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Facade\Ignition\SolutionProviders;

use Facade\Ignition\Solutions\SuggestLivewirePropertyNameSolution;
use Facade\Ignition\Support\LivewireComponentParser;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Livewire\Exceptions\PropertyNotFoundException;
use Livewire\Exceptions\PublicPropertyNotFoundException;
use Throwable;

class UndefinedLivewirePropertySolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return $throwable instanceof PropertyNotFoundException || $throwable instanceof PublicPropertyNotFoundException;
}

public function getSolutions(Throwable $throwable): array
{
['variable' => $variable, 'component' => $component] = $this->getMethodAndComponent($throwable);

if ($variable === null || $component === null) {
return [];
}

$parsed = LivewireComponentParser::create($component);

return $parsed->getPropertyNamesLike($variable)
->map(function (string $suggested) use ($parsed, $variable) {
return new SuggestLivewirePropertyNameSolution(
$variable,
$parsed->getComponentClass(),
'$'.$suggested
);
})
->toArray();
}

protected function getMethodAndComponent(Throwable $throwable): array
{
preg_match_all('/\[([\d\w\-_\$]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER, 0);

return [
'variable' => $matches[0][1] ?? null,
'component' => $matches[1][1] ?? null,
];
}
}
Loading

0 comments on commit 7d1f1e6

Please sign in to comment.