wip: refactor service connection components and add ConnectionProtocol and ConnectionProvider services

- Renamed `ConnectedServicesService` to `ConnectedAppService` and adjusted related factories, DTOs, and forms.
- Introduced `ConnectionProtocolService` and `ConnectionProviderService` for managing protocols and providers.
- Updated Livewire forms to improve validation and allow real-time provider searches.
- Added a `ConnectionStatus` field and related dropdown to service connection forms.
- Enhanced form handling with new services, search functionality, and validation rules.
- Refactored factory and DTO structure for better consistency and modularity.
- Added service layer documentation and streamlined component data mappings.
This commit is contained in:
= 2026-05-13 13:13:29 +00:00
parent 6f07068b1d
commit df3a37d6cb
11 changed files with 159 additions and 39 deletions

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Data\ConnectService;
use Spatie\LaravelData\Data;
class ConnectServiceData extends Data
{
public function __construct(
public string $name,
public int $serviceProviderId,
public int $serviceProtocolId,
public ?string $connectionService = null,
public ?string $logoUrl = null,
public ?string $slug = null,
) {}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Data\ConnectedApp;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
class ConnectAppRequest extends Data
{
public function __construct(
public string $name,
public int $connectionProviderId,
public int $connectionProtocolId,
public ?string $connectionService = null,
public ?string $logoUrl = null,
public ?string $slug = null,
public ?int $statusId = null,
) {}
}

View File

@ -4,25 +4,26 @@
namespace App\Factories; namespace App\Factories;
use App\Data\ConnectService\ConnectServiceData; use App\Data\ConnectedApp\ConnectAppRequest;
use App\Enums\ConnectionStatusEnum; use App\Enums\ConnectionStatusEnum;
use App\Models\ConnectionStatus;
final class ConnectedAppFactory final class ConnectedAppFactory
{ {
public function createDataFromDto(ConnectServiceData $data): array public function createDataFromDto(ConnectAppRequest $data): array
{ {
$result = [ $result = [
'name' => $data->name, 'name' => $data->name,
'service_provider_id' => $data->serviceProviderId, 'connection_provider_id' => $data->connectionProviderId,
'service_protocol_id' => $data->serviceProtocolId, 'connection_protocol_id' => $data->connectionProtocolId,
'logo_url' => $data->logoUrl, 'logo_url' => $data->logoUrl,
'slug' => $data->slug, 'slug' => $data->slug,
'service_status_id' => $data->statusId, 'connection_status_id' => $data->statusId,
]; ];
// Check if the staus is filled, if not select disconnected by default // Check if the staus is filled, if not select disconnected by default
if (null === $data->statusId) { if (null === $data->statusId) {
$status = ConnectionStatusEnum::query() $status = ConnectionStatus::query()
->where('name', '=', ConnectionStatusEnum::Disconnected->value) ->where('name', '=', ConnectionStatusEnum::Disconnected->value)
->pluck('id'); ->pluck('id');
$result['service_status_id'] = $status[0]; $result['service_status_id'] = $status[0];

View File

@ -0,0 +1,16 @@
All the forms used in the application are stored here.
Each form should validate the data before sending it to the server.
This is done by using the `validate` method.
Validations that require the db, should not be live (realtime),
they should be done on the only once, preferably after the form is submitted.
Form can be used to both create and update data. Name should start with:
- `Store` for both cases - preferred
- `Create` for creating a new record
- `Update` for updating an existing record
When handle both cases, use separate `save` and `update` methods.
Livewire form docs: https://livewire.laravel.com/docs/4.x/forms

View File

@ -4,8 +4,8 @@
namespace App\Livewire\Forms; namespace App\Livewire\Forms;
use App\Data\ConnectService\ConnectServiceData; use App\Data\ConnectedApp\ConnectAppRequest;
use App\Services\ConnectedServicesService; use App\Services\ConnectedAppService;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Form; use Livewire\Form;
@ -14,12 +14,15 @@ class StoreConnectedAppFrom extends Form
#[Validate('required|string|max:255|min:3')] #[Validate('required|string|max:255|min:3')]
public string $name = ''; public string $name = '';
#[Validate('required|numeric|min:1')] #[Validate('required|numeric|min:1|exists:connection_providers,id')]
public int $providerId = 0; public int $providerId = 0;
#[Validate('required|numeric|min:1')] #[Validate('required|numeric|min:1|exists:connection_protocols,id')]
public int $protocolId = 0; public int $protocolId = 0;
#[Validate('required|numeric|min:1|exists:connection_statuses,id')]
public int $statusId = 0;
#[Validate('nullable|string|max:255|min:3')] #[Validate('nullable|string|max:255|min:3')]
public ?string $logoUrl = null; public ?string $logoUrl = null;
@ -27,15 +30,16 @@ public function save(): void
{ {
$this->validate(); $this->validate();
$serviceData = new ConnectServiceData( $serviceData = new ConnectAppRequest(
name: $this->name, name: $this->name,
serviceProviderId: $this->providerId, connectionProviderId: $this->providerId,
serviceProtocolId: $this->protocolId, connectionProtocolId: $this->protocolId,
logoUrl: $this->logoUrl, logoUrl: $this->logoUrl,
slug: str($this->name)->slug()->toString(), slug: str($this->name)->slug()->toString(),
statusId: $this->statusId,
); );
app(ConnectedServicesService::class)->create($serviceData); app(ConnectedAppService::class)->create($serviceData);
$this->reset(); $this->reset();
} }
} }

View File

@ -4,14 +4,14 @@
namespace App\Services; namespace App\Services;
use App\Data\ConnectService\ConnectServiceData; use App\Data\ConnectedApp\ConnectAppRequest;
use App\Factories\ConnectedAppFactory; use App\Factories\ConnectedAppFactory;
use App\Models\ConnectedApp; use App\Models\ConnectedApp;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final class ConnectedServicesService final class ConnectedAppService
{ {
public function create(ConnectServiceData $data): void public function create(ConnectAppRequest $data): void
{ {
$data = app(ConnectedAppFactory::class)->createDataFromDto($data); $data = app(ConnectedAppFactory::class)->createDataFromDto($data);
DB::transaction( DB::transaction(

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\ConnectionProtocol;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
readonly class ConnectionProtocolService
{
/**
* @var Builder<ConnectionProtocol>
*/
private Builder $builder;
public function __construct(
) {
$this->builder = ConnectionProtocol::query();
}
/**
* @return Collection<int, ConnectionProtocol>
*/
public function getAll(): Collection
{
return $this->builder->get();
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\ConnectionProvider;
use Illuminate\Support\Collection;
class ConnectionProviderService
{
/**
* @return Collection<int, ConnectionProvider>
*/
public function search(string $value): Collection
{
return ConnectionProvider::query()
->whereLike('slug', "%{$value}%")
->orWhereLike('name', "%{$value}%")
->get();
}
}

6
art/STRUCTURE.md Normal file
View File

@ -0,0 +1,6 @@
## Service Layer
Service layer should return DTO objects to application layer (Livewire Components, Forms, Views).
As Livewire sends whole properties to the component, any data returned from Repository should not be returned to the
components.
It's a critical security concern not to expose any sensitive data to the components.

View File

@ -3,6 +3,10 @@
use App\Livewire\Forms\StoreConnectedAppFrom; use App\Livewire\Forms\StoreConnectedAppFrom;
use App\Models\ConnectionProtocol; use App\Models\ConnectionProtocol;
use App\Models\ConnectionProvider; use App\Models\ConnectionProvider;
use App\Models\ConnectionStatus;
use App\Services\ConnectionProtocolService;
use App\Services\ConnectionProviderService;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Component; use Livewire\Component;
use Livewire\Attributes\Title; use Livewire\Attributes\Title;
@ -18,15 +22,36 @@ public function providers()
return ConnectionProvider::query()->get(); return ConnectionProvider::query()->get();
} }
#[Computed] #[Computed]
public function protocols() public function protocols(): Collection
{ {
return ConnectionProtocol::query()->get()->map(function (ConnectionProtocol $protocol) { $protocolService = app(ConnectionProtocolService::class);
$protocol->name = ucfirst($protocol->name);
return $protocolService->getAll()->map(function (ConnectionProtocol $protocol) {
$protocol->name = strtoupper($protocol->name);
return $protocol; return $protocol;
}); });
} }
/**
* @returns Collection<int, ConnectionStatus>
*/
#[Computed]
public function connectionStatuses(): Collection
{
return ConnectionStatus::query()->get()->map(function (ConnectionStatus $status) {
$status->name = ucfirst($status->name);
return $status;
});
}
public function searchProvider(string $name)
{
$providerService = app(ConnectionProviderService::class);
$this->providers = $providerService->search($name);
}
public function save() public function save()
{ {
$this->form->save(); $this->form->save();
@ -54,6 +79,16 @@ public function save()
:single="true" :single="true"
:options="$this->providers" :options="$this->providers"
searchable searchable
search-function="searchProvider"
debounce="300"
/>
<x-choices
required
wire:model.live="form.statusId"
:label="__('Status')"
:single="true"
:options="$this->connectionStatuses"
placeholder="Select"
/> />
<div class="md:col-span-2 flex gap-x-4 font-medium justify-end"> <div class="md:col-span-2 flex gap-x-4 font-medium justify-end">
<x-shared.button>Cancel</x-shared.button> <x-shared.button>Cancel</x-shared.button>

2
storage/debugbar/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore