Moved from "app/src" to "app/application".

Otherwise phpstorm doesn't understand the paths correctly. He thinks that this is not a complete application, but a package. And when creating a class, the namespace indicates “app” with a small letter, but should be “App”.
This commit is contained in:
2024-04-11 19:52:37 +05:00
parent e41f63f94f
commit b7d0a2453e
314 changed files with 5 additions and 5 deletions

View File

@@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Dto\User\ManyRoleDto;
use App\Enums\SystemRole;
use App\Repositories\RoleRepository;
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} {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.');
}
$user = 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);
return $user;
});
} catch (\Throwable $e) {
$this->errorMessageAndStop($e->getMessage());
}
$this->info('The command was successful!');
}
private function getData(): Validator
{
return ValidatorFacade::make([
'email' => $this->argument('email'),
'password' => $this->argument('password'),
], [
'email' => ['required', 'email', 'unique:users,email'],
'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,15 @@
<?php declare(strict_types=1);
namespace App\Contracts\Models;
use App\Enums\StorageType;
use App\Models\Storage as StorageModel;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Collection;
interface Storage
{
public function storage(): MorphMany;
public function getStorageOne(StorageType $type): ?StorageModel;
public function getStorageMany(StorageType $type): Collection;
}

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,10 @@
<?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,11 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface StorageType
{
public function getTitle(): string;
public function getAcceptMimes(): array;
public function getFolderName(): string;
public static function cases(): array;
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Audio extends StorageTypeContract
{
public function isAudio(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Image extends StorageTypeContract
{
public function isImage(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Video extends StorageTypeContract
{
public function isVideo(): bool;
}

View File

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

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,23 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Language;
use App\Dto\Service\Dto;
final readonly class NewLanguage extends Dto
{
public function __construct(
private string $name,
private int $index,
) { }
public function getName(): string
{
return $this->name;
}
public function getIndex(): int
{
return $this->index;
}
}

View File

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

View File

@@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
use App\Dto\Service\Dto;
final readonly class Language extends Dto
{
public function __construct(
private string $title,
private string $code,
private int $sort,
private bool $isDefault,
private ?int $id,
) { }
public function getTitle(): string
{
return $this->title;
}
public function getCode(): string
{
return $this->code;
}
public function getSort(): int
{
return $this->sort;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function getId(): ?int
{
return $this->id;
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
final class Languages
{
private $languages = [];
public function addLanguage(Language $language): void
{
$this->languages[] = $language;
}
public function getLanguages(): array
{
return $this->languages;
}
}

View File

@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
use App\Dto\Service\Dto;
use App\Dto\Service\Storage\Storages;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $code,
private bool $isPublic,
private Languages $languages,
private Storages $storages,
private ?string $httpHost,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): string
{
return $this->code;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function getLanguages(): Languages
{
return $this->languages;
}
public function getHttpHost(): ?string
{
return $this->httpHost;
}
public function getStorages(): Storages
{
return $this->storages;
}
}

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,36 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\User;
use App\Dto\Service\Dto;
use App\Dto\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
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;
}
}

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\Private\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\Private\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,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Contracts\StorageType;
use App\Dto\Service\Dto;
use App\Exceptions\Dto\Storage\FileException;
final readonly class File extends Dto
{
public function __construct(
private int $id,
private StorageType $storageType,
) {
if ($this->id < 1) {
throw new FileException('ID cannot be equal to or less than zero: ' . $this->id);
}
}
public function getId(): int
{
return $this->id;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Dto\Service\Dto;
use App\Enums\StorageType;
use app\Exceptions\Dto\Storage\StorageException;
final readonly class Storage extends Dto
{
private ?File $file;
private bool $isDelete;
public function __construct(
array $data,
private StorageType $storageType,
private bool $isMany,
) {
$file = null;
if (isset($data['file'])) {
if ((int) $data['file'] <= 0) {
throw new StorageException('ID cannot be equal to or less than zero: ' . $data['file']);
}
$file = new File(
id: (int) $data['file'],
storageType: $this->storageType,
);
}
$this->file = $file;
$this->isDelete = !empty($data['delete']);
}
public function getFile(): ?File
{
return $this->file;
}
public function isDelete(): bool
{
return $this->isDelete;
}
public function isFile(): bool
{
return \is_null($this->file) === false;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
public function isMany(): bool
{
return $this->isMany;
}
}

View File

@@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Enums\StorageType;
use app\Exceptions\Dto\Storage\StoragesException;
final class Storages
{
private array $storages = [];
public function add(array $data, StorageType $type): void
{
if (isset($this->storages[$type->value])) {
throw new StoragesException('You cannot attach two files of the same type!');
}
$this->storages[$type->value] = new Storage(data: $data, storageType: $type, isMany: false);
}
public function addMany(array $data, StorageType $type): void
{
if (!isset($this->storages[$type->value])) {
$this->storages[$type->value] = [];
}
foreach ($data as $storageData) {
$this->storages[$type->value][] = new Storage(data: $storageData, storageType: $type, isMany: true);
}
}
public function toArray(): array
{
return $this->storages;
}
public function getAllStorageIds(): array
{
$ids = [];
foreach ($this->storages as $storage) {
/** @var Storage $storage */
if (!is_array($storage)) {
if ($storage->isFile() && !$storage->isDelete()) {
$ids[] = $storage->getFile()->getId();
}
continue;
}
foreach ($storage as $storageOne) {
/** @var Storage $storageOne */
if ($storageOne->isFile() && !$storageOne->isDelete()) {
$ids[] = $storageOne->getFile()->getId();
}
}
}
return $ids;
}
public function getAllStorages(): array
{
$storages = [];
foreach ($this->storages as $storage) {
if (!is_array($storage)) {
$storages[] = $storage;
continue;
}
foreach ($storage as $storageOne) {
$storages[] = $storageOne;
}
}
return $storages;
}
}

View File

@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Dto\Service\Dto;
use App\Enums\Morph;
use App\Enums\StorageType;
use Illuminate\Http\UploadedFile;
final readonly class Upload extends Dto
{
public function __construct(
private UploadedFile $file,
private StorageType $storageType,
private Morph $morph,
) { }
public function getFile(): UploadedFile
{
return $this->file;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
public function getMorph(): Morph
{
return $this->morph;
}
}

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,32 @@
<?php declare(strict_types=1);
namespace App\Dto\User;
use App\Exceptions\Dto\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,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,31 @@
<?php declare(strict_types=1);
namespace App\Enums;
use App\Models\Project;
enum Morph: int
{
case Project = 1;
public function getPathModel(): string
{
return match ($this) {
self::Project => Project::class,
};
}
public function getFolderName(): string
{
return $this->name;
}
public static function map(): array
{
$map = [];
foreach (self::cases() as $item) {
$map[$item->value] = $item->getPathModel();
}
return $map;
}
}

View File

@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case AdminPanel = 'allow-admin-panel';
case Role = 'role';
case User = 'user';
case Project = 'project';
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,53 @@
<?php declare(strict_types=1);
namespace App\Enums;
use App\Contracts\StorageType\Audio;
use App\Contracts\StorageType\Image;
use App\Contracts\StorageType\Video;
enum StorageType: int implements Image, Video, Audio
{
case Logo = 1;
public function getTitle(): string
{
return match ($this) {
self::Logo => __('validation.attributes.logo'),
};
}
public function getAcceptMimes(): array
{
return match ($this) {
self::Logo => ['jpeg', 'jpg', 'png'],
};
}
public function isImage(): bool
{
return match ($this) {
self::Logo => true,
default => false
};
}
public function isVideo(): bool
{
return match ($this->name) {
default => false
};
}
public function isAudio(): bool
{
return match ($this->name) {
default => false
};
}
public function getFolderName(): string
{
return $this->name;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace app\Exceptions\Dto\Storage;
final class StorageException extends \Exception
{
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace app\Exceptions\Dto\Storage;
final class StoragesException extends \Exception
{
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Helpers;
use App\Models\User;
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();
});
}
public static function getUserTimeZone() {
return auth()->user()?->timezone ?? config('app.user_timezone');
}
/**
* $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,25 @@
<?php declare(strict_types = 1);
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\Languages\NewLanguageRequest;
use App\Http\Resources\Admin\Languages\NewLanguage;
use App\Services\Admin\LanguageService;
use Illuminate\Http\JsonResponse;
final class LanguagesController extends Controller
{
public function __construct(
private readonly LanguageService $languageService,
) { }
public function newLanguage(NewLanguageRequest $request): JsonResponse
{
$result = $this->languageService->newLanguage($request->getDto());
if ($result->isError()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new NewLanguage($result));
}
}

View File

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

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: ['morph']
);
$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,47 @@
<?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');
}
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('admin.home');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
use Illuminate\Http\Response;
abstract class Controller
{
final protected function errors(ServiceResultErrorContract $result): never
{
if ($result->getCode() === Response::HTTP_UNPROCESSABLE_ENTITY) {
redirect()->back()->withInput()->withErrors($result->getErrors());
exit;
}
abort($result->getCode(), $result->getMessage());
}
}

View File

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

View File

@@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Enums\Lang;
use App\Helpers\Helpers;
use App\Http\Requests\Private\Profile\UpdatePasswordRequest;
use App\Http\Requests\Private\Profile\UpdateRequest;
use App\Http\Requests\Private\Profile\UpdateSettingsRequest;
use App\Services\Private\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('private/profile/profile', [
'user' => $request->user()
]);
}
public function settings(Request $request): View
{
return view('private/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,10 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Storage;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
}

View File

@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace App\Http\Controllers\Storage;
use App\Http\Requests\Storage\ImageRequest;
use App\Http\Resources\Storage\Upload;
use App\Services\Storage\ImageService;
final class ImagesController extends Controller
{
public function __construct(
private readonly ImageService $imageService,
) { }
public function uploadAndResize(ImageRequest $request)
{
$data = $request->getDto();
$user = $request->user();
$result = $this->imageService->uploadAndResize($data, $user);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new Upload($result->getStorage()));
}
}

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;
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,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,29 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Languages;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Language\NewLanguage;
use Illuminate\Foundation\Http\FormRequest;
class NewLanguageRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'index' => ['required', 'numeric'],
'name' => ['required', 'string'],
];
}
public function getDto(): NewLanguage
{
return new NewLanguage(
name: $this->input('name'),
index: $this->input('index'),
);
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\Project;
use App\Dto\Service\Admin\Project\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.projects.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
projectBuilderDto: new Project(),
page: (int) $this->input('page', 1)
);
}
}

View File

@@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\Language;
use App\Dto\Service\Admin\Project\Languages;
use App\Dto\Service\Admin\Project\StoreUpdate;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use App\Rules\HttpHost;
use Illuminate\Foundation\Http\FormRequest;
class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'],
'http_host' => ['nullable', 'string', 'max:255', new HttpHost()],
'is_public' => ['required', 'boolean'],
'logo.file' => ['nullable', 'numeric', 'min:1'],
'logo.delete' => ['nullable', 'boolean'],
'languages.items.*.id' => ['nullable', 'numeric'],
'languages.items.*.title' => ['required', 'string', 'max:255'],
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-zA-Z_]+$/i'],
'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'],
'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) {
$languages = $this->input('languages.items', []);
if (!isset($languages[$value])) {
$this->validator->getMessageBag()->add('languages.default', __('validation.required', ['attribute' => __('validation.attributes.language-default')]));
}
}],
'languages.items' => ['required', 'array', function (string $attribute, mixed $value, \Closure $fail) {
$values = [];
foreach ($value as $index => $item) {
if (!isset($item['code'])) {
continue;
}
if (in_array($item['code'], $values)) {
$this->validator->getMessageBag()->add('languages.items.' . $index . '.code', __('validation.unique', ['attribute' => __('validation.attributes.language-code')]));
}
$values[] = $item['code'];
}
}],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
code: $this->input('code'),
isPublic: (bool) $this->input('is_public', false),
languages: $this->languages(),
storages: $this->storages(),
httpHost: $this->input('http_host', null),
);
}
private function languages(): Languages
{
$languages = new Languages();
$default = $this->input('languages.default', null);
if ($default !== null) {
$default = (int) $default;
}
foreach ($this->input('languages.items', []) as $index => $lang) {
$languageId = $lang['id'] ?? null;
if ($languageId !== null) {
$languageId = (int) $languageId;
}
$language = new Language(
title: $lang['title'],
code: $lang['code'],
sort: (int) $lang['sort'],
isDefault: ($default === $index),
id: $languageId,
);
$languages->addLanguage($language);
}
return $languages;
}
private function storages(): Storages
{
$storages = new Storages();
$logo = $this->get('logo', []);
$storages->add($logo, StorageType::Logo);
return $storages;
}
}

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,42 @@
<?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\User\ManyRoleDto;
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'],
'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'),
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,32 @@
<?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
{
return [
'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'],
'captcha-verified' => ['captcha'],
'remember' => ['nullable', 'boolean'],
];
}
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\Private\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\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Private\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\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Private\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,40 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Storage;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Storage\Upload;
use App\Enums\Morph;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
final class ImageRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$mimes = '';
if ($this->has('storage_type')) {
$storageType = StorageType::tryFrom((int) $this->input('storage_type')) ?? [];
$mimes = implode(',', $storageType->getAcceptMimes());
}
return [
'file' => ['required', 'file', 'mimes:' . $mimes],
'storage_type' => ['required', 'numeric', new Enum(StorageType::class)],
'morph' => ['required', 'numeric', new Enum(Morph::class)],
];
}
public function getDto(): Upload
{
return new Upload(
file: $this->file('file'),
storageType: StorageType::from((int) $this->input('storage_type')),
morph: Morph::from((int) $this->input('morph')),
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Resources\Admin\Languages;
use App\ServiceResults\Admin\LanguageService\NewLanguageResult;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class NewLanguage extends JsonResource
{
/**
* @var NewLanguageResult
*/
public $resource;
public function toArray(Request $request): array
{
return [
'html' => view('components.volt.forms.languages.language', [
'index' => $this->resource->getIndex(),
'name' => $this->resource->getName(),
'lang' => $this->resource->getLanguage()->toArray(),
])->render(),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Http\Resources\Storage;
use App\Models\Storage;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class Upload extends JsonResource
{
/**
* @var Storage
*/
public $resource;
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'url' => $this->resource->url,
];
}
}

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Contracts\Models\Storage as StorageContract;
final class Project extends Model implements StorageContract
{
use HasFactory, SoftDeletes, StorageTrait;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'code',
'http_host',
'is_public'
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_public' => 'boolean',
];
}
public function roles(): MorphMany
{
return $this->morphMany(Role::class, 'morph');
}
public function languages(): HasMany
{
return $this->hasMany(ProjectLanguage::class);
}
}

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Models\Scopes\SortScope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([SortScope::class])]
final class ProjectLanguage extends Model
{
use HasFactory, SoftDeletes;
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'is_default' => false,
'sort' => 100,
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'code',
'is_default',
'sort',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_default' => 'boolean',
'sort' => 'integer',
];
}
}

View File

@@ -0,0 +1,75 @@
<?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\Relations\MorphTo;
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');
}
protected function nameWithMorph(): Attribute
{
return Attribute::make(
get: function () {
$name = $this->name;
if ($this->morph_type) {
$name .= ' (' . __('admin-sections.Projects') . ': ' . $this->morph?->name . ')';
}
return $name;
},
)->shouldCache();
}
public function morph(): MorphTo
{
return $this->morphTo('morph');
}
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 () => ( \is_null($this->morphable_type) && \is_null($this->morphable_id) && $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,18 @@
<?php declare(strict_types=1);
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
final class SortScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->orderBy('sort', 'asc');
}
}

View File

@@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\StorageType;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage as StorageSupport;
class Storage extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'storage';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'data',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'data' => 'array',
'type' => StorageType::class,
];
}
protected function dataImages(): Attribute
{
return Attribute::make(
get: fn () => $this->data['images'] ?? [],
);
}
protected function url(): Attribute
{
return Attribute::make(
get: function () {
if (\is_null($this->file)) {
return '';
}
return StorageSupport::disk(config('storage.disk'))->url($this->file) . '?time=' . $this->updated_at->getTimestamp();
},
);
}
protected function path(): Attribute
{
return Attribute::make(
get: function () {
if (\is_null($this->file)) {
return '';
}
return StorageSupport::disk(config('storage.disk'))->path($this->file);
},
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Models\Traits;
use App\Enums\StorageType;
use App\Models\Storage as StorageModel;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait StorageTrait
{
public function storage(): MorphMany
{
return $this->morphMany(StorageModel::class, 'morph');
}
public function getStorageOne(StorageType $type): ?StorageModel
{
return $this->storage->firstWhere('type', $type);
}
public function getStorageMany(StorageType $type): Collection
{
return $this->storage->where('type', $type);
}
}

View File

@@ -0,0 +1,87 @@
<?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\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
final class User extends Authenticatable
{
use HasFactory, Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'nickname',
'timezone',
'lang',
];
/**
* 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,
];
}
public function roles(): belongsToMany
{
return $this->belongsToMany(Role::class);
}
public function hasPermission(string $permission): bool
{
return $this->permissions->search($permission) !== false;
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => $this->roles
->where('code', SystemRole::Admin->value)
->whereNull('morphable_type')
->whereNull('morphable_id')
->isNotEmpty(),
)->shouldCache();
}
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
$roles = $this->roles->modelKeys();
return RolePermission::query()->whereIn('role_id', $roles)->select('permission')->pluck('permission');
},
)->shouldCache();
}
}

View File

@@ -0,0 +1,16 @@
<?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')
|| $user->roles->where('code', SystemRole::AdminProject->value)->isNotEmpty();
}
}

View File

@@ -0,0 +1,20 @@
<?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_admin) {
return true;
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
final readonly class ProjectPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('project.view');
}
public function view(User $user, Project $project): bool
{
return $user->hasPermission('project.view');
}
public function create(User $user): bool
{
return $user->hasPermission('project.create');
}
public function update(User $user, Project $project): bool
{
return $user->hasPermission('project.update');
}
public function delete(User $user, Project $project): bool
{
return $user->hasPermission('project.delete');
}
public function upload(User $user): bool
{
if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) {
return true;
}
return false;
}
}

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,92 @@
<?php declare(strict_types=1);
namespace App\Providers;
use App\Enums\Morph;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use App\Services\Storage\Image\ResizeCommandHandler;
use App\Services\Storage\ImageService;
use App\Services\Storage\StorageCommandHandler;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\URL;
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);
});
$this->app->bind(StorageCommandHandler::class, function () {
return new StorageCommandHandler(disc: (string) config('storage.disk'));
});
$this->app->bind(ImageService::class, function (Application $app) {
return new ImageService(
storageCommandHandler: $app->make(StorageCommandHandler::class),
resizeCommandHandler: $app->make(ResizeCommandHandler::class),
maxImageWidth: (int) config('storage.max_image_width', 4000),
maxImageHeight: (int) config('storage.max_image_height', 4000),
);
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
if (config('app.force_https') === true) {
URL::forceScheme('https');
}
$this->passwordDefaults();
Relation::enforceMorphMap(Morph::map());
$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,49 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Dto\Builder\Project as ProjectBuilderDto;
use App\Models\Project;
use App\Services\Project\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getProjects(ProjectBuilderDto $projectBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: Project::query()->with($with),
projectBuilderDto: $projectBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function getProjectById(int $id): ?Project
{
return Project::query()->where('id', $id)->first();
}
public function getProjectByCode(string $code): ?Project
{
return Project::query()->where('code', $code)->first();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Project::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\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
{
$roles = Role::query()
->with(['morph'])
->when($withExcepts, function (Builder $query, array $withExcepts) {
$query->withTrashed()->whereNull('deleted_at')->orWhereIn('id', $withExcepts);
})->get();
return $roles->pluck('name_with_morph', 'id')->toArray();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Role::query()
->where('code', $code)
->whereNull('morphable_type')
->whereNull('morphable_id')
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\Storage;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Contracts\Search;
final readonly class StorageRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
) { }
public function getStorageById(int $id): ?Storage
{
return Storage::query()->where('id', $id)->first();
}
public function getStoregsByIds(array $ids): Search
{
$query = Storage::query()->whereIn('id', $ids);
return $this->createSearchInstanceCommand->execute($query);
}
}

View File

@@ -0,0 +1,50 @@
<?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();
}
}

View File

@@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
final readonly class HttpHost 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->validateHttpHost($value, $fail);
}
if (is_array($value)) {
foreach ($value as $item) {
$this->validateHttpHost($item, $fail);
}
}
}
private function validateHttpHost(string $value, Closure $fail): void
{
$scheme = explode('://', $value, 2);
if (count($scheme) != 2 || in_array($scheme[0], ['http', 'https'], true) !== true) {
$fail('validation.http_host')->translate();
return;
}
$host = explode(':', $scheme[1], 2);
if (!filter_var($host[0], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
$fail('validation.http_host')->translate();
return;
}
if (isset($host[1]) && (!is_numeric($host[1]) || $host[1] <= 0)) {
$fail('validation.http_host')->translate();
return;
}
}
}

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,30 @@
<?php declare(strict_types = 1);
namespace App\ServiceResults\Admin\LanguageService;
use App\Models\ProjectLanguage;
use App\ServiceResults\ServiceResult;
final class NewLanguageResult extends ServiceResult
{
public function __construct(
private readonly ProjectLanguage $language,
private readonly string $name,
private readonly int $index,
) { }
public function getLanguage(): ProjectLanguage
{
return $this->language;
}
public function getName(): string
{
return $this->name;
}
public function getIndex(): int
{
return $this->index;
}
}

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;
}
}

Some files were not shown because too many files have changed in this diff Show More