refactor: Remove SynchronizesModelData trait and implement syncData method for model synchronization
This commit is contained in:
parent
3d9c4954c1
commit
faa62dec57
7 changed files with 548 additions and 267 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
Loading…
Reference in a new issue