英文:
Difficult issue to troubleshoot with Gate and Livewire
问题
I have been trying to solve this issue for days, but I struggle.
我已经尝试解决这个问题好几天了,但我感到困难。
I tried to pack this post with as many information as I could because it is not easy to troubleshoot.
我尽量提供尽可能多的信息,因为故障排除并不容易。
Explanation
解释
If I drag any rows with the admin role, it works perfectly.
如果我使用管理员角色拖动任何行,它完美运行。
If I drag a role with the manager role, the first drag works, on the second drag I get this:
如果我使用经理角色拖动一个角色,第一次拖动正常工作,但在第二次拖动时出现以下错误:
Unknown named parameter $id <---THIS IS THE ERROR
未知的命名参数 $id <---这是错误
Routes:
路由:
require __DIR__ . '/auth.php';
Route::prefix('/')
->middleware(['auth','verified'])
->group(function () {
Route::resource('roles', RoleController::class);
Route::resource('permissions', PermissionController::class);
Route::resource('users', UserController::class);
Route::resource('popups', PopupController::class);
});
Illuminate\Auth\Access\Gate: 535 callAuthCallback
Illuminate\Auth\Access\Gate: 535 callAuthCallback
/**
-
Resolve and call the appropriate authorization callback.
-
解决并调用适当的授权回调。
-
@param \Illuminate\Contracts\Auth\Authenticatable|null $user
-
@param string $ability
-
@param array $arguments
-
@return bool
/
/* -
@param \Illuminate\Contracts\Auth\Authenticatable|null $user
-
@param string $ability
-
@param array $arguments
-
@return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);return $callback($user, ...$arguments); //This line shows in RED
}
I using this library to drag and drop a table row:
我使用这个库来拖放表格行:
GitHub - nextapps-be/livewire-sortablejs
This is my component:
这是我的组件:
<table class="w-full max-w-full mb-4 bg-transparent">
<thead class="text-gray-700">
<tr>
<th class="px-4 py-3 text-left"></th>
<th class="px-4 py-3 text-left"></th>
<th class="px-4 py-3 text-left"></th>
<th></th>
</tr>
</thead>
<tbody wire:sortable="reorder" wire:sortable.options="{ animation: 100 }" class="text-gray-600">
@forelse($popups as $popup)
<tr wire:sortable.item="{{ $popup['id'] }}" wire:sortable.triggers="reorder" class="hover:bg-gray-50 {{ $popup['order'] === 1 ? 'bg-yellow-50' : '' }}" wire:key="popup-{{ $popup['id'] }}">
<td class="px-4 py-3 text-left">
{{ $popup['id'] ?? '-' }}
</td>
<td class="px-4 py-3 text-left">
{{ $popup['title'] ?? '-' }}
</td>
<td class="px-4 py-3 text-left">
{{ $popup['reset_popup_days'] ?? '-' }}
</td>
<td>
<button wire:sortable.handle>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/>
</svg>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="7">
@lang('crud.common.no_items_found')
</td>
</tr>
@endforelse
</tbody>
</table>
The livewire class:
Livewire 类:
<?php
namespace App\Http\Livewire\Table\Popup;
use App\Models\Popup;
use Livewire\Component;
class Index extends Component
{
public $popups;
public function mount()
{
$this->popups = Popup::orderBy('order')->get();
}
public function reorder($reorderedIds)
{
$orderedIds = collect($reorderedIds)->sortBy('order')->pluck('value');
$this->popups = $orderedIds->map(function ($id) {
return collect($this->popups)->where('id', (int)$id)->first();
})->values();
foreach ($this->popups as $index => $popup) {
$popup = Popup::find($popup['id']);
$popup->order = $index + 1;
$popup->save();
}
}
public function render()
{
return view('livewire.table.popup.index', ['popups' => $this->popups]);
}
}
I am using Gates and Policies for the Laravel controllers (not using it on livewire components directly yet as I am not sure if I should).
This is my controller so when the user lands on the CRUD, he can access all the different pages:
这是我的控制器,当用户访问 CRUD 时,他可以访问所有不同的页面:
<?php
namespace App\Http\Controllers;
use App\Models\Popup;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Http\Requests\PopupStoreRequest;
use App\Http\Requests\PopupUpdateRequest;
class PopupController extends Controller
{
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$this->authorize('view-any', Popup::class);
$search = $request->get('search', '');
$popups = Popup::search($search)
->latest()
->paginate(5)
->withQueryString();
return view('app.popups.index', compact('popups', 'search'));
}
// Other controller methods...
// 其他控制器方法...
}
My policies:
我的策略:
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Popup;
use Illuminate\Auth\Access\HandlesAuthorization;
class PopupPolicy
{
<details>
<summary>英文:</summary>
I have been trying to solve this issue for days, but I struggle.
I tried to pack this post with as many information as I could because it is not easy to troubleshoot.
"laravel/framework": "9.41.0",
"spatie/laravel-permission": "5.7"
**Explanation**
If I drag any rows with the admin role, it works perfectly.
If I drag a role with the manager role, the first drag works, on the second drag I get this:
**Unknown named parameter $id** <---THIS IS THE ERROR
Routes:
require DIR . '/auth.php';
Route::prefix('/')
->middleware(['auth','verified'])
->group(function () {
Route::resource('roles', RoleController::class);
Route::resource('permissions', PermissionController::class);
Route::resource('users', UserController::class);
Route::resource('popups', PopupController::class);
});
Illuminate\ Auth\ Access \ Gate : 535 callAuthCallback
/**
* Resolve and call the appropriate authorization callback.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);
return $callback($user, ...$arguments); //This line shows in RED
}
I using this library to drag and drop a table row:
https://github.com/nextapps-be/livewire-sortablejs
**This is my component:**
<table class="w-full max-w-full mb-4 bg-transparent">
<thead class="text-gray-700">
<tr>
<th class="px-4 py-3 text-left"></th>
<th class="px-4 py-3 text-left"></th>
<th class="px-4 py-3 text-left"></th>
<th></th>
</tr>
</thead>
<tbody wire:sortable="reorder" wire:sortable.options="{ animation: 100 }" class="text-gray-600">
@forelse($popups as $popup)
<tr wire:sortable.item="{{ $popup['id'] }}" wire:sortable.triggers="reorder" class="hover:bg-gray-50 {{ $popup['order'] === 1 ? 'bg-yellow-50' : '' }}" wire:key="popup-{{ $popup['id'] }}">
<td class="px-4 py-3 text-left">
{{ $popup['id'] ?? '-' }}
</td>
<td class="px-4 py-3 text-left">
{{ $popup['title'] ?? '-' }}
</td>
<td class="px-4 py-3 text-left">
{{ $popup['reset_popup_days'] ?? '-' }}
</td>
<td>
<button wire:sortable.handle>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/>
</svg>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="7">
@lang('crud.common.no_items_found')
</td>
</tr>
@endforelse
</tbody>
</table>
**The livewire class:**
<?php
namespace App\Http\Livewire\Table\Popup;
use App\Models\Popup;
use Livewire\Component;
class Index extends Component
{
public $popups;
public function mount()
{
$this->popups = Popup::orderBy('order')->get();
}
public function reorder($reorderedIds)
{
$orderedIds = collect($reorderedIds)->sortBy('order')->pluck('value');
$this->popups = $orderedIds->map(function ($id) {
return collect($this->popups)->where('id', (int)$id)->first();
})->values();
foreach ($this->popups as $index => $popup) {
$popup = Popup::find($popup['id']);
$popup->order = $index + 1;
$popup->save();
}
}
public function render()
{
return view('livewire.table.popup.index', ['popups' => $this->popups]);
}
}
I am using Gates and Policies for the Laravel controllers(not using it on livewire components directly yet as I am not sure if I should).
This is my controller so when the user lands on the CRUD, he can access all the different pages.
<?php
namespace App\Http\Controllers;
use App\Models\Popup;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Http\Requests\PopupStoreRequest;
use App\Http\Requests\PopupUpdateRequest;
class PopupController extends Controller
{
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$this->authorize('view-any', Popup::class);
$search = $request->get('search', '');
$popups = Popup::search($search)
->latest()
->paginate(5)
->withQueryString();
return view('app.popups.index', compact('popups', 'search'));
}
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
$this->authorize('create', Popup::class);
return view('app.popups.create');
}
/**
* @param \App\Http\Requests\PopupStoreRequest $request
* @return \Illuminate\Http\Response
*/
public function store(PopupStoreRequest $request)
{
$this->authorize('create', Popup::class);
$validated = $request->validated();
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('public');
}
//$latestOrder = Popup::orderBy('order')->get()->last();
$latestOrder = Popup::orderBy('order', 'desc')->first();
$order = $latestOrder ? $latestOrder->order + 1 : 0;
$popup = Popup::create(array_merge($validated, ['order' => $order]));
//$popup = Popup::create($validated);
return redirect()
->route('popups.edit', $popup)
->withSuccess(__('crud.common.created'));
}
/**
* @param \Illuminate\Http\Request $request
* @param \App\Models\Popup $popup
* @return \Illuminate\Http\Response
*/
public function show(Request $request, Popup $popup)
{
$this->authorize('view', $popup);
return view('app.popups.show', compact('popup'));
}
/**
* @param \Illuminate\Http\Request $request
* @param \App\Models\Popup $popup
* @return \Illuminate\Http\Response
*/
public function edit(Request $request, Popup $popup)
{
$this->authorize('update', $popup);
return view('app.popups.edit', compact('popup'));
}
/**
* @param \App\Http\Requests\PopupUpdateRequest $request
* @param \App\Models\Popup $popup
* @return \Illuminate\Http\Response
*/
public function update(PopupUpdateRequest $request, Popup $popup)
{
$this->authorize('update', $popup);
$validated = $request->validated();
if ($request->hasFile('image')) {
if ($popup->image) {
Storage::delete($popup->image);
}
$validated['image'] = $request->file('image')->store('public');
}
$popup->update($validated);
return redirect()
->route('popups.edit', $popup)
->withSuccess(__('crud.common.saved'));
}
/**
* @param \Illuminate\Http\Request $request
* @param \App\Models\Popup $popup
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, Popup $popup)
{
$this->authorize('delete', $popup);
if ($popup->image) {
Storage::delete($popup->image);
}
$popup->delete();
return redirect()
->route('popups.index')
->withSuccess(__('crud.common.removed'));
}
}
**My policies:**
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Popup;
use Illuminate\Auth\Access\HandlesAuthorization;
class PopupPolicy
{
use HandlesAuthorization;
/**
* Determine whether the popup can view any models.
*
* @param App\Models\User $user
* @return mixed
*/
public function viewAny(User $user)
{
return $user->hasPermissionTo('list popups');
}
/**
* Determine whether the popup can view the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function view(User $user, Popup $model)
{
return $user->hasPermissionTo('view popups');
}
/**
* Determine whether the popup can create models.
*
* @param App\Models\User $user
* @return mixed
*/
public function create(User $user)
{
return $user->hasPermissionTo('create popups');
}
/**
* Determine whether the popup can update the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function update(User $user, Popup $model)
{
return $user->hasPermissionTo('update popups');
}
/**
* Determine whether the popup can delete the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function delete(User $user, Popup $model)
{
return $user->hasPermissionTo('delete popups');
}
/**
* Determine whether the user can delete multiple instances of the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function deleteAny(User $user)
{
return $user->hasPermissionTo('delete popups');
}
/**
* Determine whether the popup can restore the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function restore(User $user, Popup $model)
{
return false;
}
/**
* Determine whether the popup can permanently delete the model.
*
* @param App\Models\User $user
* @param App\Models\Popup $model
* @return mixed
*/
public function forceDelete(User $user, Popup $model)
{
return false;
}
}
**And the manager is assigned:**
Permission::create(['name' => 'list popups']);
Permission::create(['name' => 'view popups']);
Permission::create(['name' => 'create popups']);
Permission::create(['name' => 'update popups']);
Permission::create(['name' => 'delete popups']);
While the admin is assigned all the permissions like this:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
// Automatically finding the Policies
Gate::guessPolicyNamesUsing(function ($modelClass) {
return 'App\\Policies\\' . class_basename($modelClass) . 'Policy';
});
$this->registerPolicies();
// Implicitly grant "Super Admin" role all permission checks using can()
Gate::before(function ($user, $ability) {
logger($ability);
if ($user->isSuperAdmin()) {
return true;
}
});
}
}
[TRIED]
<?php
namespace App\Http\Livewire\Table\Popup;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use App\Models\Popup;
use Livewire\Component;
class Index extends Component
{
use AuthorizesRequests;
public $popups;
public function mount()
{
//$this->popups = Popup::orderBy('order')->get()->toArray();
$this->popups = Popup::orderBy('order')->get();
}
public function reorder($reorderedIds)
{
$this->authorize('update', array($this->popups));
$orderedIds = collect($reorderedIds)->sortBy('order')->pluck('value');
$this->popups = $orderedIds->map(function ($id) {
return collect($this->popups)->where('id', (int)$id)->first();
})->values();
//logger($this->popups);
foreach ($this->popups as $index => $popup) {
$popup = Popup::find($popup['id']);
$popup->order = $index + 1;
$popup->save();
}
}
public function render()
{
return view('livewire.table.popup.index', ['popups' => $this->popups]);
}
}
But getting this:
403 THIS ACTION IS UNAUTHORIZED.
//Event with the action is authorised(I checked the database and it is 100% authorised.
I think array($this->popups) is causing the issue as the data is different from the controler update authorised method....But how can I solve this if both methods deal differently with the data. In the old controller the update was dealing with a $POST array, while $this->popups deals with a collection.
[SOLUTION]
The solution to the $id issue was a change in code in the livewire class:
$popupIds = collect($reorderedIds)->pluck('value'); $popups = Popup::whereIn('id', $popupIds) ->orderBy('order') ->get() ->mapWithKeys(function ($popup) { return [$popup->id => $popup]; }); $this->popups = collect($reorderedIds)->map(function ($item) use ($popups) { return $popups[$item['value']]; });
</details>
# 答案1
**得分**: 1
我认为这个问题与[此PHP 8修改][1]相关,该修改将字符串键解释为函数参数名称。因此,当使用参数解包(`...`)运算符调用自定义策略函数时:
```php
return $callback($user, ...$arguments);
来自例如$this->authorize('update', $popup);
的$popup
变量(它的行为类似于关联数组)在解包后成为策略函数的命名参数。然而,由于策略函数没有$id
参数,它们会引发错误。
您可以尝试在PHP 7中运行代码以确认问题的来源。
要解决这个问题,您可以尝试将authorize()
的额外参数嵌入一个数组中,这样第一个数组解包将只解构外部数组,因此$popup
数组保持不变:
$this->authorize('update', array($popup));
或者,您还可以删除authorize()
的额外参数,因为您在策略函数中没有使用它们。
英文:
I think this issue is connected to this PHP 8 modification that makes string keys interpreted as function parameter names. So when the custom policy function is called with the argument unpacking (...
) operator here:
return $callback($user, ...$arguments);
the $popup
variable (which is behaving like an associative array) from e.g. $this->authorize('update', $popup);
becomes the named arguments of the policy functions after the unpacking. However since the policy functions do not have an $id
argument, they throw the error.
You can try to run the code with PHP 7 to confirm the source of the issue.
To fix this you can try to embed the extra arguments of authorize()
in an array, so the first array unpacking will only deconstruct the outer array, so the $popup
array remains intact:
$this->authorize('update', array($popup));
Or you can also remove the extra arguments of authorize()
since you don't use them in the policy functions.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论