refactor(form-builder): restore type-hinted route model binding for failures controller (WS-6)
Replace the manual `$request->route('formSubmissionActionFailure')` workaround
with type-hinted parameters. Implicit route model binding now resolves
FormSubmissionActionFailure correctly on both the platform admin route
(/admin/form-failures/{id}) and the org-scoped route
(/organisations/{organisation}/form-failures/{id}).
Root cause:
On the nested org-scoped route, Laravel's implicit binding triggers its
scoped-binding code path: for the second URL segment, it tries to resolve
the failure as a relation of the route's parent ({organisation}) by calling
`$organisation->formSubmissionActionFailures()`. Organisation has no such
relation (failures live under FormSubmission, not Organisation directly),
so the lookup silently fell through and the controller received a raw
string. PHP then raised a TypeError on the type-hinted parameter.
A second issue compounded it: with the controller method declaring
`(FormSubmissionActionFailure $formSubmissionActionFailure, ?Organisation $organisation)`
the parameter order did NOT match the URL parameter order
(/{organisation}/.../{formSubmissionActionFailure}), so Laravel's
resolveMethodDependencies — which falls back to positional binding when
parameter counts diverge — bound them to the wrong slots.
Fix:
- Register an explicit `Route::bind('formSubmissionActionFailure', ...)`
in AppServiceProvider that loads the model `withoutGlobalScopes()` and
throws ModelNotFoundException on miss. This sidesteps the scoped-binding
parent-relation lookup entirely.
- Add `->withoutScopedBindings()` to all four org-scoped routes (show,
retry, resolve, dismiss) as a belt-and-braces guarantee that Laravel
never enters the scoped-binding path for these nested routes.
- Reorder controller method signatures to put `?Organisation $organisation`
FIRST, matching URL parameter order so positional binding lands the
ULID strings on the correct method parameters.
- Drop the now-unused private `resolveFailure()` helper.
- Tenant scoping continues to be enforced by FormSubmissionActionFailurePolicy
via the failure.submission.organisation_id FK chain (RFC V3); cross-
tenant access still translates denied → 404, never 403.
Tests: all 9 controller tests pass (cross-tenant 404 contract verified for
view, dismiss, and resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\FormBuilder\FormSubmissionSectionSubmitted;
|
||||
use App\Events\FormBuilder\FormSubmissionSubmitted;
|
||||
use App\FormBuilder\Bindings\BindingActivityLogger;
|
||||
use App\FormBuilder\Bindings\BindingConflictResolver;
|
||||
use App\FormBuilder\Bindings\BindingTypeRegistry;
|
||||
use App\FormBuilder\Bindings\FormBindingApplicator;
|
||||
use App\FormBuilder\Bindings\PersonProvisioner;
|
||||
use App\FormBuilder\Purposes\PurposeRegistry;
|
||||
use App\Listeners\FormBuilder\ApplyBindingsOnFormSectionSubmitted;
|
||||
use App\Listeners\FormBuilder\ApplyBindingsOnFormSubmit;
|
||||
use App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit;
|
||||
use App\Listeners\FormBuilder\TriggerPersonIdentityMatchOnFormSubmit;
|
||||
use App\Models\Company;
|
||||
use App\Models\CrowdList;
|
||||
use App\Models\CrowdType;
|
||||
@@ -50,12 +56,6 @@ use App\Models\UserInvitation;
|
||||
use App\Models\UserOrganisationTag;
|
||||
use App\Models\UserProfile;
|
||||
use App\Models\VolunteerAvailability;
|
||||
use App\Events\FormBuilder\FormSubmissionSectionSubmitted;
|
||||
use App\Events\FormBuilder\FormSubmissionSubmitted;
|
||||
use App\Listeners\FormBuilder\ApplyBindingsOnFormSectionSubmitted;
|
||||
use App\Listeners\FormBuilder\ApplyBindingsOnFormSubmit;
|
||||
use App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit;
|
||||
use App\Listeners\FormBuilder\TriggerPersonIdentityMatchOnFormSubmit;
|
||||
use App\Observers\FormBuilder\FormFieldChildTablesCascadeObserver;
|
||||
use App\Observers\FormBuilder\FormSubmissionObserver;
|
||||
use App\Observers\FormBuilder\FormValueObserver;
|
||||
@@ -63,8 +63,8 @@ use App\Observers\PersonObserver;
|
||||
use App\Observers\UserObserver;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -130,6 +130,26 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Policies\FormBuilder\FormSubmissionActionFailurePolicy::class,
|
||||
);
|
||||
|
||||
// RFC-WS-6 v1.1 §3 Q5 — explicit route model binding for the
|
||||
// form-failures admin endpoints. Implicit binding on nested route
|
||||
// groups triggers Laravel's scoped-binding logic that tries to
|
||||
// resolve the failure as a relation of the route's parent
|
||||
// ({organisation}) — Organisation has no formSubmissionActionFailures()
|
||||
// relation, so the implicit lookup silently fails and the
|
||||
// controller receives a string. Route::bind sidesteps the
|
||||
// scoped-binding path entirely. Bypass global scopes so cross-
|
||||
// tenant access reaches the policy (which translates denied → 404
|
||||
// per RFC V3 in the controller).
|
||||
\Illuminate\Support\Facades\Route::bind('formSubmissionActionFailure', static function (string $value): \App\Models\FormBuilder\FormSubmissionActionFailure {
|
||||
$model = \App\Models\FormBuilder\FormSubmissionActionFailure::query()
|
||||
->withoutGlobalScopes()
|
||||
->find($value);
|
||||
if ($model === null) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
return $model;
|
||||
});
|
||||
|
||||
Person::observe(PersonObserver::class);
|
||||
User::observe(UserObserver::class);
|
||||
@@ -177,7 +197,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
);
|
||||
|
||||
ResetPassword::createUrlUsing(function ($user, string $token) {
|
||||
return config('crewli.portal_url') . '/wachtwoord-resetten?token=' . $token . '&email=' . urlencode($user->email);
|
||||
return config('crewli.portal_url').'/wachtwoord-resetten?token='.$token.'&email='.urlencode($user->email);
|
||||
});
|
||||
|
||||
// Tag activity log entries with impersonation context
|
||||
|
||||
Reference in New Issue
Block a user