Skeleton is ready

This commit is contained in:
2026-01-05 16:33:20 +04:00
commit eeaf43ab5d
89 changed files with 2704 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Command;
use Illuminate\Console\Command;
class DoSomething extends Command
{
protected $signature = 'example:do-something';
protected $description = 'Do something example';
public function handle(): int
{
return 0;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Dto;
use Diffhead\PHP\Dto\Dto;
use Diffhead\PHP\Dto\Property;
/**
* @property \Diffhead\PHP\Dto\Property<int> $page
* @property \Diffhead\PHP\Dto\Property<int> $perPage
*/
class SearchUsers extends Dto
{
protected Property $page;
protected Property $perPage;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Http\Controller;
use App\Feature\Example\Dto\SearchUsers;
use App\Feature\Example\Http\Request\Index;
use App\Feature\Example\Http\Resource\Users;
use App\Feature\Example\Service\UsersRepository;
class User
{
public function __construct(
private UsersRepository $users,
) {}
public function index(Index $request): Users
{
$dto = SearchUsers::fromArray($request->validated());
return Users::make($this->users->search($dto));
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Http\Request;
use Illuminate\Foundation\Http\FormRequest;
/**
* @property int|null $page
* @property int|null $per_page
*/
class Index extends FormRequest
{
public function rules(): array
{
return [
'page' => ['sometimes', 'integer', 'min:1'],
'per_page' => ['sometimes', 'integer', 'min:1', 'max:200'],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Http\Resource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \App\Models\User\User $resource
*/
class User extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->resource->getKey(),
'login' => $this->resource->login,
'created_at' => $this->resource->created_at,
'updated_at' => $this->resource->updated_at,
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Http\Resource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
/**
* @property \Illuminate\Pagination\LengthAwarePaginator<\App\Models\User\User> $collection
*/
class Users extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => User::collection($this->collection)
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Listener;
use App\Shared\Event\User\Created;
use Illuminate\Contracts\Queue\ShouldQueue;
class DoSomethingOnEvent implements ShouldQueue
{
public function handle(Created $event): void
{
}
public function viaConnection(): string
{
return 'sync';
}
public function viaQueue(): string
{
return 'default';
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example;
use App\Feature\Example\Command\DoSomething;
use App\Feature\Example\Http\Controller\User;
use App\Feature\Example\Listener\DoSomethingOnEvent;
use App\Kernel\Feature\HasCommandsList;
use App\Kernel\Feature\HasEventListeners;
use App\Shared\Event\User\Created as UserCreated;
use Illuminate\Routing\Route;
use Illuminate\Support\ServiceProvider;
class Provider extends ServiceProvider
{
use HasCommandsList, HasEventListeners;
/**
* @var array<string,class-string>
*/
public array $bindings = [
/** Bindings section */
];
/**
* @var array<int,class-string>
*/
private array $commandsList = [
DoSomething::class,
];
private array $eventListeners = [
UserCreated::class => [
DoSomethingOnEvent::class,
],
];
public function register(): void
{
/** Register something here */
}
public function boot(): void
{
$this->registerCommands();
$this->registerListeners();
$this->registerRoutes();
}
private function registerRoutes(): void
{
Route::middleware('auth:sanctum')
->controller(User::class)
->group(function (): void {
Route::get('/users', 'index')->name('users.index');
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Feature\Example\Service;
use App\Feature\Example\Dto\SearchUsers;
use App\Models\User\User;
use App\Shared\Service\User\SearchById;
use Illuminate\Pagination\LengthAwarePaginator;
class UsersRepository extends SearchById
{
/**
* @param \App\Feature\Example\Dto\SearchUsers $dto
*
* @return \Illuminate\Pagination\LengthAwarePaginator<\App\Models\User\User>
*/
public function search(SearchUsers $dto): LengthAwarePaginator
{
return User::query()->paginate(
page: $dto->page->exists() ? $dto->page->value() : 1,
perPage: $dto->perPage->exists() ? $dto->perPage->value() : 20,
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Kernel\DateTime;
enum Format: string
{
case CutISO8601DateTime = 'Y-m-d H:i:s';
case CutISO8601Date = 'Y-m-d';
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Kernel\DateTime;
use DateTimeInterface;
class Util
{
public static function toString(
DateTimeInterface $datetime,
Format $format
): string{
return $datetime->format($format->value);
}
}

12
app/Kernel/Db/Ilike.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Db;
enum Ilike: string
{
case StartsWith = '%s%%%%';
case EndsWith = '%%%s';
case Contains = '%%%%%s%%%%';
}

16
app/Kernel/Db/Query.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Db;
class Query
{
public static function ilike(
int|float|string $value,
string $modifier = '%s',
Ilike $mode = Ilike::Contains
): string {
return sprintf(sprintf($mode->value, $modifier), $value);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Event;
enum Event: string
{
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Feature;
trait HasCommandsList
{
public function registerCommands(): void
{
if (isset($this->commandsList) && method_exists($this, 'commands')) {
$this->commands($this->commandsList);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Feature;
use Illuminate\Support\Facades\Event;
trait HasEventListeners
{
public function registerListeners(): void
{
if (isset($this->eventListeners)) {
/**
* @var string $event
* @var array<int,string> $listeners
*/
foreach ($this->eventListeners as $event => $listeners) {
foreach ($listeners as $listener) {
Event::listen($event, $listener);
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Http;
enum ContentType: string
{
case ApplicationJson = 'application/json';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Http;
enum Header: string
{
case Accept = 'Accept';
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Http\Middleware;
use App\Kernel\Http\ContentType;
use App\Kernel\Http\Header;
use App\Kernel\Object\Enum;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
class CheckAcceptHeader
{
public function handle(Request $request, Closure $next): Response
{
$headerFound = false;
if ($this->acceptApplicationJson($request)) {
$headerFound = true;
}
if ($request->isMethod('OPTIONS') || $headerFound) {
return $next($request);
}
throw new NotAcceptableHttpException('Valid accept header not found');
}
private function acceptApplicationJson(Request $request): bool
{
$passedHeader = $request->headers->get(Header::Accept->value);
$expectedHeader = Enum::value(ContentType::ApplicationJson);
return $passedHeader === $expectedHeader;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Http;
use App\Kernel\String\Regex;
class Validation
{
public static function uuid7(): string
{
return sprintf('regex:/%s/', Regex::Uuid7->value);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
use BackedEnum;
class Enum
{
/**
* Extract values from an array of BackedEnum instances.
*
* @param array<int,BackedEnum> $enums
*
* @return array<int,int|string|float>
*/
public static function values(array $enums): array
{
return array_map(fn (BackedEnum $enum) => static::value($enum), $enums);
}
public static function value(BackedEnum $enum): int|string|float
{
return $enum->value;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
trait HasHiddenAttributes
{
public function isHidden(string $key): bool
{
if (property_exists($this, 'hidden') && is_array($this->hidden)) {
return in_array($key, $this->hidden, true);
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
interface HasPermissions
{
public function permissions(): array;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
interface HasRoles
{
public function roles(): array;
}

View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
interface HasRolesWithPermissions extends HasRoles, HasPermissions {}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Object;
interface HasUuidAsIdentifier
{
public function id(): string;
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Queue;
enum Queue: string
{
}

11
app/Kernel/Role/Role.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Kernel\Role;
enum Role: string
{
case Admin = 'admin';
case User = 'user';
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Kernel\String;
class Hash
{
public static function xxh128(string $value): string
{
return hash('xxh128', $value);
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Kernel\String;
enum Regex: string
{
case Uuid7 = '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
case Numeric = '^[0-9]+$';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Kernel\String;
enum String: string
{
case Empty = '';
}

57
app/Models/User/User.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models\User;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
/**
* @property string $id
* @property string $login
* @property string $password
* @property bool $is_active
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Carbon\Carbon|null $deleted_at
* @property \Illuminate\Database\Eloquent\Collection<int,\Spatie\Permission\Models\Role> $roles
* @property \Illuminate\Database\Eloquent\Collection<int,\Spatie\Permission\Models\Permission> $permissions
*
* @method ?string getKey()
*
* @method static \App\Models\User\User findOrFail(string $id)
*/
class User extends Authenticatable
{
use HasApiTokens, HasUuids, HasRoles, SoftDeletes;
/**
* @var array<int,string>
*/
protected $fillable = [
'login',
'password',
'is_active',
];
/**
* @var array<int,string>
*/
protected $hidden = [
'password',
'laravel_through_key',
];
/**
* @return array<string,string>
*/
protected function casts(): array
{
return [
'is_active' => 'boolean',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class SharedService extends ServiceProvider
{
/**
* @var array<string,string>
*/
public array $bindings = [
/** User repository */
\App\Shared\Service\User\SearchByIdContract::class =>
\App\Shared\Service\User\SearchById::class,
];
/**
* @var array<int,string>
*/
public array $singletons = [
/** Singletons */
];
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Shared\Event\User;
class Created
{
private string $userId;
private string $createdAt;
public function __construct(string $userId, string $createdAt)
{
$this->userId = $userId;
$this->createdAt = $createdAt;
}
public function getUserId(): string
{
return $this->userId;
}
public function getCreatedAt(): string
{
return $this->createdAt;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Shared\Event\User;
class Deleted
{
private string $userId;
private string $deletedAt;
public function __construct(string $userId, string $deletedAt)
{
$this->userId = $userId;
$this->deletedAt = $deletedAt;
}
public function getUserId(): string
{
return $this->userId;
}
public function getDeletedAt(): string
{
return $this->deletedAt;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Shared\Event\User;
class Updated
{
private string $userId;
private string $updatedAt;
public function __construct(string $userId, string $updatedAt)
{
$this->userId = $userId;
$this->updatedAt = $updatedAt;
}
public function getUserId(): string
{
return $this->userId;
}
public function getUpdatedAt(): string
{
return $this->updatedAt;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Shared\Service\User;
use App\Models\User\User;
class SearchById implements SearchByIdContract
{
/**
* @param string $id
*
* @return \App\Models\User\User
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function getById(string $id): User
{
return User::findOrFail($id);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Shared\Service\User;
use App\Models\User\User;
interface SearchByIdContract
{
public function getById(string $id): User;
}