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