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
+
+
+
+```
+
+**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();
-});