Init git.

This commit is contained in:
2024-06-18 22:35:54 +05:00
commit a8d656148a
282 changed files with 25996 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Dto\Service\User\ManyRoleDto;
use App\Enums\SystemRole;
use App\Repositories\RoleRepository;
use App\Rules\Username;
use App\Services\User\UserCommandHandler;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Validator;
final class CreateUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:create-user-admin {email} {username} {password}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create admin user.';
/**
* Execute the console command.
*/
public function handle(UserCommandHandler $userCommandHandler, RoleRepository $roleRepository): void
{
$validator = $this->getData();
if ($validator->fails()) {
$this->errorMessageAndStop($validator->errors()->all());
}
$data = $validator->valid();
try {
$role = $roleRepository->getRoleByCode(SystemRole::Admin->value);
if (is_null($role)) {
$this->errorMessageAndStop('Administrator role not found.');
}
DB::transaction(function () use($data, $userCommandHandler, $role) {
$data['name'] = 'Administrator';
$user = $userCommandHandler->handleStore($data, $data['password']);
$userCommandHandler->handleConfirmationByEmail($user);
$roles = new ManyRoleDto([$role->id]);
$userCommandHandler->handleSyncRoles($user, $roles);
});
} catch (\Throwable $e) {
$this->errorMessageAndStop($e->getMessage());
}
$this->info('The command was successful!');
}
private function getData(): Validator
{
return ValidatorFacade::make([
'email' => $this->argument('email'),
'username' => $this->argument('username'),
'password' => $this->argument('password'),
], [
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'username' => ['required', 'string', new Username(), 'unique:users,username'],
'password' => ['required', Password::default()],
]);
}
private function stop(): never
{
exit;
}
private function errorMessageAndStop(string | array $error): never
{
$this->info('User not created. See error messages below:');
if (is_array($error)) {
foreach ($error as $err) {
$this->error($err);
}
} else {
$this->error($error);
}
$this->stop();
}
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use App\Dto\Service\Dto;
interface FormRequestDto
{
public function getDto(): Dto;
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
interface Search
{
public function __construct(Relation | Builder $query);
public function all(): Collection;
public function get(int $limit): Collection;
public function pagination(int $limit, int $page = 1): LengthAwarePaginator;
public function cursorPaginate(int $limit): CursorPaginator;
}

View File

@@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResult
{
public function isSuccess(): bool;
public function isError(): bool;
}

View File

@@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResultError
{
public function getCode(): ?int;
public function getMessage(): string;
public function getErrors(): array;
public function getErrorsOrMessage(): array|string;
public function getData(): array;
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class Role
{
public function __construct(
) { }
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class User
{
public function __construct(
) { }
}

View File

@@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class QuerySettingsDto
{
public function __construct(
private int $limit,
private int $page = 1,
private array $queryWith = []
) { }
public function getLimit(): int
{
return $this->limit;
}
public function getPage(): int
{
return $this->page;
}
public function getQueryWith(): array
{
return $this->queryWith;
}
}

View File

@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Role;
use App\Dto\Builder\Role;
use App\Dto\Service\Pages;
final readonly class Index extends Pages
{
public function __construct(
private Role $roleBuilderDto,
int $page
) {
parent::__construct($page);
}
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Role;
use App\Dto\Service\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private ?string $code,
private array $permissions,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): ?string
{
return $this->code;
}
public function getPermissions(): array
{
return $this->permissions;
}
}

View File

@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\User;
use App\Dto\Builder\User;
use App\Dto\Service\Pages;
final readonly class Index extends Pages
{
public function __construct(
private User $userBuilderDto,
int $page
) {
parent::__construct($page);
}
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
}

View File

@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\User;
use App\Dto\Service\Dto;
use App\Dto\Service\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
private string $username,
private bool $isActive,
private ManyRoleDto $roles,
private ?string $password = null
) { }
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getRoles(): ManyRoleDto
{
return $this->roles;
}
public function getUsername(): string
{
return $this->username;
}
public function isActive(): bool
{
return $this->isActive;
}
}

View File

