*/ private const PURPOSE_SUBJECT_FQCN = [ 'person' => Person::class, 'user' => User::class, 'company' => Company::class, 'artist' => 'App\\Models\\Artist', ]; public function register(): void { $this->mergeConfigFrom( base_path('config/form_builder/purposes.php'), 'form_builder.purposes', ); $this->mergeConfigFrom( base_path('config/form_builder/binding_targets.php'), 'form_builder.binding_targets', ); $this->app->singleton(PurposeRegistry::class); $this->app->singleton(BindingTypeRegistry::class); // Telescope is a dev-only debugging dashboard. Three-layer // defense keeps it out of production: composer `dont-discover` // suppresses auto-registration, this block gates manual // registration to local/testing, and TELESCOPE_ENABLED in .env // is the runtime toggle (see /dev-docs/TELESCOPE.md). The vendor // service provider registers routes/migrations/publishing; // App\Providers\TelescopeServiceProvider attaches the gate + // filter. Both must register for the dashboard to work. if ($this->app->environment('local', 'testing')) { $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); $this->app->register(\App\Providers\TelescopeServiceProvider::class); } } public function boot(): void { $this->registerMorphMap(); Person::observe(PersonObserver::class); User::observe(UserObserver::class); FormValue::observe(FormValueObserver::class); \App\Models\FormBuilder\FormSubmission::observe(FormSubmissionObserver::class); // Cascade binding / validation-rule / config rows on owner delete. // Children are physical state; deleted on soft-delete as well as // hard-delete of the owner (WS-5a bindings, WS-5b validation rules // + configs). FormField::observe(FormFieldChildTablesCascadeObserver::class); FormFieldLibrary::observe(FormFieldChildTablesCascadeObserver::class); // ARCH §31.10 — FORM-02 TAG_PICKER sync listener. \Illuminate\Support\Facades\Event::listen( FormSubmissionSubmitted::class, SyncTagPickerSelectionsOnSubmit::class, ); // ARCH §31.1 — identity-match trigger on event_registration. \Illuminate\Support\Facades\Event::listen( FormSubmissionSubmitted::class, TriggerPersonIdentityMatchOnFormSubmit::class, ); ResetPassword::createUrlUsing(function ($user, string $token) { return config('crewli.portal_url') . '/wachtwoord-resetten?token=' . $token . '&email=' . urlencode($user->email); }); // Tag activity log entries with impersonation context Activity::saving(function (Activity $activity) { $request = request(); $impersonator = $request->attributes->get('impersonator'); $session = $request->attributes->get('impersonation_session'); if ($impersonator && $session) { $properties = $activity->properties?->toArray() ?? []; $properties['impersonated_by'] = [ 'user_id' => $impersonator->id, 'name' => $impersonator->full_name, 'email' => $impersonator->email, ]; $properties['impersonation_session_id'] = $session->id; $activity->properties = collect($properties); } }); } /** * Morph-map is built from three labelled blocks: * * 1. Domain subject types — derived from PurposeRegistry. These are * the aliases allowed on `form_submissions.subject_type`. * 2. Non-purpose domain types — form_schemas owner_types that are * not subjects (`organisation`, `event`, `user_profile`) plus * domain models referenced as activity-log subjects/causers. * 3. Framework types — spatie/activitylog subjects. * * Sanctum's `personal_access_tokens.tokenable_type` is intentionally * absent (addendum Q4: framework polymorphie uses framework defaults, * not the domain morph-map). `MorphMapAlignmentTest` guards blocks * 1 and 2 against drift. */ private function registerMorphMap(): void { /** @var PurposeRegistry $registry */ $registry = $this->app->make(PurposeRegistry::class); // Block 1 — domain subject types, derived from PurposeRegistry. $domainSubjectTypes = []; foreach ($registry->allSubjectTypes() as $alias) { $fqcn = self::PURPOSE_SUBJECT_FQCN[$alias] ?? null; if ($fqcn === null) { throw new \RuntimeException( "No FQCN mapping for purpose subject_type '{$alias}'. " .'Add it to AppServiceProvider::PURPOSE_SUBJECT_FQCN.' ); } $domainSubjectTypes[$alias] = $fqcn; } // Block 2 — non-purpose domain types. $nonPurposeDomainTypes = [ // form_schemas owner_type candidates that are not subject_types. 'organisation' => Organisation::class, 'event' => Event::class, 'user_profile' => UserProfile::class, // Domain models referenced as activity-log subjects/causers. 'crowd_list' => CrowdList::class, 'crowd_type' => CrowdType::class, 'email_change_request' => EmailChangeRequest::class, 'email_log' => EmailLog::class, 'festival_section' => FestivalSection::class, 'impersonation_session' => ImpersonationSession::class, 'location' => Location::class, 'mfa_backup_code' => MfaBackupCode::class, 'mfa_email_code' => MfaEmailCode::class, 'organisation_email_settings' => OrganisationEmailSettings::class, 'organisation_email_template' => OrganisationEmailTemplate::class, 'person_identity_match' => PersonIdentityMatch::class, 'person_section_preference' => PersonSectionPreference::class, 'person_tag' => PersonTag::class, 'shift' => Shift::class, 'shift_assignment' => ShiftAssignment::class, 'shift_waitlist' => ShiftWaitlist::class, 'time_slot' => TimeSlot::class, 'trusted_device' => TrustedDevice::class, 'user_invitation' => UserInvitation::class, 'user_organisation_tag' => UserOrganisationTag::class, 'volunteer_availability' => VolunteerAvailability::class, // Form-builder models used as activity-log subjects and (S2+) // polymorphic webhook payload subjects. 'form_schema' => FormSchema::class, 'form_schema_section' => FormSchemaSection::class, 'form_field' => FormField::class, 'form_field_library' => FormFieldLibrary::class, 'form_submission' => FormSubmission::class, 'form_submission_section_status' => FormSubmissionSectionStatus::class, 'form_submission_delegation' => FormSubmissionDelegation::class, 'form_submission_action_failure' => \App\Models\FormBuilder\FormSubmissionActionFailure::class, 'form_value' => FormValue::class, 'form_value_option' => FormValueOption::class, 'form_template' => FormTemplate::class, 'form_schema_webhook' => FormSchemaWebhook::class, 'form_webhook_delivery' => FormWebhookDelivery::class, ]; // Block 3 — framework types (spatie/activitylog). Sanctum is absent // on purpose; see class-level docblock. $frameworkTypes = [ // activitylog causer/subject fall-back entries are not needed // here because every framework morph value lands on a registered // domain alias above. Reserved for future framework extensions. ]; Relation::enforceMorphMap(array_merge( $domainSubjectTypes, $nonPurposeDomainTypes, $frameworkTypes, )); } }