refactor: Remove SynchronizesModelData trait and implement syncData method for model synchronization

This commit is contained in:
Andras Bacsai 2025-11-04 09:18:05 +01:00
parent 3d9c4954c1
commit faa62dec57
7 changed files with 548 additions and 267 deletions

View file

@ -267,6 +267,353 @@ 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
<div>
<form wire:submit="submit">
<x-forms.input
canGate="update"
:canResource="$application"
id="name"
label="Name"
required />
<x-forms.input
canGate="update"
:canResource="$application"
id="gitRepository"
label="Git Repository" />
<x-forms.input
canGate="update"
:canResource="$application"
id="installCommand"
label="Install Command" />
<x-forms.checkbox
instantSave
canGate="update"
:canResource="$application"
id="isStatic"
label="Static Site" />
<x-forms.button
canGate="update"
:canResource="$application"
type="submit">
Save Changes
</x-forms.button>
</form>
</div>
```
**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

View file

@ -1,35 +0,0 @@
<?php
namespace App\Livewire\Concerns;
trait SynchronizesModelData
{
/**
* Define the mapping between component properties and model keys.
*
* @return array<string, string> 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);
}
}
}

View file

@ -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.<br><br>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);

View file

@ -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);
}

View file

@ -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.<br><br>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);

View file

@ -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()) {

View file

@ -1,163 +0,0 @@
<?php
/**
* Tests for SynchronizesModelData trait
*
* NOTE: These tests verify that the trait properly handles nested Eloquent models
* and marks them as dirty when syncing properties.
*/
use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\Application;
use App\Models\ApplicationSetting;
it('syncs nested eloquent model properties correctly', function () {
// Create a test component that uses the trait
$component = new class
{
use SynchronizesModelData;
public bool $is_static = true;
public Application $application;
protected function getModelBindings(): array
{
return [
'is_static' => '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();
});