feature: add ConnectionProvider creation flow

- Introduced `HandlesOperations` trait for reusable error handling with toasts and logging.
- Added `StoreConnectionProviderForm` Livewire component with validation and slug uniqueness check.
- Implemented `StoreConnectionProviderRequestData` DTO for structured data handling.
- Updated `ConnectionProviderService` to include `create()` method using database transactions.
- Registered new routes for creating connection providers.
- Enhanced `ConnectionProvider` model with `Fillable` attribute for `name` and `slug`.
This commit is contained in:
= 2026-05-14 10:32:19 +00:00
parent db8e34b622
commit bdffb30b12
7 changed files with 169 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Concerns;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Mary\Traits\Toast;
use Throwable;
trait HandlesOperations
{
use Toast;
public function attempt(callable $action, ?callable $onError = null, string $successMessage = 'Success !', string $errorMessage = 'Something gone wrong!'): void
{
try {
// call the action callback
$action();
$this->success($successMessage);
} catch (ValidationException $exception) {
/**
* We are catching this exception intentionally so that, generic throwable catch block
* doesn't catch this, which will lead to close the modal.
*
* But, we are rethrowing this so that validation errors can be handled by others.
*/
throw $exception;
} catch (Throwable $exception) {
if (null !== $onError) {
// call the callback
$onError();
}
$this->error($errorMessage);
Log::error($exception->getMessage());
Log::error($exception->getTraceAsString());
}
}
}

View File

@ -0,0 +1,21 @@
<?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 StoreConnectionProviderRequestData extends Data
{
public string $slug;
public function __construct(
public string $name,
) {
$this->slug = str($name)->slug()->toString();
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Livewire\Forms;
use App\Data\ConnectedApp\StoreConnectionProviderRequestData;
use App\Models\ConnectionProvider;
use App\Services\ConnectionProviderService;
use Closure;
use Illuminate\Support\Str;
use Livewire\Form;
class StoreConnectionProviderForm extends Form
{
public string $name = '';
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:3',
'max:255',
// Custom Closure to check the generated slug
function (string $attribute, mixed $value, Closure $fail): void {
$slug = Str::slug($value);
// Check if the slug already exists in your database table
$exists = ConnectionProvider::query()
->where('slug', $slug)
->exists();
if ($exists) {
$fail("The name '{$value}' is already taken. Please choose a different name.");
}
},
],
];
}
public function save(): void
{
$this->validate();
$service = app(ConnectionProviderService::class);
$data = new StoreConnectionProviderRequestData(
name: $this->name,
);
$service->create($data);
}
}

View File

@ -4,6 +4,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'slug'])]
final class ConnectionProvider extends Model {}

View File

@ -4,11 +4,20 @@
namespace App\Services;
use App\Data\ConnectedApp\StoreConnectionProviderRequestData;
use App\Models\ConnectionProvider;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ConnectionProviderService
{
public function create(StoreConnectionProviderRequestData $requestData): void
{
DB::transaction(function () use ($requestData): void {
ConnectionProvider::query()->create($requestData->toArray());
});
}
/**
* @return Collection<int, ConnectionProvider>
*/

View File

@ -0,0 +1,39 @@
<?php
use App\Concerns\HandlesOperations;
use App\Livewire\Forms\StoreConnectionProviderForm;
use Laravel\Mcp\Server\Attributes\Title;
use Livewire\Component;
use Mary\Traits\Toast;
use Illuminate\Support\Facades\Log;
new
#[Title('Create Connection Provider')]
class extends Component {
use HandlesOperations;
public StoreConnectionProviderForm $form;
public function save(): void
{
$this->attempt(
action: function () {
$this->form->save();
$this->redirectRoute('dashboard', navigate: true);
}
);
}
};
?>
<div>
<x-shared.card :title="__('Create Connection Provider')">
<x-mary-form :no-separator="true" class="p-4" wire:submit="save">
<x-mary-input wire:model.blur="form.name" :label="__('Name')"/>
<x-slot:actions>
<x-mary-button class="btn-primary" type="submit" spinner="save" wire:click="form.store"
:label="__('Save')"/>
</x-slot:actions>
</x-mary-form>
</x-shared.card>
</div>

View File

@ -12,6 +12,10 @@
Route::prefix('connected-apps')->name('connected-apps.')->group(function (): void {
Route::livewire('create', 'pages::connected-apps.create')->name('create');
});
Route::prefix('connection-providers')->name('users.')->group(function (): void {
Route::livewire('create', 'pages::connecton-providers.create')->name('create');
});
});
require __DIR__.'/settings.php';