@@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
final readonly class Authorization extends Dto
{
public function __construct(
private string $email,
private string $password,
private bool $remember = false
) { }
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
public function getRemember(): bool
{
return $this->remember;
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
abstract readonly class Dto
{
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
abstract readonly class Pages extends Dto
{
public function __construct(
private int $page
) { }
public function getPage(): int
{
return $this->page;
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Site\Profile;
use App\Dto\Service\Dto;
final readonly class Update extends Dto
{
public function __construct(
private string $name
) { }
public function getName(): string
{
return $this->name;
}
}

View File

@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Site\Profile;
use App\Dto\Service\Dto;
use App\Enums\Lang;
final readonly class UpdateSettings extends Dto
{
public function __construct(
private ?Lang $lang,
private ?string $timezone
) { }
public function getLang(): ?Lang
{
return $this->lang;
}
public function getTimezone(): ?string
{
return $this->timezone;
}
}

View File

@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\User;
use App\Exceptions\Dto\Service\User\ManyRoleDtoException;
final class ManyRoleDto
{
private array $roles = [];
public function __construct(array $roles = []) {
foreach ($roles as $role) {
if (!is_numeric($role) || is_float($role)) {
throw new ManyRoleDtoException('Not an integer: ' . $role . '.');
}
$this->add((int) $role);
}
}
public function add(int $id): void
{
if ($id < 1) {
throw new ManyRoleDtoException('Only Integer > 0.');
}
$this->roles[] = $id;
}
public function toArray(): array
{
return $this->roles;
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\User;
use App\Dto\Service\Dto;
final readonly class UpdatePassword extends Dto
{
public function __construct(
private string $password
) { }
public function getPassword(): string
{
return $this->password;
}
}

View File

@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Enums;
use Illuminate\Support\Collection;
enum Lang: int
{
case Ru = 1;
case En = 2;
public function getTitle(): string
{
return match ($this) {
self::Ru => 'Русский',
self::En => 'English'
};
}
public function getLocale(): string
{
return match ($this) {
self::Ru => 'ru',
self::En => 'en'
};
}
public static function toArray(): array
{
$choices = [];
foreach (self::cases() as $lang) {
$choices[] = [
'name' => $lang->name,
'value' => $lang->value,
'title' => $lang->getTitle(),
'locale' => $lang->getLocale()
];
}
return $choices;
}
public static function toCollection(): Collection
{
return collect(self::toArray());
}
}

View File

@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case AdminPanel = 'allow-admin-panel';
case Role = 'role';
case User = 'user';
public function getPermissions(): array
{
$permissions = match ($this) {
self::AdminPanel => [
'view' => __('permissions.Administrative panel allowed'),
],
default => $this->getBasePermissions()
};
return $permissions;
}
public function getTitle(): string
{
return __('permissions.' . $this->name);
}
public function formatValue(string $permission): string
{
return $this->value . '.' . $permission;
}
public static function toArrayList(): array
{
$permissions = [];
foreach (self::cases() as $permissionEnum) {
foreach ($permissionEnum->getPermissions() as $permissionName => $permissionTitle) {
$name = $permissionEnum->formatValue($permissionName);
$title = $permissionEnum->getTitle() . ' - ' . $permissionTitle;
$permissions[$name] = $title;
}
}
return $permissions;
}
public static function toArrayListCodes(): array
{
return \array_keys(self::toArrayList());
}
private function getBasePermissions(): array
{
return [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),
'update' => __('permissions.Allowed to edit'),
'delete' => __('permissions.Allowed to delete'),
];
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum SystemRole: string
{
case Admin = 'admin';
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Dto\Service\User;
final class ManyRoleDtoException extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\Rule;
final class RoleSyncPermissionsCommandHandlerException extends \Exception
{
}

View File

@@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace App\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
final readonly class Helpers
{
public static function getTimeZoneList(): Collection
{
return Cache::rememberForever('timezones_list_collection', function () {
$timezone = [];
foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key => $value) {
$timezone[$value] = $value . ' (UTC ' . now($value)->format('P') . ')';
}
return collect($timezone)->sortKeys();
});
}
/**
* $name = 'field[key]' return 'field.key'
*/
public static function formatAttributeNameToRequestName(string $name): string
{
return Str::of($name)
->replace(
['.', '[', ']'],
['_', '.', ''],
)
->rtrim('.')
->value();
}
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
final class DashboardController extends Controller
{
public function index(): View
{
return view('admin/dashboard/index');
}
}

View File

@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Admin\Roles\IndexRequest;
use App\Http\Requests\Admin\Roles\StoreUpdateRequest;
use App\Services\Admin\RoleService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class RolesController extends Controller
{
public function __construct(
private readonly RoleService $roleService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->roleService->index($data->getRoleBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/roles/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->roleService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/roles/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->roleService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/roles/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->roleService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.roles.index')->withSuccess($result->getMessage());
}
}

View File

@@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Admin\Users\IndexRequest;
use App\Http\Requests\Admin\Users\StoreUpdateRequest;
use App\Http\Requests\Admin\Users\UpdatePasswordRequest;
use App\Services\Admin\UserService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class UsersController extends Controller
{
public function __construct(
private readonly UserService $userService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->userService->index($data->getUserBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/users/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->userService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/users/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->userService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/users/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function updatePassword(int $id, UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->updatePassword($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->userService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.users.index')->withSuccess($result->getMessage());
}
}

View File

@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\AuthorizationRequest;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
final class AuthController extends Controller
{
public function __construct(
private readonly AuthService $authService
) { }
public function login(): View
{
return view('login', [
'captcha' => config('app.captcha', false),
]);
}
public function authorization(AuthorizationRequest $request): RedirectResponse
{
$authorization = $request->getDto();
$result = $this->authService->authorization($authorization);
if ($result->isError()) {
if ($result->getCode() === Response::HTTP_UNAUTHORIZED) {
Log::warning('Unauthorized ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
}
return redirect()->route('login')->withInput()->withErrors($result->getMessage());
}
$request->session()->regenerate();
Log::notice('Logged in ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
return redirect()->route('home');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use Illuminate\View\View;
final class HomeController extends Controller
{
public function index(): View
{
return \view('site.home.index');
}
}

View File

@@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Enums\Lang;
use App\Helpers\Helpers;
use App\Http\Requests\Site\Profile\UpdatePasswordRequest;
use App\Http\Requests\Site\Profile\UpdateRequest;
use App\Http\Requests\Site\Profile\UpdateSettingsRequest;
use App\Services\Site\ProfileService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProfileController extends Controller
{
public function __construct(
private readonly ProfileService $profileService
) { }
public function profile(Request $request): View
{
return view('site.profile.profile', [
'user' => $request->user()
]);
}
public function settings(Request $request): View
{
return view('site.profile.settings', [
'user' => $request->user(),
'languages' => Lang::toCollection()->pluck(value: 'title', key: 'value')->toArray(),
'timezone' => Helpers::getTimeZoneList()->toArray(),
]);
}
public function update(UpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->update($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updatePassword(UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updatePassword($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updateSettings(UpdateSettingsRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updateSettings($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.settings')->withSuccess($result->getMessage());
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class AdminPanel
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (
$request->user() === null || $request->user()->cannot('AdminPanel')
) {
abort(403);
}
return $next($request);
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class UserIsActive
{
public function handle(Request $request, \Closure $next)
{
if ($request->user()->is_active === false) {
\abort(Response::HTTP_FORBIDDEN, 'User disabled');
}
return $next($request);
}
}

View File

@@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
final class UserLocale
{
public function handle(Request $request, Closure $next)
{
if ($request->user() && $request->user()->lang) {
App::setLocale($request->user()->lang->getLocale());
}
return $next($request);
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\Role;
use App\Dto\Service\Admin\Role\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('admin.roles.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
roleBuilderDto: new Role(),
page: (int) $this->input('page', 1)
);
}
}

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Role\StoreUpdate;
use App\Rules\Permission;
use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'max:255'],
'permissions' => ['array', new Permission()]
];
if ($this->getMethod() === 'POST') {
$rules['code'] = ['required', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'];
}
return $rules;
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
code: $this->input('code', null),
permissions: $this->input('permissions', [])
);
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\User;
use App\Dto\Service\Admin\User\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('admin.users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
userBuilderDto: new User(),
page: (int) $this->input('page', 1)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\User\StoreUpdate;
use App\Dto\Service\User\ManyRoleDto;
use App\Rules\Username;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'username' => ['required', 'string', new Username()],
'is_active' => ['required', 'boolean'],
'roles' => ['array', Rule::exists('roles', 'id')],
];
if ($this->getMethod() === 'POST') {
$rules['password'] = ['required', Password::default()];
}
return $rules;
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
email: $this->input('email'),
username: $this->input('username'),
isActive: (bool) $this->input('is_active', false),
roles: new ManyRoleDto($this->input('roles', [])),
password: $this->input('password', null),
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Service\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Http\Requests;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Authorization;
use Illuminate\Foundation\Http\FormRequest;
final class AuthorizationRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'],
'remember' => ['nullable', 'boolean'],
];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
}
public function getDto(): Authorization
{
return new Authorization(
email: $this->input('email'),
password: $this->input('password'),
remember: (bool) $this->input('remember', false)
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Site\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', 'confirmed', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Site\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Site\Profile\Update;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'max:255'],
];
}
public function getDto(): Update
{
return new Update(name: $this->input('name'));
}
}

View File

@@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Site\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Site\Profile\UpdateSettings;
use App\Enums\Lang;
use App\Helpers\Helpers;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
final class UpdateSettingsRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'lang' => ['nullable', new Enum(Lang::class)],
'timezone' => ['nullable', Rule::in(Helpers::getTimeZoneList()->keys()->toArray())]
];
}
public function getDto(): UpdateSettings
{
$lang = $this->input('lang', null);
if (!is_null($lang)) {
$lang = Lang::from((int) $lang);
}
return new UpdateSettings(
lang: $lang,
timezone: $this->input('timezone', null),
);
}
}

View File

@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\SystemRole as SystemRoleEnum;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\hasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Role extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'code'
];
public function scopeLatest(Builder $query): Builder
{
return $query->orderBy('id', 'desc');
}
public function scopeAlphavit(Builder $query): Builder
{
return $query->orderBy('name', 'asc');
}
public function permissions(): hasMany
{
return $this->hasMany(RolePermission::class, 'role_id', 'id');
}
protected function isRemove(): Attribute
{
return Attribute::make(
get: fn ($dontRemove) => ( SystemRoleEnum::tryFrom($this->code) === null ),
)->shouldCache();
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => ( $this->code === SystemRoleEnum::Admin->value ),
)->shouldCache();
}
}

View File

@@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\Permission;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class RolePermission extends Model
{
use HasFactory;
protected $table = 'role_permission';
public function getPermissionTitleAttribute(): string
{
$pemissions = Permission::toArrayList();
return $pemissions[$this->permission] ?? '';
}
}

View File

@@ -0,0 +1,102 @@
<?php declare(strict_types=1);
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\Lang;
use App\Enums\SystemRole;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
final class User extends Authenticatable
{
use HasFactory, Notifiable, SoftDeletes;
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'is_active' => true,
];
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'username',
'email',
'password',
'timezone',
'lang',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'lang' => Lang::class,
'is_active' => 'boolean',
];
}
/**
* Return the user's roles
*/
public function roles(): belongsToMany
{
return $this->belongsToMany(Role::class);
}
public function hasRole(string $role): bool
{
return $this->roles->where('code', $role)->isNotEmpty();
}
public function hasPermission(string $permission): bool
{
return $this->permissions->search($permission) !== false;
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => $this->hasRole(SystemRole::Admin->value),
)->shouldCache();
}
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
$roles = $this->roles->modelKeys();
return RolePermission::whereIn('role_id', $roles)->select('permission')->pluck('permission');
},
)->shouldCache();
}
}

View File

@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Enums\SystemRole;
use App\Models\User;
readonly class AdminPanel extends Policy
{
public function view(User $user): bool
{
return $user->hasPermission('allow-admin-panel.view');
}
}

View File

@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
abstract readonly class Policy
{
use HandlesAuthorization;
final public function before(User $user): ?bool
{
if ($user->is_active !== true) {
return false;
}
if ($user->is_admin) {
return true;
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\Role;
use App\Models\User;
final readonly class RolePolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('role.view');
}
public function view(User $user, Role $role): bool
{
return $user->hasPermission('role.view');
}
public function create(User $user): bool
{
return $user->hasPermission('role.create');
}
public function update(User $user, Role $role): bool
{
return $user->hasPermission('role.update');
}
public function delete(User $user, Role $role): bool
{
return $user->hasPermission('role.delete');
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
final readonly class UserPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('user.view');
}
public function view(User $user, User $userView): bool
{
return $user->hasPermission('user.view');
}
public function create(User $user): bool
{
return $user->hasPermission('user.create');
}
public function update(User $user, User $userUpdate): bool
{
return $user->hasPermission('user.update');
}
public function delete(User $user, User $userDelete): bool
{
return $user->hasPermission('user.delete');
}
}

View File

@@ -0,0 +1,71 @@
<?php declare(strict_types=1);
namespace App\Providers;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(CreateSearchInstanceCommand::class, function () {
return new CreateSearchInstanceCommand(Search::class);
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
if (config('app.force_https') === true) {
URL::forceScheme('https');
}
$this->passwordDefaults();
$this->configureRateLimiting();
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
}
/**
* Configure the rate limiters for the application.
*/
private function configureRateLimiting(): void
{
RateLimiter::for('login', function (Request $request) {
return [
Limit::perHour(config('rate_limiting.login_max_request', 50))->by($request->getClientIp()),
Limit::perHour(config('rate_limiting.login_max_email_request', 10))->by($request->getClientIp() . '-' . $request->input('email')),
];
});
}
private function passwordDefaults(): void
{
Password::defaults(function () {
$rule = Password::min(8);
if ($this->app->isProduction()) {
$rule->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised();
}
return $rule;
});
}
}

View File

@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\Role;
use App\Dto\Builder\Role as RoleBuilderDto;
use App\Services\Role\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class RoleRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getRoleById(int $id): ?Role
{
return Role::query()->where('id', $id)->first();
}
public function getRoleByCode(string $code): ?Role
{
return Role::query()->where('code', $code)->first();
}
public function getRoles(RoleBuilderDto $roleBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: Role::query()->with($with),
roleBuilderDto: $roleBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function getRolesForSelect(array $withExcepts = []): array
{
return Role::query()
->when($withExcepts, function (Builder $query, array $withExcepts) {
$query->withTrashed()->whereNull('deleted_at')->orWhereIn('id', $withExcepts);
})
->pluck('name', 'id')
->toArray();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Role::query()
->where('code', $code)
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\User;
use App\Dto\Builder\User as UserBuilderDto;
use App\Services\User\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder;
final readonly class UserRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getUserById(int $id): ?User
{
return User::query()->where('id', $id)->first();
}
public function getUserByEmail(string $email): ?User
{
return User::query()->where('email', Str::lower($email))->first();
}
public function getUsers(UserBuilderDto $userBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: User::query()->with($with),
userBuilderDto: $userBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function isExistsEmail(string $email, ?int $exceptId = null): bool
{
return User::query()
->where('email', Str::lower($email))
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
public function isExistsUsername(string $username, ?int $exceptId = null): bool
{
return User::query()
->where('username', Str::lower($username))
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
final readonly class Permission implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
if (is_string($value)) {
$this->validatePermission($value, $fail);
}
if (is_array($value)) {
foreach ($value as $item) {
$this->validatePermission($item, $fail);
}
}
}
private function validatePermission(string $value, Closure $fail): void
{
$value = explode('.', $value, 2);
if (count($value) !== 2) {
$fail('validation.enum')->translate();
return;
}
list($permissionEnum, $permission) = $value;
$permissionEnum = \App\Enums\Permission::tryFrom($permissionEnum);
if (is_null($permissionEnum)) {
$fail('validation.enum')->translate();
return;
}
$permissions = $permissionEnum->getPermissions();
if (!isset($permissions[$permission])) {
$fail('validation.enum')->translate();
return;
}
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Validator;
use Closure;
final readonly class Repository implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
$validator = Validator::make([
'slug' => $value,
], [
'slug' => 'min:1|max:150|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/',
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$fail($error);
}
}
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Closure;
use Illuminate\Support\Facades\Validator;
final readonly class Username implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
$validator = Validator::make([
'username' => $value,
], [
'username' => 'min:1|max:70|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/',
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$fail($error);
}
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
use App\Contracts\ServiceResult as ServiceResultContract;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
abstract class ServiceResult implements ServiceResultContract
{
public function isSuccess(): bool
{
return $this->isError() === false;
}
public function isError(): bool
{
return $this instanceof ServiceResultErrorContract === true;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
final class ServiceResultArray extends ServiceResult
{
public function __construct(
private readonly array $data,
) { }
public function getData(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
final class ServiceResultError extends ServiceResult implements ServiceResultErrorContract
{
public function __construct(
private readonly string $message,
private readonly array $errors = [],
private readonly ?int $code = null,
) {
}
public function getMessage(): string
{
return $this->message;
}
public function getCode(): ?int
{
return $this->code;
}
public function getErrors(): array
{
return $this->errors;
}
public function getErrorsOrMessage(): array|string
{
if (!empty($this->getErrors())) {
return $this->getErrors();
}
return $this->getMessage();
}
public function getData(): array
{
return [
'message' => $this->getMessage(),
'errors' => $this->errors
];
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\ServiceResults;
final class ServiceResultSuccess extends ServiceResult
{
public function __construct(
private readonly string $message
) { }
public function getMessage(): string
{
return $this->message;
}
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\ServiceResults;
use Illuminate\Database\Eloquent\Model;
final class StoreUpdateResult extends ServiceResult
{
public function __construct(
private readonly Model $model,
private readonly string $message
) { }
public function getModel(): Model
{
return $this->model;
}
public function getMessage(): string
{
return $this->message;
}
}

View File

@@ -0,0 +1,165 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Builder\Role as RoleBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Role\StoreUpdate;
use App\Models\Role;
use App\Models\User;
use App\Repositories\RoleRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Role\RoleCommandHandler;
use App\Services\Role\RoleSyncPermissionsCommandHandler;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
final class RoleService extends Service
{
public function __construct(
private readonly RoleRepository $roleRepository,
private readonly RoleCommandHandler $roleCommandHandler,
private readonly RoleSyncPermissionsCommandHandler $roleSyncPermissionsCommandHandler
) { }
public function index(RoleBuilderDto $roleBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
$roles = $this->roleRepository->getRoles(
$roleBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'roles' => $roles
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'role' => new Role(),
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $role)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'role' => $role,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->roleRepository->isExistsCode($data->getCode())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.code')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.code')])]
);
}
try {
$role = DB::transaction(function () use ($data) {
$dataRole = $this->getDataRole($data);
$dataRole['code'] = $data->getCode();
$role = $this->roleCommandHandler->handleStore($dataRole);
$role = $this->roleSyncPermissionsCommandHandler->handle($role, $data->getPermissions());
return $role;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($role, __('The group was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $role)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$role = DB::transaction(function () use ($data, $role) {
$dataRole = $this->getDataRole($data);
$role = $this->roleCommandHandler->handleUpdate($role, $dataRole);
$role = $this->roleSyncPermissionsCommandHandler->handle($role, $data->getPermissions());
return $role;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($role, __('The group was successfully updated'));
}
public function destroy(int $id, User $user): ServiceResultError|ServiceResultSuccess
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $role)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($role) {
$this->roleCommandHandler->handleDestroy($role);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The group has been deleted'));
}
private function getDataRole(StoreUpdate $data): array
{
return [
'name' => $data->getName(),
];
}
}

View File

@@ -0,0 +1,216 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Builder\User as UserBuilderDto;
use App\Dto\Service\Admin\User\StoreUpdate;
use App\Dto\Service\User\UpdatePassword;
use App\Helpers\Helpers;
use App\Models\User;
use App\Dto\QuerySettingsDto;
use App\Repositories\RoleRepository;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Service;
use App\Services\User\UserCommandHandler;
use Illuminate\Support\Facades\DB;
final class UserService extends Service
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly UserCommandHandler $userCommandHandler,
private readonly RoleRepository $roleRepository
) { }
public function index(UserBuilderDto $userBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
$users = $this->userRepository->getUsers(
$userBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'users' => $users,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'user' => new User(),
'roles' => $this->roleRepository->getRolesForSelect(),
'userRoles' => [],
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
$userRoles = $modelUser->roles()->withTrashed()->pluck('id')->toArray();
return $this->result([
'user' => $modelUser,
'roles' => $this->roleRepository->getRolesForSelect($userRoles),
'userRoles' => $userRoles,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
if ($this->userRepository->isExistsUsername($data->getUsername())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.username')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.username')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword());
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail(), $modelUser->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
if ($this->userRepository->isExistsUsername($data->getUsername(), $modelUser->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.username')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.username')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data, $modelUser) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleUpdate($modelUser, $dataUser);
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully updated'));
}
public function updatePassword(int $id, UpdatePassword $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword());
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->resultStoreUpdateModel($modelUser, __('The password has been changed'));
}
public function destroy(int $id, User $user): ServiceResultError | ServiceResultSuccess
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($modelUser) {
$this->userCommandHandler->handleDestroy($modelUser);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The user has been deleted'));
}
private function getDataUser(StoreUpdate $data): array
{
return [
'name' => $data->getName(),
'email' => $data->getEmail(),
'username' => $data->getUsername(),
'is_active' => $data->isActive(),
];
}
}

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Dto\Service\Authorization;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
final class AuthService extends Service
{
public function __construct(
private readonly UserRepository $userRepository
) { }
public function authorization(Authorization $authorization): ServiceResultError | ServiceResultSuccess
{
$user = $this->userRepository->getUserByEmail($authorization->getEmail());
if (is_null($user)) {
return $this->errUnauthorized(__('auth.failed'));
}
if (Hash::check($authorization->getPassword(), $user->password) !== true) {
return $this->errUnauthorized(__('auth.password'));
}
try {
Auth::login($user, $authorization->getRemember());
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('auth.success'));
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Dto\Builder\Role as RoleBuilderDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, RoleBuilderDto $roleBuilderDto): Relation | Builder
{
return $query;
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Models\Role;
final readonly class RoleCommandHandler
{
public function handleStore(array $data): Role
{
return Role::create($data);
}
public function handleUpdate(Role $role, array $data): Role
{
$role->update($data);
$role->touch();
return $role;
}
public function handleDestroy(Role $role): void
{
$role->delete();
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Enums\Permission;
use App\Exceptions\Services\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Models\Role;
use App\Models\RolePermission;
final readonly class RoleSyncPermissionsCommandHandler
{
public function handle(Role $role, array $dataPermissions): Role
{
$rolePermissions = $role->permissions->pluck('id', 'permission')->toArray();
$data = $this->getInsertDeleteData($role->id, $rolePermissions, $dataPermissions);
if (!empty($data['insert'])) {
RolePermission::query()->insert($data['insert']);
}
if (!empty($data['delete'])) {
RolePermission::query()->whereIn('id', $data['delete'])->delete();
}
return $role;
}
private function getInsertDeleteData(int $roleId, array $rolePermissions, array $permissionsData): array
{
$data = [
'insert' => [],
'delete' => []
];
$permissions = Permission::toArrayListCodes();
foreach ($permissionsData as $permission) {
if (array_search($permission, $permissions) === false) {
throw new RoleSyncPermissionsCommandHandlerException('Таких разрешений в системе нет: ' . $permission);
}
if (isset($rolePermissions[$permission])) {
unset($rolePermissions[$permission]);
continue;
}
$data['insert'][] = [
'role_id' => $roleId,
'permission' => $permission,
];
}
$data['delete'] = array_values($rolePermissions);
return $data;
}
}

View File

@@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Services\Search;
use App\Contracts\Search;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class CreateSearchInstanceCommand
{
public function __construct(
private string $abstract
) { }
public function execute(Relation | Builder $query): Search
{
return new $this->abstract($query);
}
}

View File

@@ -0,0 +1,79 @@
<?php declare(strict_types=1);
namespace App\Services\Search;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use App\Contracts\Search as SearchContract;
final readonly class Search implements SearchContract
{
public function __construct(
private Relation|Builder $query
) { }
public function all(): Collection
{
return $this->query->get();
}
public function get(int $limit): Collection
{
return $this->query->limit($limit)->get();
}
public function pagination(int $limit, int $page = 1): LengthAwarePaginator
{
if ($page > 100) {
return $this->paginationPerfomance($limit, $page);
}
return $this->query->paginate($limit, page: $page)->withQueryString();
}
public function cursorPaginate(int $limit): CursorPaginator
{
return $this->query->cursorPaginate($limit);
}
private function paginationPerfomance(int $limit, int $page = 1): LengthAwarePaginator
{
$total = $this->query->clone()->count();
$options = [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
];
$result = collect();
if ($total > 0) {
$result = $this->subQuery($limit, $page);
}
$pagination = Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $result,
'total' => $total,
'perPage' => $limit,
'currentPage' => $page,
'options' => $options
]);
return $pagination->withQueryString();
}
private function subQuery(int $limit, int $page): Collection
{
$table = $this->query->getModel()->getTable();
return $this->query->getModel()::query()
->select($table.'.*')
->with($this->query->getEagerLoads())
->from(
clone $this->query->select('id')->forPage($page, $limit),
'q'
)->join($table.' as '.$table, $table.'.id', '=', 'q.id')
->get();
}
}

View File

@@ -0,0 +1,74 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Models\Project;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
use App\ServiceResults\StoreUpdateResult;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Response;
abstract class Service
{
final protected function errValidate(string $message, array $errors = []): ServiceResultError
{
return $this->error(Response::HTTP_UNPROCESSABLE_ENTITY, $message, $errors);
}
final protected function errFobidden(string $message): ServiceResultError
{
return $this->error(Response::HTTP_FORBIDDEN, $message);
}
final protected function errNotFound(string $message): ServiceResultError
{
return $this->error(Response::HTTP_NOT_FOUND, $message);
}
final protected function errService(string $message): ServiceResultError
{
return $this->error(Response::HTTP_INTERNAL_SERVER_ERROR, $message);
}
final protected function notAcceptable(string $message): ServiceResultError
{
return $this->error(Response::HTTP_NOT_ACCEPTABLE, $message);
}
final protected function errUnauthorized(string $message): ServiceResultError
{
return $this->error(Response::HTTP_UNAUTHORIZED, $message);
}
final protected function ok(string $message = 'OK'): ServiceResultSuccess
{
return new ServiceResultSuccess($message);
}
final protected function resultStoreUpdateModel(Model $model, string $message = 'OK'): StoreUpdateResult
{
return new StoreUpdateResult($model, $message);
}
final protected function result(array $data = []): ServiceResultArray
{
return new ServiceResultArray(data: $data);
}
final protected function resultSitePage(Project $project, WebsiteTranslations $websiteTranslations, array $data, bool $isTranslation): PagePossibleWithoutTranslation
{
return new PagePossibleWithoutTranslation($project, $websiteTranslations, $data, $isTranslation);
}
final protected function error(int $code, string $message, array $errors = []): ServiceResultError
{
return new ServiceResultError(
message: $message,
errors: $errors,
code: $code
);
}
}

View File

@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace App\Services\Site;
use App\Dto\Service\Site\Profile\Update;
use App\Dto\Service\Site\Profile\UpdateSettings;
use App\Dto\Service\User\UpdatePassword;
use App\Models\User;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\Service;
use App\Services\User\UserCommandHandler;
final class ProfileService extends Service
{
public function __construct(
private readonly UserCommandHandler $userCommandHandler
) { }
public function update(Update $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$data = [
'name' => $update->getName()
];
$this->userCommandHandler->handleUpdate($user, $data);
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('Profile saved successfully'));
}
public function updatePassword(UpdatePassword $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$this->userCommandHandler->handleUpdatePassword($user, $update->getPassword());
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('The password has been changed'));
}
public function updateSettings(UpdateSettings $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$data = [
'lang' => $update->getLang(),
'timezone' => $update->getTimezone(),
];
$this->userCommandHandler->handleUpdate($user, $data);
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('The settings have been saved'));
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Services\User;
use App\Dto\Builder\User as UserBuilderDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, UserBuilderDto $userBuilderDto): Relation | Builder
{
return $query;
}
}

View File

@@ -0,0 +1,78 @@
<?php declare(strict_types=1);
namespace App\Services\User;
use App\Dto\Service\User\ManyRoleDto;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
final readonly class UserCommandHandler
{
public function handleStore(array $data, int|string $password): User
{
$data['email'] = Str::lower($data['email']);
$data['username'] = Str::lower($data['username']);
$data['password'] = $this->hashPassword($password);
$user = User::create($data);
return $user;
}
public function handleUpdate(User $user, array $data): User
{
if (isset($data['email'])) {
$data['email'] = Str::lower($data['email']);
}
if (isset($data['username'])) {
$data['username'] = Str::lower($data['username']);
}
if (isset($data['password'])) {
unset($data['password']);
}
$user->update($data);
$user->touch();
return $user;
}
public function handleConfirmationByEmail(User $user): void
{
$user->update(['email_verified_at' => new Carbon('NOW')]);
}
public function handleUpdatePassword(User $user, int|string $password): void
{
$user->update(['password' => $this->hashPassword($password)]);
}
public function handleSyncRoles(User $user, ManyRoleDto $roles): void
{
$user->roles()->sync($roles->toArray());
}
public function attachRole(User $user, Role $role): void
{
$user->roles()->attach($role);
}
public function detachRole(User $user, Role $role): void
{
$user->roles()->detach($user);
}
private function hashPassword(int|string $password): string
{
return Hash::make($password);
}
public function handleDestroy(User $user): void
{
$user->delete();
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\View\Components\Layout;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Admin extends Component
{
/**
* @inheritDoc
*/
public function render(): View
{
return view('layout.admin');
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\View\Components\Layout;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Site extends Component
{
/**
* @inheritDoc
*/
public function render(): View
{
return view('layout.site');
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Checkbox extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $checkboxValue,
private readonly ?string $userValue = '',
private readonly ?string $notCheckedValue = null
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getCheckboxValue(): string
{
return (string) $this->checkboxValue;
}
private function getUserValue(): string
{
return (string) old($this->getRequestName(), $this->userValue);
}
private function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.site.forms.checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'checkboxValue' => $this->getCheckboxValue(),
'userValue' => $this->getUserValue(),
'notCheckedValue' => $this->getNotCheckedValue(),
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use App\Helpers\Helpers;
use Illuminate\View\Component;
use Illuminate\View\View;
abstract class Form extends Component
{
private ?string $requestName = null;
abstract protected function getName(): string;
abstract public function render(): View;
protected function getRequestName(): string
{
if (!is_null($this->requestName)) {
return $this->requestName;
}
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
return $this->requestName;
}
}

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Input extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $type = 'text',
private readonly ?string $value = '',
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getType(): string
{
return $this->type;
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.site.forms.input', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'type' => $this->getType(),
'value' => $this->getValue(),
]);
}
}

View File

@@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Select extends Form
{
/**
* @param string $title
* @param string $name
* @param array $list = [ [key => value], ... ]
* @param null|string $value
*/
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly array $list,
private readonly ?string $value = ''
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
private function getList(): array
{
return $this->list;
}
public function render(): View
{
return view('components.site.forms.select', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'list' => $this->getList(),
'value' => $this->getValue()
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Checkbox extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $checkboxValue,
private readonly ?string $userValue = '',
private readonly ?string $notCheckedValue = null
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getCheckboxValue(): string
{
return (string) $this->checkboxValue;
}
private function getUserValue(): string
{
return (string) old($this->getRequestName(), $this->userValue);
}
private function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'checkboxValue' => $this->getCheckboxValue(),
'userValue' => $this->getUserValue(),
'notCheckedValue' => $this->getNotCheckedValue(),
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use App\Helpers\Helpers;
use Illuminate\View\Component;
use Illuminate\View\View;
abstract class Form extends Component
{
private ?string $requestName = null;
abstract protected function getName(): string;
abstract public function render(): View;
protected function getRequestName(): string
{
if (!is_null($this->requestName)) {
return $this->requestName;
}
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
return $this->requestName;
}
}

View File

@@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Input extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $type = 'text',
private readonly ?string $value = '',
private readonly ?string $example = null,
private readonly ?string $allowedCharacters = null,
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getType(): string
{
return $this->type;
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
private function getExample(): ?string
{
return $this->example;
}
public function getAllowedCharacters(): ?string
{
return $this->allowedCharacters;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.input', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'type' => $this->getType(),
'value' => $this->getValue(),
'example' => $this->getExample(),
'allowedCharacters' => $this->getAllowedCharacters(),
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class MultiCheckbox extends Form
{
/**
* @param string $title
* @param string $name
* @param array $list = [ [key => value], ... ]
* @param array $value
*/
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly array $list,
private readonly array $value = []
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): array
{
return old($this->getRequestName(), $this->value);
}
private function getList(): array
{
return $this->list;
}
public function render(): View
{
return view('components.volt.forms.multi_checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'list' => $this->getList(),
'value' => $this->getValue()
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use App\Enums\Permission;
use App\Models\Role;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class PermissionsForRole extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly Role $role,
private readonly array $value
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): Collection
{
$value = old($this->getRequestName(), $this->value);
return collect($value);
}
private function getRole(): Role
{
return $this->role;
}
public function render(): View
{
return view('components.volt.forms.permissions_for_role', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'permissions' => Permission::cases(),
'role' => $this->getRole(),
'value' => $this->getValue(),
]);
}
}