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']]; });
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.