From faa62dec57129b9c0df2afa192eadea9f9ae22ef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:18:05 +0100 Subject: [PATCH] refactor: Remove SynchronizesModelData trait and implement syncData method for model synchronization --- .cursor/rules/frontend-patterns.mdc | 351 +++++++++++++++++- .../Concerns/SynchronizesModelData.php | 35 -- app/Livewire/Project/Service/EditDomain.php | 35 +- app/Livewire/Project/Service/FileStorage.php | 35 +- .../Service/ServiceApplicationView.php | 75 +++- app/Livewire/Project/Shared/HealthChecks.php | 121 ++++-- tests/Unit/SynchronizesModelDataTest.php | 163 -------- 7 files changed, 548 insertions(+), 267 deletions(-) delete mode 100644 app/Livewire/Concerns/SynchronizesModelData.php delete mode 100644 tests/Unit/SynchronizesModelDataTest.php diff --git a/.cursor/rules/frontend-patterns.mdc b/.cursor/rules/frontend-patterns.mdc index 663490d3b..4730160b2 100644 --- a/.cursor/rules/frontend-patterns.mdc +++ b/.cursor/rules/frontend-patterns.mdc @@ -267,18 +267,365 @@ For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-c ## Form Handling Patterns +### Livewire Component Data Synchronization Pattern + +**IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models. + +#### Property Naming Convention +- **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`) +- **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`) +- **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`) + +#### The syncData() Method Pattern + +```php +use Livewire\Attributes\Validate; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + +class MyComponent extends Component +{ + use AuthorizesRequests; + + public Application $application; + + // Properties with validation attributes + #[Validate(['required'])] + public string $name; + + #[Validate(['string', 'nullable'])] + public ?string $description = null; + + #[Validate(['boolean', 'required'])] + public bool $isStatic = false; + + public function mount() + { + $this->authorize('view', $this->application); + $this->syncData(); // Load from model + } + + public function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->validate(); + + // Sync TO model (camelCase → snake_case) + $this->application->name = $this->name; + $this->application->description = $this->description; + $this->application->is_static = $this->isStatic; + + $this->application->save(); + } else { + // Sync FROM model (snake_case → camelCase) + $this->name = $this->application->name; + $this->description = $this->application->description; + $this->isStatic = $this->application->is_static; + } + } + + public function submit() + { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save to model + $this->dispatch('success', 'Saved successfully.'); + } +} +``` + +#### Validation with #[Validate] Attributes + +All component properties should have `#[Validate]` attributes: + +```php +// Boolean properties +#[Validate(['boolean'])] +public bool $isEnabled = false; + +// Required strings +#[Validate(['string', 'required'])] +public string $name; + +// Nullable strings +#[Validate(['string', 'nullable'])] +public ?string $description = null; + +// With constraints +#[Validate(['integer', 'min:1'])] +public int $timeout; +``` + +#### Benefits of syncData() Pattern + +- **Explicit Control**: Clear visibility of what's being synchronized +- **Type Safety**: #[Validate] attributes provide compile-time validation info +- **Easy Debugging**: Single method to check for data flow issues +- **Maintainability**: All sync logic in one place +- **Flexibility**: Can add custom logic (encoding, transformations, etc.) + +#### Creating New Form Components with syncData() + +#### Step-by-Step Component Creation Guide + +**Step 1: Define properties in camelCase with #[Validate] attributes** +```php +use Livewire\Attributes\Validate; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Component; + +class MyFormComponent extends Component +{ + use AuthorizesRequests; + + // The model we're syncing with + public Application $application; + + // Component properties in camelCase with validation + #[Validate(['string', 'required'])] + public string $name; + + #[Validate(['string', 'nullable'])] + public ?string $gitRepository = null; + + #[Validate(['string', 'nullable'])] + public ?string $installCommand = null; + + #[Validate(['boolean'])] + public bool $isStatic = false; +} +``` + +**Step 2: Implement syncData() method** +```php +public function syncData(bool $toModel = false): void +{ + if ($toModel) { + $this->validate(); + + // Sync TO model (component camelCase → database snake_case) + $this->application->name = $this->name; + $this->application->git_repository = $this->gitRepository; + $this->application->install_command = $this->installCommand; + $this->application->is_static = $this->isStatic; + + $this->application->save(); + } else { + // Sync FROM model (database snake_case → component camelCase) + $this->name = $this->application->name; + $this->gitRepository = $this->application->git_repository; + $this->installCommand = $this->application->install_command; + $this->isStatic = $this->application->is_static; + } +} +``` + +**Step 3: Implement mount() to load initial data** +```php +public function mount() +{ + $this->authorize('view', $this->application); + $this->syncData(); // Load data from model to component properties +} +``` + +**Step 4: Implement action methods with authorization** +```php +public function instantSave() +{ + try { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save component properties to model + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } +} + +public function submit() +{ + try { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save component properties to model + $this->dispatch('success', 'Changes saved successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } +} +``` + +**Step 5: Create Blade view with camelCase bindings** +```blade +
+
+ + + + + + + + + + Save Changes + + +
+``` + +**Key Points**: +- Use `wire:model="camelCase"` and `id="camelCase"` in Blade views +- Component properties are camelCase, database columns are snake_case +- Always include authorization checks (`authorize()`, `canGate`, `canResource`) +- Use `instantSave` for checkboxes that save immediately without form submission + +#### Special Patterns + +**Pattern 1: Related Models (e.g., Application → Settings)** +```php +public function syncData(bool $toModel = false): void +{ + if ($toModel) { + $this->validate(); + + // Sync main model + $this->application->name = $this->name; + $this->application->save(); + + // Sync related model + $this->application->settings->is_static = $this->isStatic; + $this->application->settings->save(); + } else { + // From main model + $this->name = $this->application->name; + + // From related model + $this->isStatic = $this->application->settings->is_static; + } +} +``` + +**Pattern 2: Custom Encoding/Decoding** +```php +public function syncData(bool $toModel = false): void +{ + if ($toModel) { + $this->validate(); + + // Encode before saving + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + } else { + // Decode when loading + $this->customLabels = $this->application->parseContainerLabels(); + } +} +``` + +**Pattern 3: Error Rollback** +```php +public function submit() +{ + $this->authorize('update', $this->resource); + $original = $this->model->getOriginal(); + + try { + $this->syncData(toModel: true); + $this->dispatch('success', 'Saved successfully.'); + } catch (\Throwable $e) { + // Rollback on error + $this->model->setRawAttributes($original); + $this->model->save(); + $this->syncData(); // Reload from model + return handleError($e, $this); + } +} +``` + +#### Property Type Patterns + +**Required Strings** +```php +#[Validate(['string', 'required'])] +public string $name; // No ?, no default, always has value +``` + +**Nullable Strings** +```php +#[Validate(['string', 'nullable'])] +public ?string $description = null; // ?, = null, can be empty +``` + +**Booleans** +```php +#[Validate(['boolean'])] +public bool $isEnabled = false; // Always has default value +``` + +**Integers with Constraints** +```php +#[Validate(['integer', 'min:1'])] +public int $timeout; // Required + +#[Validate(['integer', 'min:1', 'nullable'])] +public ?int $port = null; // Nullable +``` + +#### Testing Checklist + +After creating a new component with syncData(), verify: + +- [ ] All checkboxes save correctly (especially `instantSave` ones) +- [ ] All form inputs persist to database +- [ ] Custom encoded fields (like labels) display correctly if applicable +- [ ] Form validation works for all fields +- [ ] No console errors in browser +- [ ] Authorization checks work (`@can` directives and `authorize()` calls) +- [ ] Error rollback works if exceptions occur +- [ ] Related models save correctly if applicable (e.g., Application + ApplicationSetting) + +#### Common Pitfalls to Avoid + +1. **snake_case in component properties**: Always use camelCase for component properties (e.g., `$gitRepository` not `$git_repository`) +2. **Missing #[Validate] attributes**: Every property should have validation attributes for type safety +3. **Forgetting to call syncData()**: Must call `syncData()` in `mount()` to load initial data +4. **Missing authorization**: Always use `authorize()` in methods and `canGate`/`canResource` in views +5. **View binding mismatch**: Use camelCase in Blade (e.g., `id="gitRepository"` not `id="git_repository"`) +6. **wire:model vs wire:model.live**: Use `.live` for `instantSave` checkboxes to avoid timing issues +7. **Validation sync**: If using `rules()` method, keep it in sync with `#[Validate]` attributes +8. **Related models**: Don't forget to save both main and related models in syncData() method + ### Livewire Forms ```php class ServerCreateForm extends Component { public $name; public $ip; - + protected $rules = [ 'name' => 'required|min:3', 'ip' => 'required|ip', ]; - + public function save() { $this->validate(); diff --git a/app/Livewire/Concerns/SynchronizesModelData.php b/app/Livewire/Concerns/SynchronizesModelData.php deleted file mode 100644 index f8218c715..000000000 --- a/app/Livewire/Concerns/SynchronizesModelData.php +++ /dev/null @@ -1,35 +0,0 @@ - Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content']) - */ - abstract protected function getModelBindings(): array; - - /** - * Synchronize component properties TO the model. - * Copies values from component properties to the model. - */ - protected function syncToModel(): void - { - foreach ($this->getModelBindings() as $property => $modelKey) { - data_set($this, $modelKey, $this->{$property}); - } - } - - /** - * Synchronize component properties FROM the model. - * Copies values from the model to component properties. - */ - protected function syncFromModel(): void - { - foreach ($this->getModelBindings() as $property => $modelKey) { - $this->{$property} = data_get($this, $modelKey); - } - } -} diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index f759dd71e..371c860ca 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -2,14 +2,16 @@ namespace App\Livewire\Project\Service; -use App\Livewire\Concerns\SynchronizesModelData; use App\Models\ServiceApplication; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; class EditDomain extends Component { - use SynchronizesModelData; + use AuthorizesRequests; + public $applicationId; public ServiceApplication $application; @@ -20,6 +22,7 @@ class EditDomain extends Component public $forceSaveDomains = false; + #[Validate(['nullable'])] public ?string $fqdn = null; protected $rules = [ @@ -28,16 +31,24 @@ class EditDomain extends Component public function mount() { - $this->application = ServiceApplication::query()->findOrFail($this->applicationId); + $this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId); $this->authorize('view', $this->application); - $this->syncFromModel(); + $this->syncData(); } - protected function getModelBindings(): array + public function syncData(bool $toModel = false): void { - return [ - 'fqdn' => 'application.fqdn', - ]; + if ($toModel) { + $this->validate(); + + // Sync to model + $this->application->fqdn = $this->fqdn; + + $this->application->save(); + } else { + // Sync from model + $this->fqdn = $this->application->fqdn; + } } public function confirmDomainUsage() @@ -64,8 +75,8 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - // Sync to model for domain conflict check - $this->syncToModel(); + // Sync to model for domain conflict check (without validation) + $this->application->fqdn = $this->fqdn; // Check for domain conflicts if not forcing save if (! $this->forceSaveDomains) { $result = checkDomainUsage(resource: $this->application); @@ -83,7 +94,7 @@ public function submit() $this->validate(); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); updateCompose($this->application); if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); @@ -96,7 +107,7 @@ public function submit() $originalFqdn = $this->application->getOriginal('fqdn'); if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; - $this->syncFromModel(); + $this->syncData(); } return handleError($e, $this); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 40539b13e..2ce4374a0 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -2,7 +2,6 @@ namespace App\Livewire\Project\Service; -use App\Livewire\Concerns\SynchronizesModelData; use App\Models\Application; use App\Models\InstanceSettings; use App\Models\LocalFileVolume; @@ -19,11 +18,12 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Validate; use Livewire\Component; class FileStorage extends Component { - use AuthorizesRequests, SynchronizesModelData; + use AuthorizesRequests; public LocalFileVolume $fileStorage; @@ -37,8 +37,10 @@ class FileStorage extends Component public bool $isReadOnly = false; + #[Validate(['nullable'])] public ?string $content = null; + #[Validate(['required', 'boolean'])] public bool $isBasedOnGit = false; protected $rules = [ @@ -61,15 +63,24 @@ public function mount() } $this->isReadOnly = $this->fileStorage->isReadOnlyVolume(); - $this->syncFromModel(); + $this->syncData(); } - protected function getModelBindings(): array + public function syncData(bool $toModel = false): void { - return [ - 'content' => 'fileStorage.content', - 'isBasedOnGit' => 'fileStorage.is_based_on_git', - ]; + if ($toModel) { + $this->validate(); + + // Sync to model + $this->fileStorage->content = $this->content; + $this->fileStorage->is_based_on_git = $this->isBasedOnGit; + + $this->fileStorage->save(); + } else { + // Sync from model + $this->content = $this->fileStorage->content; + $this->isBasedOnGit = $this->fileStorage->is_based_on_git; + } } public function convertToDirectory() @@ -96,7 +107,7 @@ public function loadStorageOnServer() $this->authorize('update', $this->resource); $this->fileStorage->loadStorageOnServer(); - $this->syncFromModel(); + $this->syncData(); $this->dispatch('success', 'File storage loaded from server.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -165,14 +176,16 @@ public function submit() if ($this->fileStorage->is_directory) { $this->content = null; } - $this->syncToModel(); + // Sync component properties to model + $this->fileStorage->content = $this->content; + $this->fileStorage->is_based_on_git = $this->isBasedOnGit; $this->fileStorage->save(); $this->fileStorage->saveStorageOnServer(); $this->dispatch('success', 'File updated.'); } catch (\Throwable $e) { $this->fileStorage->setRawAttributes($original); $this->fileStorage->save(); - $this->syncFromModel(); + $this->syncData(); return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 20358218f..2a661c4cf 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,20 +2,19 @@ namespace App\Livewire\Project\Service; -use App\Livewire\Concerns\SynchronizesModelData; use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; class ServiceApplicationView extends Component { use AuthorizesRequests; - use SynchronizesModelData; public ServiceApplication $application; @@ -31,20 +30,28 @@ class ServiceApplicationView extends Component public $forceSaveDomains = false; + #[Validate(['nullable'])] public ?string $humanName = null; + #[Validate(['nullable'])] public ?string $description = null; + #[Validate(['nullable'])] public ?string $fqdn = null; + #[Validate(['string', 'nullable'])] public ?string $image = null; + #[Validate(['required', 'boolean'])] public bool $excludeFromStatus = false; + #[Validate(['nullable', 'boolean'])] public bool $isLogDrainEnabled = false; + #[Validate(['nullable', 'boolean'])] public bool $isGzipEnabled = false; + #[Validate(['nullable', 'boolean'])] public bool $isStripprefixEnabled = false; protected $rules = [ @@ -79,7 +86,15 @@ public function instantSaveAdvanced() return; } - $this->syncToModel(); + // Sync component properties to model + $this->application->human_name = $this->humanName; + $this->application->description = $this->description; + $this->application->fqdn = $this->fqdn; + $this->application->image = $this->image; + $this->application->exclude_from_status = $this->excludeFromStatus; + $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->is_gzip_enabled = $this->isGzipEnabled; + $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; $this->application->save(); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (\Throwable $e) { @@ -114,24 +129,39 @@ public function mount() try { $this->parameters = get_route_parameters(); $this->authorize('view', $this->application); - $this->syncFromModel(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - protected function getModelBindings(): array + public function syncData(bool $toModel = false): void { - return [ - 'humanName' => 'application.human_name', - 'description' => 'application.description', - 'fqdn' => 'application.fqdn', - 'image' => 'application.image', - 'excludeFromStatus' => 'application.exclude_from_status', - 'isLogDrainEnabled' => 'application.is_log_drain_enabled', - 'isGzipEnabled' => 'application.is_gzip_enabled', - 'isStripprefixEnabled' => 'application.is_stripprefix_enabled', - ]; + if ($toModel) { + $this->validate(); + + // Sync to model + $this->application->human_name = $this->humanName; + $this->application->description = $this->description; + $this->application->fqdn = $this->fqdn; + $this->application->image = $this->image; + $this->application->exclude_from_status = $this->excludeFromStatus; + $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->is_gzip_enabled = $this->isGzipEnabled; + $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; + + $this->application->save(); + } else { + // Sync from model + $this->humanName = $this->application->human_name; + $this->description = $this->application->description; + $this->fqdn = $this->application->fqdn; + $this->image = $this->application->image; + $this->excludeFromStatus = $this->application->exclude_from_status; + $this->isLogDrainEnabled = $this->application->is_log_drain_enabled; + $this->isGzipEnabled = $this->application->is_gzip_enabled; + $this->isStripprefixEnabled = $this->application->is_stripprefix_enabled; + } } public function convertToDatabase() @@ -193,8 +223,15 @@ public function submit() if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - // Sync to model for domain conflict check - $this->syncToModel(); + // Sync to model for domain conflict check (without validation) + $this->application->human_name = $this->humanName; + $this->application->description = $this->description; + $this->application->fqdn = $this->fqdn; + $this->application->image = $this->image; + $this->application->exclude_from_status = $this->excludeFromStatus; + $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->is_gzip_enabled = $this->isGzipEnabled; + $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; // Check for domain conflicts if not forcing save if (! $this->forceSaveDomains) { $result = checkDomainUsage(resource: $this->application); @@ -212,7 +249,7 @@ public function submit() $this->validate(); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); updateCompose($this->application); if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); @@ -224,7 +261,7 @@ public function submit() $originalFqdn = $this->application->getOriginal('fqdn'); if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; - $this->syncFromModel(); + $this->syncData(); } return handleError($e, $this); diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index c8029761d..05f786690 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -2,42 +2,54 @@ namespace App\Livewire\Project\Shared; -use App\Livewire\Concerns\SynchronizesModelData; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Validate; use Livewire\Component; class HealthChecks extends Component { use AuthorizesRequests; - use SynchronizesModelData; public $resource; // Explicit properties + #[Validate(['boolean'])] public bool $healthCheckEnabled = false; + #[Validate(['string'])] public string $healthCheckMethod; + #[Validate(['string'])] public string $healthCheckScheme; + #[Validate(['string'])] public string $healthCheckHost; + #[Validate(['nullable', 'string'])] public ?string $healthCheckPort = null; + #[Validate(['string'])] public string $healthCheckPath; + #[Validate(['integer'])] public int $healthCheckReturnCode; + #[Validate(['nullable', 'string'])] public ?string $healthCheckResponseText = null; + #[Validate(['integer', 'min:1'])] public int $healthCheckInterval; + #[Validate(['integer', 'min:1'])] public int $healthCheckTimeout; + #[Validate(['integer', 'min:1'])] public int $healthCheckRetries; + #[Validate(['integer'])] public int $healthCheckStartPeriod; + #[Validate(['boolean'])] public bool $customHealthcheckFound = false; protected $rules = [ @@ -56,36 +68,69 @@ class HealthChecks extends Component 'customHealthcheckFound' => 'boolean', ]; - protected function getModelBindings(): array - { - return [ - 'healthCheckEnabled' => 'resource.health_check_enabled', - 'healthCheckMethod' => 'resource.health_check_method', - 'healthCheckScheme' => 'resource.health_check_scheme', - 'healthCheckHost' => 'resource.health_check_host', - 'healthCheckPort' => 'resource.health_check_port', - 'healthCheckPath' => 'resource.health_check_path', - 'healthCheckReturnCode' => 'resource.health_check_return_code', - 'healthCheckResponseText' => 'resource.health_check_response_text', - 'healthCheckInterval' => 'resource.health_check_interval', - 'healthCheckTimeout' => 'resource.health_check_timeout', - 'healthCheckRetries' => 'resource.health_check_retries', - 'healthCheckStartPeriod' => 'resource.health_check_start_period', - 'customHealthcheckFound' => 'resource.custom_healthcheck_found', - ]; - } - public function mount() { $this->authorize('view', $this->resource); - $this->syncFromModel(); + $this->syncData(); + } + + public function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->validate(); + + // Sync to model + $this->resource->health_check_enabled = $this->healthCheckEnabled; + $this->resource->health_check_method = $this->healthCheckMethod; + $this->resource->health_check_scheme = $this->healthCheckScheme; + $this->resource->health_check_host = $this->healthCheckHost; + $this->resource->health_check_port = $this->healthCheckPort; + $this->resource->health_check_path = $this->healthCheckPath; + $this->resource->health_check_return_code = $this->healthCheckReturnCode; + $this->resource->health_check_response_text = $this->healthCheckResponseText; + $this->resource->health_check_interval = $this->healthCheckInterval; + $this->resource->health_check_timeout = $this->healthCheckTimeout; + $this->resource->health_check_retries = $this->healthCheckRetries; + $this->resource->health_check_start_period = $this->healthCheckStartPeriod; + $this->resource->custom_healthcheck_found = $this->customHealthcheckFound; + + $this->resource->save(); + } else { + // Sync from model + $this->healthCheckEnabled = $this->resource->health_check_enabled; + $this->healthCheckMethod = $this->resource->health_check_method; + $this->healthCheckScheme = $this->resource->health_check_scheme; + $this->healthCheckHost = $this->resource->health_check_host; + $this->healthCheckPort = $this->resource->health_check_port; + $this->healthCheckPath = $this->resource->health_check_path; + $this->healthCheckReturnCode = $this->resource->health_check_return_code; + $this->healthCheckResponseText = $this->resource->health_check_response_text; + $this->healthCheckInterval = $this->resource->health_check_interval; + $this->healthCheckTimeout = $this->resource->health_check_timeout; + $this->healthCheckRetries = $this->resource->health_check_retries; + $this->healthCheckStartPeriod = $this->resource->health_check_start_period; + $this->customHealthcheckFound = $this->resource->custom_healthcheck_found; + } } public function instantSave() { $this->authorize('update', $this->resource); - $this->syncToModel(); + // Sync component properties to model + $this->resource->health_check_enabled = $this->healthCheckEnabled; + $this->resource->health_check_method = $this->healthCheckMethod; + $this->resource->health_check_scheme = $this->healthCheckScheme; + $this->resource->health_check_host = $this->healthCheckHost; + $this->resource->health_check_port = $this->healthCheckPort; + $this->resource->health_check_path = $this->healthCheckPath; + $this->resource->health_check_return_code = $this->healthCheckReturnCode; + $this->resource->health_check_response_text = $this->healthCheckResponseText; + $this->resource->health_check_interval = $this->healthCheckInterval; + $this->resource->health_check_timeout = $this->healthCheckTimeout; + $this->resource->health_check_retries = $this->healthCheckRetries; + $this->resource->health_check_start_period = $this->healthCheckStartPeriod; + $this->resource->custom_healthcheck_found = $this->customHealthcheckFound; $this->resource->save(); $this->dispatch('success', 'Health check updated.'); } @@ -96,7 +141,20 @@ public function submit() $this->authorize('update', $this->resource); $this->validate(); - $this->syncToModel(); + // Sync component properties to model + $this->resource->health_check_enabled = $this->healthCheckEnabled; + $this->resource->health_check_method = $this->healthCheckMethod; + $this->resource->health_check_scheme = $this->healthCheckScheme; + $this->resource->health_check_host = $this->healthCheckHost; + $this->resource->health_check_port = $this->healthCheckPort; + $this->resource->health_check_path = $this->healthCheckPath; + $this->resource->health_check_return_code = $this->healthCheckReturnCode; + $this->resource->health_check_response_text = $this->healthCheckResponseText; + $this->resource->health_check_interval = $this->healthCheckInterval; + $this->resource->health_check_timeout = $this->healthCheckTimeout; + $this->resource->health_check_retries = $this->healthCheckRetries; + $this->resource->health_check_start_period = $this->healthCheckStartPeriod; + $this->resource->custom_healthcheck_found = $this->customHealthcheckFound; $this->resource->save(); $this->dispatch('success', 'Health check updated.'); } catch (\Throwable $e) { @@ -111,7 +169,20 @@ public function toggleHealthcheck() $wasEnabled = $this->healthCheckEnabled; $this->healthCheckEnabled = ! $this->healthCheckEnabled; - $this->syncToModel(); + // Sync component properties to model + $this->resource->health_check_enabled = $this->healthCheckEnabled; + $this->resource->health_check_method = $this->healthCheckMethod; + $this->resource->health_check_scheme = $this->healthCheckScheme; + $this->resource->health_check_host = $this->healthCheckHost; + $this->resource->health_check_port = $this->healthCheckPort; + $this->resource->health_check_path = $this->healthCheckPath; + $this->resource->health_check_return_code = $this->healthCheckReturnCode; + $this->resource->health_check_response_text = $this->healthCheckResponseText; + $this->resource->health_check_interval = $this->healthCheckInterval; + $this->resource->health_check_timeout = $this->healthCheckTimeout; + $this->resource->health_check_retries = $this->healthCheckRetries; + $this->resource->health_check_start_period = $this->healthCheckStartPeriod; + $this->resource->custom_healthcheck_found = $this->customHealthcheckFound; $this->resource->save(); if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) { diff --git a/tests/Unit/SynchronizesModelDataTest.php b/tests/Unit/SynchronizesModelDataTest.php deleted file mode 100644 index 4551fb056..000000000 --- a/tests/Unit/SynchronizesModelDataTest.php +++ /dev/null @@ -1,163 +0,0 @@ - 'application.settings.is_static', - ]; - } - - // Expose protected method for testing - public function testSync(): void - { - $this->syncToModel(); - } - }; - - // Create real ApplicationSetting instance - $settings = new ApplicationSetting; - $settings->is_static = false; - - // Create Application instance - $application = new Application; - $application->setRelation('settings', $settings); - - $component->application = $application; - $component->is_static = true; - - // Sync to model - $component->testSync(); - - // Verify the value was set on the model - expect($component->application->settings->is_static)->toBeTrue(); -}); - -it('syncs boolean values correctly', function () { - $component = new class - { - use SynchronizesModelData; - - public bool $is_spa = true; - - public bool $is_build_server_enabled = false; - - public Application $application; - - protected function getModelBindings(): array - { - return [ - 'is_spa' => 'application.settings.is_spa', - 'is_build_server_enabled' => 'application.settings.is_build_server_enabled', - ]; - } - - public function testSync(): void - { - $this->syncToModel(); - } - }; - - $settings = new ApplicationSetting; - $settings->is_spa = false; - $settings->is_build_server_enabled = true; - - $application = new Application; - $application->setRelation('settings', $settings); - - $component->application = $application; - - $component->testSync(); - - expect($component->application->settings->is_spa)->toBeTrue() - ->and($component->application->settings->is_build_server_enabled)->toBeFalse(); -}); - -it('syncs from model to component correctly', function () { - $component = new class - { - use SynchronizesModelData; - - public bool $is_static = false; - - public bool $is_spa = false; - - public Application $application; - - protected function getModelBindings(): array - { - return [ - 'is_static' => 'application.settings.is_static', - 'is_spa' => 'application.settings.is_spa', - ]; - } - - public function testSyncFrom(): void - { - $this->syncFromModel(); - } - }; - - $settings = new ApplicationSetting; - $settings->is_static = true; - $settings->is_spa = true; - - $application = new Application; - $application->setRelation('settings', $settings); - - $component->application = $application; - - $component->testSyncFrom(); - - expect($component->is_static)->toBeTrue() - ->and($component->is_spa)->toBeTrue(); -}); - -it('handles properties that do not exist gracefully', function () { - $component = new class - { - use SynchronizesModelData; - - public Application $application; - - protected function getModelBindings(): array - { - return [ - 'non_existent_property' => 'application.name', - ]; - } - - public function testSync(): void - { - $this->syncToModel(); - } - }; - - $application = new Application; - $component->application = $application; - - // Should not throw an error - $component->testSync(); - - expect(true)->toBeTrue(); -});