Browse Source
- Implement new console commands (, ) and stubs for improved development workflow. - Standardize domain structures and refactor relationships across Academic, Account, Admission, and Channel modules. - Add infrastructure support for API endpoints and update system settings synchronization. - Apply widespread UI/UX updates to Livewire components and Blade views across identity and system modules. Co-authored-by: Junie <junie@jetbrains.com>master
258 changed files with 4832 additions and 2256 deletions
@ -0,0 +1,193 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Console\Commands; |
||||
|
|
||||
|
use Illuminate\Console\GeneratorCommand; |
||||
|
use Illuminate\Support\Str; |
||||
|
use Yajra\DataTables\Generators\DataTablesMakeCommand; |
||||
|
|
||||
|
class DataTableMakeCommand extends DataTablesMakeCommand |
||||
|
{ |
||||
|
/** |
||||
|
* The name and signature of the console command. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $signature = 'domain:datatable |
||||
|
{name : The name of the DataTable.} |
||||
|
{domain : The domain name.} |
||||
|
{--model= : The name of the model to be used.}'; |
||||
|
|
||||
|
/** |
||||
|
* The console command description. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $description = 'Create a new DataTable service class in a domain.'; |
||||
|
|
||||
|
/** |
||||
|
* Build the class with the given name. |
||||
|
* |
||||
|
* @param string $name |
||||
|
*/ |
||||
|
protected function buildClass($name): string |
||||
|
{ |
||||
|
$stub = $this->files->get($this->getStub()); |
||||
|
|
||||
|
$domain = ucfirst($this->argument('domain')); |
||||
|
$className = class_basename(str_replace('/', '\\', $this->getNameInput())); |
||||
|
|
||||
|
$modelOption = $this->option('model') ?? Str::replaceLast('DataTable', '', $className); |
||||
|
$modelData = $this->resolveModel($modelOption, $domain); |
||||
|
|
||||
|
$modelClass = $modelData['class']; |
||||
|
$modelVariable = Str::camel($modelClass); |
||||
|
$tableId = Str::kebab($modelClass).'-table'; |
||||
|
|
||||
|
$replacements = [ |
||||
|
'{{ namespace }}' => $this->getNamespace($name), |
||||
|
'{{ class }}' => $className, |
||||
|
'{{ modelImport }}' => "use {$modelData['full']};\n", |
||||
|
'{{ model }}' => $modelClass, |
||||
|
'{{ modelVariable }}' => $modelVariable, |
||||
|
'{{ tableId }}' => $tableId, |
||||
|
]; |
||||
|
|
||||
|
return str_replace(array_keys($replacements), array_values($replacements), $stub); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Resolve model data. |
||||
|
*/ |
||||
|
protected function resolveModel(string $modelOption, string $domain): array |
||||
|
{ |
||||
|
if (Str::contains($modelOption, ['\\', '/'])) { |
||||
|
if (Str::startsWith(str_replace('/', '\\', $modelOption), 'App\\')) { |
||||
|
$full = str_replace('/', '\\', $modelOption); |
||||
|
$class = class_basename($full); |
||||
|
} else { |
||||
|
$parts = explode('/', str_replace('\\', '/', $modelOption)); |
||||
|
$targetDomain = ucfirst($parts[0]); |
||||
|
$targetName = implode('\\', array_map('ucfirst', array_slice($parts, 1))); |
||||
|
$full = "App\\Domains\\{$targetDomain}\\Models\\{$targetName}"; |
||||
|
$class = class_basename($full); |
||||
|
} |
||||
|
} else { |
||||
|
$full = "App\\Domains\\{$domain}\\Models\\".ucfirst($modelOption); |
||||
|
$class = ucfirst($modelOption); |
||||
|
} |
||||
|
|
||||
|
return [ |
||||
|
'full' => $full, |
||||
|
'class' => $class, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the destination class path. |
||||
|
* |
||||
|
* @param string $name |
||||
|
*/ |
||||
|
protected function getPath($name): string |
||||
|
{ |
||||
|
$name = Str::replaceFirst($this->rootNamespace(), '', $name); |
||||
|
|
||||
|
return $this->laravel->basePath().'/app/'.str_replace('\\', '/', $name).'.php'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the default namespace for the class. |
||||
|
* |
||||
|
* @param string $rootNamespace |
||||
|
*/ |
||||
|
protected function getDefaultNamespace($rootNamespace): string |
||||
|
{ |
||||
|
$domain = ucfirst($this->argument('domain')); |
||||
|
|
||||
|
return $rootNamespace."\\Http\\DataTables\\{$domain}"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the stub file for the generator. |
||||
|
*/ |
||||
|
protected function getStub(): string |
||||
|
{ |
||||
|
return app_path('Console/stubs/domain-datatable/datatable.stub'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parse the name and format according to the root namespace. |
||||
|
* |
||||
|
* @param string $name |
||||
|
* @return string |
||||
|
*/ |
||||
|
protected function qualifyClass($name) |
||||
|
{ |
||||
|
$name = ltrim($name, '\\/'); |
||||
|
|
||||
|
$rootNamespace = $this->rootNamespace(); |
||||
|
|
||||
|
if (Str::startsWith($name, $rootNamespace)) { |
||||
|
return $name; |
||||
|
} |
||||
|
|
||||
|
return $this->qualifyClass( |
||||
|
$this->getDefaultNamespace(trim($rootNamespace, '\\')).'\\'.$name |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execute the console command. |
||||
|
* |
||||
|
* @return bool|null |
||||
|
*/ |
||||
|
public function handle() |
||||
|
{ |
||||
|
$status = GeneratorCommand::handle(); |
||||
|
|
||||
|
if ($status !== false) { |
||||
|
$this->createView(); |
||||
|
} |
||||
|
|
||||
|
return $status; |
||||
|
} |
||||
|
|
||||
|
protected function createView(): void |
||||
|
{ |
||||
|
$domain = ucfirst($this->argument('domain')); |
||||
|
$className = class_basename(str_replace('/', '\\', $this->getNameInput())); |
||||
|
$capability = Str::replaceLast('DataTable', '', $className); |
||||
|
|
||||
|
$directory = resource_path('views/pages/'.Str::lower($domain).'/'.Str::lower(Str::plural($capability))); |
||||
|
$path = "{$directory}/index.blade.php"; |
||||
|
|
||||
|
if ($this->files->exists($path)) { |
||||
|
$this->components->warn("View already exists: [{$path}]"); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$this->files->ensureDirectoryExists($directory); |
||||
|
|
||||
|
$stub = $this->files->get(app_path('Console/stubs/domain-datatable/datatable-view.stub')); |
||||
|
|
||||
|
$modelOption = $this->option('model') ?? $capability; |
||||
|
$modelData = $this->resolveModel($modelOption, $domain); |
||||
|
|
||||
|
$replacements = [ |
||||
|
'{{ domain }}' => $domain, |
||||
|
'{{ domainLower }}' => Str::lower($domain), |
||||
|
'{{ domainDot }}' => Str::lower($domain), |
||||
|
'{{ capabilityDot }}' => Str::lower(Str::plural($capability)), |
||||
|
'{{ model }}' => $modelData['class'], |
||||
|
'{{ modelFull }}' => $modelData['full'], |
||||
|
'{{ modelVariable }}' => Str::camel($modelData['class']), |
||||
|
]; |
||||
|
|
||||
|
$content = str_replace(array_keys($replacements), array_values($replacements), $stub); |
||||
|
|
||||
|
$this->files->put($path, $content); |
||||
|
|
||||
|
$this->components->info('View created: resources/views/pages/'.Str::lower($domain).'/'.Str::lower(Str::plural($capability)).'/index.blade.php'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Console\Commands; |
||||
|
|
||||
|
use Illuminate\Console\Command; |
||||
|
use Illuminate\Filesystem\Filesystem; |
||||
|
use Illuminate\Support\Str; |
||||
|
|
||||
|
class PageMakeCommand extends Command |
||||
|
{ |
||||
|
protected $signature = 'domain:make-page |
||||
|
{domain : Domain name, e.g. Identity, Account, System} |
||||
|
{capability : Capability name, e.g. Users, Roles, Profile} |
||||
|
{name : Page name, e.g. index, detail} |
||||
|
{--modal : Generate a Livewire modal component}'; |
||||
|
|
||||
|
protected $description = 'Generate a blade view inside resources/views/pages/{domain}/{capability}'; |
||||
|
|
||||
|
public function __construct(protected Filesystem $files) |
||||
|
{ |
||||
|
parent::__construct(); |
||||
|
} |
||||
|
|
||||
|
public function handle(): int |
||||
|
{ |
||||
|
$domain = strtolower($this->argument('domain')); |
||||
|
$capability = strtolower($this->argument('capability')); |
||||
|
$name = $this->argument('name'); |
||||
|
$isModal = $this->option('modal'); |
||||
|
|
||||
|
$directory = resource_path("views/pages/{$domain}/{$capability}"); |
||||
|
|
||||
|
if ($isModal) { |
||||
|
return $this->handleModal($directory, $name); |
||||
|
} |
||||
|
|
||||
|
$path = "{$directory}/{$name}.blade.php"; |
||||
|
|
||||
|
if ($this->files->exists($path)) { |
||||
|
$this->components->error("File already exists: [{$path}]"); |
||||
|
|
||||
|
return self::FAILURE; |
||||
|
} |
||||
|
|
||||
|
$this->files->ensureDirectoryExists($directory); |
||||
|
|
||||
|
$stubPath = app_path('Console/stubs/domain-make/page.stub'); |
||||
|
$stub = $this->files->get($stubPath); |
||||
|
|
||||
|
$this->files->put($path, $stub); |
||||
|
|
||||
|
$this->components->info("Page created: resources/views/pages/{$domain}/{$capability}/{$name}.blade.php"); |
||||
|
|
||||
|
return self::SUCCESS; |
||||
|
} |
||||
|
|
||||
|
protected function handleModal(string $directory, string $name): int |
||||
|
{ |
||||
|
$modalName = Str::kebab($name); |
||||
|
if (! str_ends_with($modalName, '-modal')) { |
||||
|
$modalName .= '-modal'; |
||||
|
} |
||||
|
|
||||
|
$modalDir = "{$directory}/⚡{$modalName}"; |
||||
|
|
||||
|
if ($this->files->exists($modalDir)) { |
||||
|
$this->components->error("Modal directory already exists: [{$modalDir}]"); |
||||
|
|
||||
|
return self::FAILURE; |
||||
|
} |
||||
|
|
||||
|
$this->files->ensureDirectoryExists($modalDir); |
||||
|
|
||||
|
$modelVariable = Str::camel(Str::before($modalName, '-modal')); |
||||
|
|
||||
|
// Class |
||||
|
$classStub = $this->files->get(app_path('Console/stubs/domain-make/page-modal-class.stub')); |
||||
|
$classContent = str_replace('{{ modelVariable }}', $modelVariable, $classStub); |
||||
|
$this->files->put("{$modalDir}/{$modalName}.php", $classContent); |
||||
|
|
||||
|
// View |
||||
|
$viewStub = $this->files->get(app_path('Console/stubs/domain-make/page-modal-view.stub')); |
||||
|
$viewContent = str_replace('{{ modalId }}', $modalName, $viewStub); |
||||
|
$this->files->put("{$modalDir}/{$modalName}.blade.php", $viewContent); |
||||
|
|
||||
|
$this->components->info('Livewire Modal created in: resources/views/pages/...'."/⚡{$modalName}"); |
||||
|
|
||||
|
return self::SUCCESS; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
<x-layouts.app> |
||||
|
<x-card> |
||||
|
{{ $dataTable->table() }} |
||||
|
</x-card> |
||||
|
|
||||
|
<livewire:pages::system.audit.audit-view-modal key-name="ulid" model="\{{ modelFull }}::class" |
||||
|
translation="domains/{{ domainLower }}/field.{{ modelVariable }}."/> |
||||
|
<livewire:datatables.delete-action key-name="ulid" :model="\{{ modelFull }}::class" |
||||
|
:action="\App\Domains\{{ domain }}\Actions\Governance\Remove{{ model }}::class"/> |
||||
|
<livewire:datatables.excel-manager :export-class="\App\Domains\{{ domain }}\Exports\{{ model }}Export::class" |
||||
|
:import-class="\App\Http\Ingestion\Excel\{{ domain }}\{{ model }}Import::class" |
||||
|
resource-name="{{ modelVariable }}"/> |
||||
|
|
||||
|
@push('page-scripts') |
||||
|
@vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js']) |
||||
|
{{ $dataTable->scripts(attributes: ['type' => 'module']) }} |
||||
|
@endpush |
||||
|
</x-layouts.app> |
||||
@ -0,0 +1,125 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace {{ namespace }}; |
||||
|
|
||||
|
{{ modelImport }}use Illuminate\Database\Eloquent\Builder as QueryBuilder; |
||||
|
use Yajra\DataTables\EloquentDataTable; |
||||
|
use Yajra\DataTables\Html\Builder as HtmlBuilder; |
||||
|
use Yajra\DataTables\Html\Button; |
||||
|
use Yajra\DataTables\Html\Column; |
||||
|
use Yajra\DataTables\Services\DataTable; |
||||
|
|
||||
|
class {{ class }} extends DataTable |
||||
|
{ |
||||
|
/** |
||||
|
* Build the DataTable class. |
||||
|
* |
||||
|
* @param QueryBuilder<{{ model }}> $query Results from query() method. |
||||
|
*/ |
||||
|
public function dataTable(QueryBuilder $query): EloquentDataTable |
||||
|
{ |
||||
|
return (new EloquentDataTable($query)) |
||||
|
->addColumn( |
||||
|
'action', |
||||
|
fn ({{ model }} ${{ modelVariable }}) => view('components.datatables.action-button', [ |
||||
|
'view' => [ |
||||
|
'modal' => '{{ modelVariable }}-view-modal', |
||||
|
'permission' => auth()->user()->can('view', ${{ modelVariable }}), |
||||
|
], |
||||
|
'edit' => [ |
||||
|
'modal' => '{{ modelVariable }}-form-modal', |
||||
|
'permission' => auth()->user()->can('update', ${{ modelVariable }}), |
||||
|
], |
||||
|
'delete' => [ |
||||
|
'url' => null, |
||||
|
'title' => __('ui/button.delete'), |
||||
|
'permission' => auth()->user()->can('delete', ${{ modelVariable }}), |
||||
|
'message' => __('ui/confirmation.delete', ['resource' => __('resources.{{ modelVariable }}')]), |
||||
|
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.{{ modelVariable }}')]), |
||||
|
], |
||||
|
'table_name' => '{{ tableId }}', |
||||
|
'id' => ${{ modelVariable }}->ulid, |
||||
|
]) |
||||
|
) |
||||
|
->addIndexColumn(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the query source of dataTable. |
||||
|
* |
||||
|
* @return QueryBuilder<{{ model }}> |
||||
|
*/ |
||||
|
public function query({{ model }} $model): QueryBuilder |
||||
|
{ |
||||
|
return $model->newQuery(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Optional method if you want to use the html builder. |
||||
|
*/ |
||||
|
public function html(): HtmlBuilder |
||||
|
{ |
||||
|
return $this->builder() |
||||
|
->setTableId('{{ tableId }}') |
||||
|
->columns($this->getColumns()) |
||||
|
->minifiedAjax() |
||||
|
->orderBy(1) |
||||
|
->layout([ |
||||
|
'topStart' => [ |
||||
|
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1', |
||||
|
'features' => ['buttons', 'pageLength'], |
||||
|
], |
||||
|
'topEnd' => [ |
||||
|
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1', |
||||
|
'features' => ['search'], |
||||
|
], |
||||
|
|
||||
|
'bottomStart' => 'info', |
||||
|
'bottomEnd' => 'paging', |
||||
|
]) |
||||
|
->parameters([ |
||||
|
'language' => [ |
||||
|
'search' => '', |
||||
|
'searchPlaceholder' => __('ui/button.lookup'), |
||||
|
], |
||||
|
]) |
||||
|
->buttons([ |
||||
|
Button::make('add') |
||||
|
->action('$("#{{ modelVariable }}-form-modal").modal("show");') |
||||
|
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml()) |
||||
|
->addClass('btn-sm'), |
||||
|
Button::make('reload') |
||||
|
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml()) |
||||
|
->addClass('btn-sm'), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the dataTable columns definition. |
||||
|
*/ |
||||
|
public function getColumns(): array |
||||
|
{ |
||||
|
return [ |
||||
|
Column::computed('DT_RowIndex') |
||||
|
->title('#') |
||||
|
->searchable(false) |
||||
|
->orderable(false), |
||||
|
Column::make('created_at') |
||||
|
->title(__('ui/label.created_at')), |
||||
|
Column::computed('action') |
||||
|
->title(__('ui/label.actions')) |
||||
|
->exportable(false) |
||||
|
->printable(false) |
||||
|
->width(60) |
||||
|
->addClass('text-center'), |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the filename for export. |
||||
|
*/ |
||||
|
protected function filename(): string |
||||
|
{ |
||||
|
return '{{ class }}_' . date('YmdHis'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace {{ namespace }}; |
||||
|
|
||||
|
class {{ class }} |
||||
|
{ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
// Inject required Domain and Cross-Domain Actions via constructor composition |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
<?php |
||||
|
|
||||
|
use App\Livewire\Concerns\WithModal; |
||||
|
use App\Livewire\Concerns\WithToast; |
||||
|
use Livewire\Attributes\Locked; |
||||
|
use Livewire\Component; |
||||
|
|
||||
|
new class extends Component |
||||
|
{ |
||||
|
use WithModal; |
||||
|
use WithToast; |
||||
|
|
||||
|
#[Locked] |
||||
|
public ?string $id = null; |
||||
|
|
||||
|
#[Locked] |
||||
|
public string $mode = 'create'; |
||||
|
|
||||
|
protected string $resourceName = '{{ modelVariable }}'; |
||||
|
|
||||
|
public function show(int|string $id): void |
||||
|
{ |
||||
|
$this->id = $id; |
||||
|
$this->mode = 'update'; |
||||
|
// $this->form->fill($this->model->only(['name'])); |
||||
|
} |
||||
|
|
||||
|
public function hide(): void |
||||
|
{ |
||||
|
// $this->form->reset(); |
||||
|
$this->reset('id', 'mode'); |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,11 @@ |
|||||
|
<x-modal id="{{ modalId }}" :title="$this->title" wire:submit="save" wire:loading form livewire> |
||||
|
|
||||
|
<div class="d-flex flex-column gap-3"> |
||||
|
{{-- Form Fields --}} |
||||
|
</div> |
||||
|
|
||||
|
<x-slot:footer> |
||||
|
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-bs-dismiss="modal" /> |
||||
|
<x-button type="submit" theme="primary" :label="__('ui/button.save')" /> |
||||
|
</x-slot:footer> |
||||
|
</x-modal> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<x-layouts.app> |
||||
|
<x-card> |
||||
|
{{-- Page Content --}} |
||||
|
</x-card> |
||||
|
</x-layouts.app> |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\Actions\Api; |
||||
|
|
||||
|
use App\Domains\Channel\DTOs\Api\EndpointUrlDTO; |
||||
|
use App\Domains\Channel\Models\ApiEndpoint; |
||||
|
use Exception; |
||||
|
|
||||
|
class AssignEndpointUrl |
||||
|
{ |
||||
|
/** |
||||
|
* @param EndpointUrlDTO[] $endpointToSave |
||||
|
* @throws Exception |
||||
|
*/ |
||||
|
public function execute(array $endpointToSave): void |
||||
|
{ |
||||
|
$validateArrayDTO = array_map(fn($endpoint) => $endpoint instanceof EndpointUrlDTO, $endpointToSave); |
||||
|
if (in_array(false, $validateArrayDTO)) { |
||||
|
throw new \Exception(sprintf('Invalid parameter. to use this action, the $endpointToSave must be an array of %s', EndpointUrlDTO::class)); |
||||
|
} |
||||
|
|
||||
|
$toSave = array_map(fn(EndpointUrlDTO $endpoint) => [ |
||||
|
'client_id' => $endpoint->clientId, |
||||
|
'key' => $endpoint->field, |
||||
|
'value' => $endpoint->endpointUrl |
||||
|
], $endpointToSave); |
||||
|
ApiEndpoint::upsert($toSave, ['client_id', 'key']); |
||||
|
} |
||||
|
} |
||||
@ -1,13 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
namespace App\Domains\Channel\Actions\AppProvisioning; |
|
||||
|
|
||||
use App\Domains\Channel\Models\Client; |
|
||||
|
|
||||
class DeauthorizConsumerApp |
|
||||
{ |
|
||||
public function execute(Client $client): void |
|
||||
{ |
|
||||
// |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,18 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\Actions\AppProvisioning; |
||||
|
|
||||
|
use App\Domains\Channel\DTOs\AppProvisioning\ModifyConsumerAppDetailDTO; |
||||
|
use App\Domains\Channel\Models\Client; |
||||
|
|
||||
|
class ModifyConsumerAppDetail |
||||
|
{ |
||||
|
public function execute(Client $client, ModifyConsumerAppDetailDTO $dto): void |
||||
|
{ |
||||
|
$client->update([ |
||||
|
'webhook' => $dto->webhook_url, |
||||
|
'auth_type' => $dto->auth_type, |
||||
|
'credentials' => $dto->credentials, |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\DTOs\Api; |
||||
|
|
||||
|
use App\Domains\Channel\Enums\Api\ApiType; |
||||
|
|
||||
|
readonly class EndpointUrlDTO |
||||
|
{ |
||||
|
public function __construct( |
||||
|
public string|int $clientId, |
||||
|
public string $endpointUrl, |
||||
|
public ApiType $field |
||||
|
) {} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\DTOs\Api; |
||||
|
|
||||
|
readonly class SyncDTO |
||||
|
{ |
||||
|
public function __construct( |
||||
|
// |
||||
|
) {} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\DTOs\AppProvisioning; |
||||
|
|
||||
|
use App\Domains\Channel\Enums\Client\AuthType; |
||||
|
|
||||
|
readonly class ModifyConsumerAppDetailDTO |
||||
|
{ |
||||
|
public function __construct( |
||||
|
public string $webhook_url, |
||||
|
public AuthType $auth_type, |
||||
|
public array $credentials, |
||||
|
) {} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\Enums\Api; |
||||
|
|
||||
|
use App\UI\Enums\Concerns\InteractsWithLabels; |
||||
|
use App\UI\Enums\Contracts\HasLabel; |
||||
|
use App\UI\Enums\InputType; |
||||
|
use App\UI\Support\Settings\SettingSchema; |
||||
|
|
||||
|
enum ApiType: string implements HasLabel |
||||
|
{ |
||||
|
use InteractsWithLabels; |
||||
|
|
||||
|
case FACULTY = 'faculty'; |
||||
|
case STUDY_PROGRAM = 'study_program'; |
||||
|
case TERM = 'term'; |
||||
|
case ADMISSION_TRACK = 'admission_track'; |
||||
|
case ADMISSION_SCHEDULE = 'admission_schedule'; |
||||
|
case ACADEMIC_PROGRAM = 'academic_program'; |
||||
|
case FEE_TYPE = 'fee_type'; |
||||
|
|
||||
|
/** |
||||
|
* Centralized Schema Definitions |
||||
|
*/ |
||||
|
public function schema(): SettingSchema |
||||
|
{ |
||||
|
return SettingSchema::make(InputType::TEXTLINE)->default('-'); |
||||
|
} |
||||
|
|
||||
|
public function default(): mixed |
||||
|
{ |
||||
|
return $this->schema()->default; |
||||
|
} |
||||
|
|
||||
|
public function inputAttributes(): array |
||||
|
{ |
||||
|
return array_merge($this->schema()->attributes, [ |
||||
|
'label' => $this->label(), |
||||
|
'options' => $this->schema()->type->isSelect() ? $this->schema()->options : '', |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
public static function section(): array |
||||
|
{ |
||||
|
return [ |
||||
|
[ |
||||
|
__('domains/system/pages.api.sections.academic') => [ |
||||
|
self::FACULTY, |
||||
|
self::STUDY_PROGRAM, |
||||
|
self::TERM, |
||||
|
], |
||||
|
], |
||||
|
[ |
||||
|
__('domains/system/pages.api.sections.admission') => [ |
||||
|
self::ACADEMIC_PROGRAM, |
||||
|
self::ADMISSION_TRACK, |
||||
|
self::ADMISSION_SCHEDULE, |
||||
|
self::FEE_TYPE, |
||||
|
], |
||||
|
], |
||||
|
]; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\Enums\Client; |
||||
|
|
||||
|
use App\Domains\System\Traits\Enum\HasPredicateMethod; |
||||
|
use App\UI\Enums\Concerns\InteractsWithLabels; |
||||
|
use App\UI\Enums\Contracts\HasLabel; |
||||
|
|
||||
|
enum AuthType: string implements HasLabel |
||||
|
{ |
||||
|
use HasPredicateMethod; |
||||
|
use InteractsWithLabels; |
||||
|
|
||||
|
case NONE = 'none'; |
||||
|
case BASIC = 'basic'; |
||||
|
case TOKEN = 'token'; |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Channel\Models; |
||||
|
|
||||
|
use App\Domains\Channel\Enums\Api\ApiType; |
||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable; |
||||
|
use Illuminate\Database\Eloquent\Model; |
||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo; |
||||
|
|
||||
|
#[Fillable(['key', 'value', 'client_id'])] |
||||
|
class ApiEndpoint extends Model |
||||
|
{ |
||||
|
protected $casts = [ |
||||
|
'key' => ApiType::class, |
||||
|
]; |
||||
|
|
||||
|
public function client(): BelongsTo |
||||
|
{ |
||||
|
return $this->belongsTo(Client::class); |
||||
|
} |
||||
|
} |
||||
@ -1,26 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
namespace App\Domains\Identity\Actions\AccessControl; |
|
||||
|
|
||||
use App\Domains\Identity\DTOs\AccessControl\CreateRoleDTO; |
|
||||
use App\Domains\Identity\Models\Role; |
|
||||
use Illuminate\Support\Facades\DB; |
|
||||
use Throwable; |
|
||||
|
|
||||
class DefineSystemRole |
|
||||
{ |
|
||||
/** |
|
||||
* @throws Throwable |
|
||||
*/ |
|
||||
public function execute(CreateRoleDTO $dto): bool |
|
||||
{ |
|
||||
return DB::transaction(function () use ($dto) { |
|
||||
$role = Role::create([ |
|
||||
'name' => $dto->name, |
|
||||
]); |
|
||||
$role->syncPermissions($dto->permissions); |
|
||||
|
|
||||
return true; |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -1,26 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
namespace App\Domains\Identity\Actions\AccessControl; |
|
||||
|
|
||||
use App\Domains\Identity\Enums\RoleType; |
|
||||
use App\Domains\Identity\Models\Role; |
|
||||
use Exception; |
|
||||
|
|
||||
use function in_array; |
|
||||
|
|
||||
class DeleteSystemRole |
|
||||
{ |
|
||||
/** |
|
||||
* @throws Exception |
|
||||
*/ |
|
||||
public function execute(Role $role): void |
|
||||
{ |
|
||||
if (in_array($role->name, [RoleType::ADMIN->value, RoleType::SYSTEM_ADMIN->value])) { |
|
||||
throw new Exception(__('domains/identity/messages.exceptions.cannot_remove_system_role')); |
|
||||
} |
|
||||
|
|
||||
if ($role->users()->exists()) { |
|
||||
throw new Exception(__('domains/identity/messages.exceptions.role_has_users')); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
namespace App\Domains\Identity\Actions\Governance; |
|
||||
|
|
||||
use App\Domains\Identity\Models\User; |
|
||||
|
|
||||
class DeleteUser |
|
||||
{ |
|
||||
public function __construct( |
|
||||
protected PurgeUser $purgeUser, |
|
||||
protected SuspendUser $suspendUser |
|
||||
) {} |
|
||||
|
|
||||
public function execute(User $user): void |
|
||||
{ |
|
||||
if ($user->status->isActive()) { |
|
||||
$this->suspendUser->execute($user); |
|
||||
} else { |
|
||||
$this->purgeUser->execute($user); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
<?php |
|
||||
|
|
||||
namespace App\Domains\System\Actions\Backup; |
|
||||
|
|
||||
use Exception; |
|
||||
use Illuminate\Support\Facades\Artisan; |
|
||||
|
|
||||
class ArchiveSystemBackup |
|
||||
{ |
|
||||
public function __construct(protected SyncBackupCatalog $syncBackupCatalog) {} |
|
||||
|
|
||||
/** |
|
||||
* @throws Exception |
|
||||
*/ |
|
||||
public function execute(): void |
|
||||
{ |
|
||||
$artisanResult = Artisan::call('backup:run', []); |
|
||||
if ($artisanResult !== 0) { |
|
||||
throw new Exception(__('domains/system/messages.backup.backup_error')); |
|
||||
} |
|
||||
|
|
||||
$this->syncBackupCatalog->execute(); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,42 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\System\Actions\Backup; |
||||
|
|
||||
|
use App\Domains\System\Models\Backup; |
||||
|
use Exception; |
||||
|
use Illuminate\Support\Facades\Artisan; |
||||
|
use Illuminate\Support\Facades\Storage; |
||||
|
|
||||
|
class SystemBackup |
||||
|
{ |
||||
|
/** |
||||
|
* @throws Exception |
||||
|
*/ |
||||
|
public function execute(): Backup |
||||
|
{ |
||||
|
$artisanResult = Artisan::call('backup:run', []); |
||||
|
if ($artisanResult !== 0) { |
||||
|
throw new Exception(__('domains/system/messages.backup.backup_error')); |
||||
|
} |
||||
|
|
||||
|
$disk = config('backup.backup.destination.disks')[0] ?? 'local'; |
||||
|
$backupName = config('backup.backup.name') ?? 'Laravel'; |
||||
|
|
||||
|
$files = Storage::disk($disk)->allFiles($backupName); |
||||
|
if (empty($files)) { |
||||
|
throw new Exception(__('domains/system/messages.backup.verification_error')); |
||||
|
} |
||||
|
|
||||
|
$latestFile = collect($files)->last(); |
||||
|
$fileName = $backupName.' '.basename($latestFile); |
||||
|
$sizeInBytes = Storage::disk($disk)->size($latestFile); |
||||
|
|
||||
|
return Backup::create([ |
||||
|
'file_name' => $fileName, |
||||
|
'disk' => $disk, |
||||
|
'path' => $latestFile, |
||||
|
'size' => $sizeInBytes, |
||||
|
'type' => 'full', |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,222 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\System\Traits\Model; |
||||
|
|
||||
|
use Illuminate\Database\Eloquent\Model; |
||||
|
use Illuminate\Support\Facades\App; |
||||
|
use Illuminate\Support\Facades\DB; |
||||
|
use Illuminate\Support\Str; |
||||
|
|
||||
|
/** |
||||
|
* @mixin Model |
||||
|
*/ |
||||
|
trait HasTranslation |
||||
|
{ |
||||
|
/** |
||||
|
* Internal cache storage for translations retrieved from the DB table. |
||||
|
* Format: ['en' => ['name' => 'Value', 'story' => '...'], 'ar' => [...]] |
||||
|
*/ |
||||
|
protected array $translationsCache = []; |
||||
|
|
||||
|
/** |
||||
|
* Tracks whether translation records have been read from the database table for this instance. |
||||
|
*/ |
||||
|
protected bool $translationsLoaded = false; |
||||
|
|
||||
|
/** |
||||
|
* Intercept the standard Eloquent model booting pipeline. |
||||
|
*/ |
||||
|
public static function bootHasTranslation(): void |
||||
|
{ |
||||
|
// Automatically save any dirty or staged translations when the parent model saves |
||||
|
static::saved(function (self $model) { |
||||
|
$model->saveTranslations(); |
||||
|
}); |
||||
|
|
||||
|
// Automatically drop related database rows when the parent record is deleted |
||||
|
static::deleted(function (self $model) { |
||||
|
$model->purgeTranslations(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Magic Getter Interception: |
||||
|
* Intercepts access to properties defined in the $translatable array. |
||||
|
*/ |
||||
|
public function __get($key) |
||||
|
{ |
||||
|
if ($this->isTranslationAttribute($key)) { |
||||
|
return $this->translate($key, $this->getLocale()); |
||||
|
} |
||||
|
|
||||
|
return parent::__get($key); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Magic Setter Interception: |
||||
|
* Intercepts updates to properties defined in the $translatable array. |
||||
|
*/ |
||||
|
public function __set($key, $value) |
||||
|
{ |
||||
|
if ($this->isTranslationAttribute($key)) { |
||||
|
$this->setTranslationValue($key, $this->getLocale(), $value); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
parent::__set($key, $value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Magic Isset Interception: |
||||
|
* Handles isset() evaluations for translatable fields. |
||||
|
*/ |
||||
|
public function __isset($key) |
||||
|
{ |
||||
|
if ($this->isTranslationAttribute($key)) { |
||||
|
return ! is_null($this->translate($key, $this->getLocale())); |
||||
|
} |
||||
|
|
||||
|
return parent::__isset($key); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Override the standard Eloquent fill process to support nested language arrays |
||||
|
* Example: $model->fill(['en' => ['name' => 'Adam'], 'color_value' => 123]) |
||||
|
*/ |
||||
|
public function fill(array $attributes) |
||||
|
{ |
||||
|
foreach ($attributes as $key => $value) { |
||||
|
// Check if the key looks like a language code (e.g., 'en', 'ar', 'id') |
||||
|
if (is_array($value) && (strlen($key) === 2 || strlen($key) === 5)) { |
||||
|
foreach ($value as $translatedKey => $translatedValue) { |
||||
|
if ($this->isTranslationAttribute($translatedKey)) { |
||||
|
$this->setTranslationValue($translatedKey, $key, $translatedValue); |
||||
|
} |
||||
|
} |
||||
|
unset($attributes[$key]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return parent::fill($attributes); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Public method to retrieve a field value for a specific language locale. |
||||
|
*/ |
||||
|
public function translate(string $attribute, ?string $locale = null): ?string |
||||
|
{ |
||||
|
// 1. High-Performance Left-Join Optimization: |
||||
|
// If the query used a leftJoin scope, the column exists directly on the primary attributes array. |
||||
|
if (array_key_exists($attribute, $this->attributes)) { |
||||
|
return $this->attributes[$attribute]; |
||||
|
} |
||||
|
|
||||
|
$locale = $locale ?? $this->getLocale(); |
||||
|
|
||||
|
// 2. Load translations from the database into the cache array if not already loaded |
||||
|
if (! $this->translationsLoaded && $this->exists) { |
||||
|
$this->loadTranslationsFromTable(); |
||||
|
} |
||||
|
|
||||
|
return $this->translationsCache[$locale][$attribute] ?? $this->getFallbackTranslation($attribute); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a specific string property name is declared as a translatable variable. |
||||
|
*/ |
||||
|
public function isTranslationAttribute(string $key): bool |
||||
|
{ |
||||
|
return isset($this->translatable) && in_array($key, $this->translatable); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load all rows from the target database translation table into local memory cache. |
||||
|
*/ |
||||
|
public function loadTranslationsFromTable(): self |
||||
|
{ |
||||
|
$records = DB::table($this->getTranslationTableName()) |
||||
|
->where($this->getTranslationForeignKey(), $this->getKey()) |
||||
|
->get(); |
||||
|
|
||||
|
foreach ($records as $record) { |
||||
|
foreach ((array) $record as $column => $value) { |
||||
|
if (in_array($column, ['id', $this->getTranslationForeignKey(), 'locale'])) { |
||||
|
continue; |
||||
|
} |
||||
|
$this->translationsCache[$record->locale][$column] = $value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$this->translationsLoaded = true; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Staged Memory Update: Write property variables straight into the memory array. |
||||
|
*/ |
||||
|
protected function setTranslationValue(string $attribute, string $locale, ?string $value): void |
||||
|
{ |
||||
|
$this->translationsCache[$locale][$attribute] = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database Mutation: Write cached memory arrays to the database table. |
||||
|
*/ |
||||
|
protected function saveTranslations(): void |
||||
|
{ |
||||
|
foreach ($this->translationsCache as $locale => $attributes) { |
||||
|
if (empty($attributes)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
DB::table($this->getTranslationTableName())->updateOrInsert( |
||||
|
[ |
||||
|
$this->getTranslationForeignKey() => $this->getKey(), |
||||
|
'locale' => $locale, |
||||
|
], |
||||
|
$attributes |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database Purge: Remove records from the database table. |
||||
|
*/ |
||||
|
protected function purgeTranslations(): void |
||||
|
{ |
||||
|
DB::table($this->getTranslationTableName()) |
||||
|
->where($this->getTranslationForeignKey(), $this->getKey()) |
||||
|
->delete(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fallback Logic: Returns the first available translation if the requested language is missing. |
||||
|
*/ |
||||
|
protected function getFallbackTranslation(string $attribute): ?string |
||||
|
{ |
||||
|
foreach ($this->translationsCache as $locale => $attributes) { |
||||
|
if (! empty($attributes[$attribute])) { |
||||
|
return $attributes[$attribute]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected function getLocale(): string |
||||
|
{ |
||||
|
return App::getLocale(); |
||||
|
} |
||||
|
|
||||
|
protected function getTranslationTableName(): string |
||||
|
{ |
||||
|
return Str::singular($this->getTable()).'_translations'; |
||||
|
} |
||||
|
|
||||
|
protected function getTranslationForeignKey(): string |
||||
|
{ |
||||
|
return Str::singular($this->getTable()).'_id'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\UI\Enums\Contracts; |
||||
|
|
||||
|
use App\UI\Support\Settings\BaseSchema; |
||||
|
|
||||
|
interface HasSchema |
||||
|
{ |
||||
|
public function schema(): BaseSchema; |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\UI\Support\Settings; |
||||
|
|
||||
|
interface BaseSchema {} |
||||
@ -0,0 +1,44 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\UI\Support\Settings; |
||||
|
|
||||
|
use App\UI\Enums\InputType; |
||||
|
|
||||
|
readonly class SettingSchema implements BaseSchema |
||||
|
{ |
||||
|
public function __construct( |
||||
|
public InputType $type, |
||||
|
public array $rules = ['required', 'string'], |
||||
|
public mixed $default = null, |
||||
|
public array $options = [], |
||||
|
public array $attributes = [] |
||||
|
) {} |
||||
|
|
||||
|
public static function make(InputType $type, array $rules = ['required', 'string']): self |
||||
|
{ |
||||
|
return new self($type, $rules); |
||||
|
} |
||||
|
|
||||
|
public function default(mixed $default): self |
||||
|
{ |
||||
|
return new self($this->type, $this->rules, $default, $this->options, $this->attributes); |
||||
|
} |
||||
|
|
||||
|
public function rules(array $rules): self |
||||
|
{ |
||||
|
return new self($this->type, $rules, $this->default, $this->options, $this->attributes); |
||||
|
} |
||||
|
|
||||
|
public function options(array $options): self |
||||
|
{ |
||||
|
// If the array is simple (non-associative), we should try to localize the values if they are Enums |
||||
|
// But for now, we just pass it as is, or expect the caller to handle it. |
||||
|
// The common case is ['value' => 'Label'] |
||||
|
return new self($this->type, $this->rules, $this->default, $options, $this->attributes); |
||||
|
} |
||||
|
|
||||
|
public function attributes(array $attributes): self |
||||
|
{ |
||||
|
return new self($this->type, $this->rules, $this->default, $this->options, $attributes); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||||
|
<?php |
||||
|
|
||||
|
use Illuminate\Database\Migrations\Migration; |
||||
|
use Illuminate\Database\Schema\Blueprint; |
||||
|
use Illuminate\Support\Facades\Schema; |
||||
|
|
||||
|
return new class extends Migration |
||||
|
{ |
||||
|
/** |
||||
|
* Run the migrations. |
||||
|
*/ |
||||
|
public function up(): void |
||||
|
{ |
||||
|
Schema::create('api_endpoints', function (Blueprint $table) { |
||||
|
$table->id(); |
||||
|
$table->string('key')->unique(); |
||||
|
$table->string('value'); |
||||
|
$table->foreignId('client_id')->constrained(); |
||||
|
$table->timestamps(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reverse the migrations. |
||||
|
*/ |
||||
|
public function down(): void |
||||
|
{ |
||||
|
Schema::dropIfExists('api_endpoints'); |
||||
|
} |
||||
|
}; |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue