From 175315493e7a104ab9fc99861b01b7a87a50d624 Mon Sep 17 00:00:00 2001 From: ArrayIterator Date: Mon, 16 Oct 2023 01:33:26 +0700 Subject: [PATCH] migrate from core --- .github/workflows/continuous-integration.yml | 33 + .gitignore | 103 +++ Entities/Admin.php | 197 ++++++ Entities/AdminLog.php | 79 +++ Entities/AdminMeta.php | 113 ++++ Entities/AdminOnlineActivity.php | 79 +++ Entities/Attachment.php | 80 +++ Entities/Capability.php | 229 +++++++ Entities/Role.php | 193 ++++++ Entities/RoleCapability.php | 158 +++++ Entities/User.php | 394 ++++++++++++ Entities/UserAttachment.php | 80 +++ Entities/UserLog.php | 79 +++ Entities/UserMeta.php | 113 ++++ Entities/UserOnlineActivity.php | 77 +++ Entities/UserTerm.php | 358 +++++++++++ Entities/UserTermGroup.php | 209 +++++++ Entities/UserTermGroupMeta.php | 113 ++++ Entities/UserTermMeta.php | 113 ++++ Factory/AdminEntityFactory.php | 32 + Factory/CapabilityFactory.php | 41 ++ Factory/UserEntityFactory.php | 32 + LICENSE | 21 + Languages/users-module.pot | 26 + README.md | 9 + Users.php | 620 +++++++++++++++++++ composer.json | 37 ++ phpcs.xml | 59 ++ 28 files changed, 3677 insertions(+) create mode 100644 .github/workflows/continuous-integration.yml create mode 100644 .gitignore create mode 100644 Entities/Admin.php create mode 100644 Entities/AdminLog.php create mode 100644 Entities/AdminMeta.php create mode 100644 Entities/AdminOnlineActivity.php create mode 100644 Entities/Attachment.php create mode 100644 Entities/Capability.php create mode 100644 Entities/Role.php create mode 100644 Entities/RoleCapability.php create mode 100644 Entities/User.php create mode 100644 Entities/UserAttachment.php create mode 100644 Entities/UserLog.php create mode 100644 Entities/UserMeta.php create mode 100644 Entities/UserOnlineActivity.php create mode 100644 Entities/UserTerm.php create mode 100644 Entities/UserTermGroup.php create mode 100644 Entities/UserTermGroupMeta.php create mode 100644 Entities/UserTermMeta.php create mode 100644 Factory/AdminEntityFactory.php create mode 100644 Factory/CapabilityFactory.php create mode 100644 Factory/UserEntityFactory.php create mode 100644 LICENSE create mode 100644 Languages/users-module.pot create mode 100644 README.md create mode 100644 Users.php create mode 100644 composer.json create mode 100644 phpcs.xml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..7d7471e --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,33 @@ +name: "Continuous Integration" + +on: + - pull_request + - push + +jobs: + coding-standards: + name: "Coding Standards" + + runs-on: ubuntu-latest + + steps: + - name: "Checkout" + uses: actions/checkout@master + + - name: "Install Php 8.3" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer:v2 + extensions: openssl, json, pdo, pdo_mysql, fileinfo, curl + + - name: "Validate composer.json" + run: php $(which composer) validate --strict + + - name: "Install dependencies with composer" + run: php $(which composer) install --no-interaction --no-progress --no-suggest + + - name: "Run PHP CodeSniffer" + run: php vendor/bin/phpcs --standard=phpcs.xml + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9668283 --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.DS_Store + + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp +*.exe + +# Windows shortcuts +*.lnk + +# +# ================================= +# Gitignore For Linux OS +# ================================= + +# KDE directory preferences +.directory + + +# +# ================================= +# Gitignore For Jetbrain +# ================================= + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +*.iml + +# Directory-based project format: +#.idea/*.xml +.idea/ + +# File-based project format: +*.ipr +*.iws + +# JIRA plugin +atlassian-ide-plugin.xml + +# +# ================================= +# Gitignore For Text Editor +# ================================= + +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Ignore Sublime .phpintel +.phpintel + +# Ignore Visual Studio code +.vscode + +# +# ================================= +# Gitignore For Composer & Projects +# ================================= + +# ignore all log file +*.log + +# ignore composer lock +composer.lock + +# ignore composer phar file +composer.phar + +# add exclude vendor +vendor/ + +# ignore npm lock +package-lock.json + +# ignore node modules +node_modules/ + +# IGNORE ENVIRONMENT +.env +config.php +.config.php +# ignore php ini user +.user.ini diff --git a/Entities/Admin.php b/Entities/Admin.php new file mode 100644 index 0000000..01122eb --- /dev/null +++ b/Entities/Admin.php @@ -0,0 +1,197 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Administrator users' + ] +)] +#[UniqueConstraint( + name: 'unique_username', + columns: ['username'] +)] +#[UniqueConstraint( + name: 'unique_email', + columns: ['email'] +)] +#[UniqueConstraint( + name: 'unique_identity_number', + columns: ['identity_number'] +)] +#[Index( + columns: ['username', 'status', 'role', 'first_name', 'last_name'], + name: 'index_username_status_role_first_name_last_name' +)] +#[Index( + columns: ['attachment_id'], + name: 'relation_admins_attachment_id_attachments_id' +)] +#[Index( + columns: ['role'], + name: 'relation_admins_role_roles_identity' +)] +#[HasLifecycleCallbacks] +class Admin extends AbstractUser +{ + const TABLE_NAME = 'admins'; + + const ROLE_SUPER_ADMIN = SuperAdminRole::NAME; + // administrator and it was co admin + const ROLE_ADMIN = 'admin'; + const ROLE_PRESIDENT = 'president'; + const ROLE_VICE_PRESIDENT = 'vice_president'; + const ROLE_RECTOR = 'rector'; + const ROLE_VICE_RECTOR = 'vice_rector'; + // dean + const ROLE_DEAN = 'dean'; + const ROLE_VICE_DEAN = 'vice_dean'; + // faculty + const ROLE_HEAD_FACULTY = 'head_faculty'; + const ROLE_VICE_HEAD_FACULTY = 'vice_head_faculty'; + + // department + const ROLE_HEAD_DEPARTMENT = 'head_department'; + const ROLE_VICE_HEAD_DEPARTMENT = 'vice_head_department'; + + // headmaster + const ROLE_HEADMASTER = 'headmaster'; + const ROLE_VICE_HEADMASTER = 'vice_headmaster'; + // hrd + const ROLE_HUMAN_RESOURCE_DEPARTMENT = 'human_resource_department'; + const ROLE_HUMAN_RESOURCE_MANAGEMENT = 'human_resource_management'; + + // lecturer + const ROLE_LECTURER = 'lecturer'; + const ROLE_TEACHER = 'teacher'; + const ROLE_COUNSELING_GUIDANCE = 'counseling_guidance'; + const ROLE_SUPERVISOR = 'supervisor'; + const ROLE_LIBRARIAN = 'librarian'; + // staff + const ROLE_OFFICE_STAFF = 'office_staff'; + // treasurer + const ROLE_TREASURER = 'treasurer'; + // office admin + const ROLE_OFFICE_ADMINISTRATION = 'office_admin'; + // other staff / worker + const ROLE_STAFF = 'staff'; + + protected array $availableRoles = [ + self::ROLE_SUPER_ADMIN, + self::ROLE_ADMIN, + self::ROLE_PRESIDENT, + self::ROLE_VICE_PRESIDENT, + self::ROLE_RECTOR, + self::ROLE_VICE_RECTOR, + self::ROLE_DEAN, + self::ROLE_VICE_DEAN, + self::ROLE_HEAD_FACULTY, + self::ROLE_VICE_HEAD_FACULTY, + self::ROLE_HEAD_DEPARTMENT, + self::ROLE_VICE_HEAD_DEPARTMENT, + self::ROLE_HEADMASTER, + self::ROLE_VICE_HEADMASTER, + self::ROLE_HUMAN_RESOURCE_DEPARTMENT, + self::ROLE_HUMAN_RESOURCE_MANAGEMENT, + self::ROLE_LECTURER, + self::ROLE_TEACHER, + self::ROLE_COUNSELING_GUIDANCE, + self::ROLE_SUPERVISOR, + self::ROLE_LIBRARIAN, + self::ROLE_OFFICE_STAFF, + self::ROLE_TREASURER, + self::ROLE_OFFICE_ADMINISTRATION, + self::ROLE_STAFF, + ]; + + #[ + JoinColumn( + name: 'attachment_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_admins_attachment_id_attachments_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: Attachment::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY' + ) + ] + protected ?Attachment $attachment = null; + + #[ + JoinColumn( + name: 'role', + referencedColumnName: 'identity', + nullable: true, + onDelete: 'RESTRICT', + options: [ + 'relation_name' => 'relation_admins_role_roles_identity', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'RESTRICT' + ], + ), + ManyToOne( + targetEntity: Role::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY' + ) + ] + protected ?Role $roleObject = null; + + public function getObjectRole(): Role + { + if (!$this->roleObject) { + $this->roleObject = new Role(); + $this->roleObject->setIdentity($this->getRole()); + $this->roleObject->setName($this->getRole()); + $entity = $this->getEntityManager(); + $entity && $this->roleObject->setEntityManager($entity); + } + return $this->roleObject; + } + + public function setRoleObject(Role $roleObject): void + { + $this->roleObject = $roleObject; + $this->setRole($roleObject->getIdentity()); + } + + public function getAttachment(): ?Attachment + { + return $this->attachment; + } + + public function setAttachment(?Attachment $attachment): void + { + $this->attachment = $attachment; + $this->setAttachmentId($attachment?->getId()); + } +} diff --git a/Entities/AdminLog.php b/Entities/AdminLog.php new file mode 100644 index 0000000..89f35e8 --- /dev/null +++ b/Entities/AdminLog.php @@ -0,0 +1,79 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Administrator user logs' + ] +)] +#[Index( + columns: ['user_id'], + name: 'relation_admin_logs_user_id_admins_id' +)] +#[Index( + columns: ['user_id', 'name', 'type'], + name: 'index_user_id_name_type' +)] +#[HasLifecycleCallbacks] +class AdminLog extends AbstractUserBasedLog +{ + const TABLE_NAME = 'admin_logs'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_admin_logs_user_id_admins_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ], + ), + ManyToOne( + targetEntity: Admin::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected Admin $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function setUser(Admin $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): Admin + { + return $this->user; + } +} diff --git a/Entities/AdminMeta.php b/Entities/AdminMeta.php new file mode 100644 index 0000000..ae0cb79 --- /dev/null +++ b/Entities/AdminMeta.php @@ -0,0 +1,113 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Administrator user metadata', + 'primaryKey' => [ + 'user_id', + 'name' + ] + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[Index( + columns: ['user_id'], + name: 'relation_admin_meta_user_id_admins_id' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $user_id + * @property-read Admin $user + */ +class AdminMeta extends AbstractBasedMeta +{ + const TABLE_NAME = 'admin_meta'; + + #[Id] + #[Column( + name: 'user_id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key composite identifier' + ] + )] + protected int $user_id; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_admin_meta_user_id_admins_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: Admin::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'EAGER' + ) + ] + protected Admin $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function getUserId(): int + { + return $this->user_id; + } + + public function setUserId(int $user_id): void + { + $this->user_id = $user_id; + } + + public function setUser(Admin $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): Admin + { + return $this->user; + } +} diff --git a/Entities/AdminOnlineActivity.php b/Entities/AdminOnlineActivity.php new file mode 100644 index 0000000..3c9a6fb --- /dev/null +++ b/Entities/AdminOnlineActivity.php @@ -0,0 +1,79 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Administrator user online activity', + ] +)] +#[Index( + columns: ['user_id', 'name', 'created_at', 'updated_at'], + name: 'index_user_id_name_created_at_updated_at' +)] +#[Index( + columns: ['user_id'], + name: 'relation_admin_online_activities_user_id_admins_id' +)] +#[HasLifecycleCallbacks] +class AdminOnlineActivity extends AbstractBasedOnlineActivity +{ + const TABLE_NAME = 'admin_online_activities'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_admin_online_activities_user_id_admins_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + OneToOne( + targetEntity: Admin::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected Admin $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function setUser(Admin $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): Admin + { + return $this->user; + } +} diff --git a/Entities/Attachment.php b/Entities/Attachment.php new file mode 100644 index 0000000..528b98c --- /dev/null +++ b/Entities/Attachment.php @@ -0,0 +1,80 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Attachments created by admin user', + ] +)] +#[UniqueConstraint( + name: 'unique_path_storage_type', + columns: ['path', 'storage_type'] +)] +#[Index( + columns: ['storage_type', 'mime_type'], + name: 'index_storage_type_mime_type' +)] +#[Index( + columns: ['user_id'], + name: 'relation_attachments_user_id_admins_id' +)] +#[Index( + columns: ['name', 'file_name', 'status', 'mime_type', 'storage_type'], + name: 'index_name_file_name_status_mime_type_storage_type' +)] +#[HasLifecycleCallbacks] +class Attachment extends AbstractAttachment +{ + const TABLE_NAME = 'attachments'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_attachments_user_id_admins_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: Admin::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY', + ) + ] + protected ?Admin $user = null; + + public function setUser(?Admin $user): void + { + $this->user = $user; + $this->setUserId($user?->getId()); + } + + public function getUser(): ?Admin + { + return $this->user; + } +} diff --git a/Entities/Capability.php b/Entities/Capability.php new file mode 100644 index 0000000..9512b50 --- /dev/null +++ b/Entities/Capability.php @@ -0,0 +1,229 @@ + $roleCapability + */ +#[Entity] +#[Table( + name: self::TABLE_NAME, + options: [ + 'charset' => 'utf8mb4', // remove this or change to utf8 if not use mysql + 'collation' => 'utf8mb4_unicode_ci', // remove this if not use mysql + 'comment' => 'Capabilities' + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[Index( + columns: ['type'], + name: 'index_type' +)] +#[HasLifecycleCallbacks] +class Capability extends AbstractEntity implements CapabilityEntityInterface +{ + const TABLE_NAME = 'capabilities'; + const TYPE_USER = 'user'; + const TYPE_ADMIN = 'admin'; + + const TYPE_GLOBAL = null; + const TYPE_GLOBAL_ALTERNATE = 'global'; + + #[Id] + #[Column( + name: 'identity', + type: Types::STRING, + length: 128, + updatable: true, + options: [ + 'comment' => 'Primary key capability identity' + ] + )] + protected string $identity; + + #[Column( + name: 'name', + type: Types::STRING, + length: 255, + options: [ + 'comment' => 'Capability name' + ] + )] + protected string $name; + + #[Column( + name: 'description', + type: Types::TEXT, + length: AbstractMySQLPlatform::LENGTH_LIMIT_TEXT, + nullable: true, + options: [ + 'comment' => 'Capability description' + ] + )] + protected ?string $description = null; + + #[Column( + name: 'type', + type: Types::STRING, + length: 20, + nullable: true, + options: [ + 'default' => self::TYPE_GLOBAL, + 'comment' => 'Capability type. user -> as users, admin -> as admins user & null/empty as global' + ] + )] + protected ?string $type = null; + + #[OneToMany( + mappedBy: 'capability', + targetEntity: RoleCapability::class, + cascade: [ + 'detach', + 'merge', + 'persist', + 'remove', + ], + fetch: 'LAZY' + )] + /** + * @var ?Collection $roleCapability + */ + protected ?Collection $roleCapability = null; + + public function getIdentity(): string + { + return $this->identity; + } + + public function setIdentity(string $identity): void + { + $this->identity = $identity; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): void + { + $this->description = $description; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(?string $type): void + { + $type = is_string($type) ? strtolower(trim($type)) : $type; + $this->type = $type; + } + + public function isGlobal() : bool + { + $type = $this->getType(); + $type = is_string($type) ? strtolower(trim($type)) : $type; + return $type === '' || $type === null || $type === self::TYPE_GLOBAL; + } + + public function isUser() : bool + { + $type = $this->getType(); + return (is_string($type) ? trim($type) : $type) === self::TYPE_USER; + } + + public function isAdmin() : bool + { + $type = $this->getType(); + return (is_string($type) ? trim($type) : $type) === self::TYPE_ADMIN; + } + + /** + * @return ?Collection + */ + public function getRoleCapability(): ?Collection + { + return $this->roleCapability; + } + + /** @noinspection PhpUnusedParameterInspection */ + #[ + PostLoad, + PrePersist + ] + public function postLoadChangeIdentity( + PrePersistEventArgs|PostLoadEventArgs $event + ) : void { + $this->identity = strtolower(trim($this->identity)); + $this->identity = preg_replace('~[\s_]+~', '_', $this->identity); + $this->identity = trim($this->identity, '_'); + if ($this->identity === '') { + throw new EmptyArgumentException( + 'Identity could not being empty or contain whitespace only' + ); + } + } + + public function has(RoleInterface|string $role) : bool + { + $role = is_object($role) ? $role->getRole() : $role; + return $this + ->getRoleCapability() + ->exists(static fn ($i, RoleCapability $r) => $r->getRoleIdentity() === $role); + } + + /** + * @return iterable + */ + public function getRoles(): iterable + { + return IterableHelper::each( + $this->getRoleCapability(), + static function (&$key, RoleCapability $r) { + $key = $r->getRoleIdentity(); + return $r->getRole(); + } + ); + } +} diff --git a/Entities/Role.php b/Entities/Role.php new file mode 100644 index 0000000..7370831 --- /dev/null +++ b/Entities/Role.php @@ -0,0 +1,193 @@ + 'utf8mb4', // remove this or change to utf8 if not use mysql + 'collation' => 'utf8mb4_unicode_ci', // remove this if not use mysql + 'comment' => 'Table roles' + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[HasLifecycleCallbacks] +/** + * @property-read string $identity + * @property-read string $name + * @property-read ?string $description + */ +class Role extends AbstractEntity implements RoleInterface +{ + const TABLE_NAME = 'roles'; + + private ?string $originIdentity = null; + + #[Id] + #[Column( + name: 'identity', + type: Types::STRING, + length: 128, + updatable: true, + options: [ + 'comment' => 'Primary key role identity' + ] + )] + protected string $identity; + + #[Column( + name: 'name', + type: Types::STRING, + length: 255, + options: [ + 'comment' => 'Role name' + ] + )] + protected string $name; + + #[Column( + name: 'description', + type: Types::TEXT, + length: AbstractMySQLPlatform::LENGTH_LIMIT_TEXT, + nullable: true, + options: [ + 'comment' => 'Role description' + ] + )] + protected ?string $description = null; + + #[OneToMany( + mappedBy: 'role', + targetEntity: RoleCapability::class, + cascade: [ + 'detach', + 'merge', + 'persist', + 'remove', + ], + fetch: 'LAZY' + )] + protected ?Collection $roleCapability = null; + + public function getIdentity(): string + { + return $this->identity; + } + + public function setIdentity(string $identity): void + { + $identity = strtolower(trim($identity)); + if ($identity === '') { + throw new EmptyArgumentException( + 'Identity could not being empty or contain whitespace only' + ); + } + $this->identity = $identity; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): void + { + $this->description = $description; + } + + public function getRole(): string + { + return $this->getIdentity(); + } + + public function getRoleCapability(): ?Collection + { + return $this->roleCapability; + } + + #[ + PreUpdate, + PrePersist + ] + public function postCheckChangeIdentity() : void + { + $this->identity = strtolower(trim($this->identity)); + if ($this->identity === '') { + throw new EmptyArgumentException( + 'Identity could not being empty or contain whitespace only' + ); + } + } + + /** @noinspection PhpUnusedParameterInspection */ + #[PostLoad] + public function postLoadIdentityLower(PostLoadEventArgs $eventArgs): void + { + $this->originIdentity ??= $this->identity; + $this->identity = strtolower(trim($this->originIdentity)); + } + + public function serialize(): ?string + { + return serialize($this->__serialize()); + } + + public function unserialize(string $data): void + { + $this->unserialize(unserialize($data)); + } + + public function __serialize(): array + { + return [ + 'identity' => $this->identity, + 'name' => $this->name, + 'description' => $this->description, + ]; + } + + public function __unserialize(array $data): void + { + $this->identity = $data['identity']; + $this->name = $data['name']; + $this->description = $data['description']; + } + + public function __toString(): string + { + return $this->getIdentity(); + } +} diff --git a/Entities/RoleCapability.php b/Entities/RoleCapability.php new file mode 100644 index 0000000..2238668 --- /dev/null +++ b/Entities/RoleCapability.php @@ -0,0 +1,158 @@ + 'utf8mb4', // remove this or change to utf8 if not use mysql + 'collation' => 'utf8mb4_unicode_ci', // remove this if not use mysql + 'comment' => 'Role relation capability & meta', + 'primaryKey' => [ + 'class_id', + 'name' + ] + ] +)] +#[Index( + columns: ['role_identity'], + name: 'relation_capabilities_role_identity_roles_identity' +)] +#[Index( + columns: ['capability_identity'], + name: 'relation_capabilities_capability_identity_capabilities_identity' +)] +#[HasLifecycleCallbacks] +class RoleCapability extends AbstractEntity +{ + const TABLE_NAME = 'role_capabilities'; + + #[Id] + #[Column( + name: 'role_identity', + type: Types::STRING, + length: 128, + updatable: true, + options: [ + 'comment' => 'Primary key composite identifier' + ] + )] + protected string $role_identity; + + #[Id] + #[Column( + name: 'capability_identity', + type: Types::STRING, + length: 128, + updatable: true, + options: [ + 'comment' => 'Primary key composite identifier' + ] + )] + protected string $capability_identity; + + #[ + JoinColumn( + name: 'role_identity', + referencedColumnName: 'identity', + nullable: false, + onDelete: 'RESTRICT', + options: [ + 'relation_name' => 'relation_capabilities_role_identity_roles_identity', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'RESTRICT' + ] + ), + ManyToOne( + targetEntity: Role::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected Role $role; + + #[ + JoinColumn( + name: 'capability_identity', + referencedColumnName: 'identity', + nullable: false, + onDelete: 'RESTRICT', + options: [ + 'relation_name' => 'relation_capabilities_capability_identity_capabilities_identity', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'RESTRICT' + ] + ), + ManyToOne( + targetEntity: Capability::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected Capability $capability; + + public function getRoleIdentity(): string + { + return $this->role_identity; + } + + public function setRoleIdentity(string $role_identity): void + { + $this->role_identity = $role_identity; + } + + public function getCapabilityIdentity(): string + { + return $this->capability_identity; + } + + public function setCapabilityIdentity(string $capability_identity): void + { + $this->capability_identity = $capability_identity; + } + + public function getRole(): Role + { + return $this->role; + } + + public function setRole(Role $role): void + { + $this->role = $role; + $this->setRoleIdentity($role->getIdentity()); + } + + public function getCapability(): Capability + { + return $this->capability; + } + + public function setCapability(Capability $capability): void + { + $this->capability = $capability; + $this->setCapabilityIdentity($capability->getIdentity()); + } +} diff --git a/Entities/User.php b/Entities/User.php new file mode 100644 index 0000000..0f765f7 --- /dev/null +++ b/Entities/User.php @@ -0,0 +1,394 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'User lists', + ] +)] +#[UniqueConstraint( + name: 'unique_username', + columns: ['username'] +)] +#[UniqueConstraint( + name: 'unique_email', + columns: ['email'] +)] +#[UniqueConstraint( + name: 'unique_identity_number', + columns: ['identity_number'] +)] +#[Index( + columns: ['school_year', 'status'], + name: 'index_school_year_status' +)] +#[Index( + columns: ['username', 'status', 'role', 'first_name', 'last_name', 'school_year'], + name: 'index_username_status_role_first_name_last_name_school_year' +)] +/* +#[Index( + columns: ['class_id', 'role'], + name: 'index_class_id_role' +)] +#[Index( + columns: ['class_id'], + name: 'relation_users_class_id_classes_id' +)] +*/ + +#[Index( + columns: ['related_user_id'], + name: 'relation_users_related_user_id_users_id' +)] +#[Index( + columns: ['attachment_id'], + name: 'relation_users_attachment_id_user_attachments_id' +)] +#[Index( + columns: ['role'], + name: 'relation_users_role_roles_identity' +)] +#[HasLifecycleCallbacks] +class User extends AbstractUser +{ + const TABLE_NAME = 'users'; + const ROLE_STUDENT = 'student'; + const ROLE_ALUMNI = 'alumni'; + const ROLE_GUARDIAN = 'guardian'; + const ROLE_GUEST = 'guest'; + + protected array $availableRoles = [ + self::ROLE_STUDENT, + self::ROLE_ALUMNI, + self::ROLE_GUARDIAN, + self::ROLE_GUEST, + ]; + + /* + #[Column( + name: 'class_id', + type: Types::BIGINT, + length: 20, + nullable: true, + options: [ + 'unsigned' => true, + 'default' => null, + 'comment' => 'Class id' + ] + )] + protected ?int $class_id = null; + + #[ + JoinColumn( + name: 'class_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_users_class_id_classes_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: Classes::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'EAGER' + ) + ] + protected ?Classes $class; + */ + + #[Column( + name: 'related_user_id', + type: Types::BIGINT, + length: 20, + nullable: true, + options: [ + 'unsigned' => true, + 'default' => null, + 'comment' => 'Relational user id' + ] + )] + protected ?int $related_user_id = null; + + #[Column( + name: 'school_year', + type: TypeList::YEAR, + nullable: false, + options: [ + 'comment' => 'User school year' + ] + )] + protected DateTimeInterface $school_year; + + #[ + JoinColumn( + name: 'related_user_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_users_related_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: User::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY' + ) + ] + protected ?User $related_user = null; + + #[ + JoinColumn( + name: 'attachment_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_users_attachment_id_user_attachments_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: UserAttachment::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY' + ) + ] + protected ?UserAttachment $attachment = null; + + #[ + JoinColumn( + name: 'role', + referencedColumnName: 'identity', + nullable: true, + onDelete: 'RESTRICT', + options: [ + 'relation_name' => 'relation_users_role_roles_identity', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'RESTRICT' + ], + ), + ManyToOne( + targetEntity: Role::class, + cascade: [ + 'persist' + ], + fetch: 'EAGER' + ) + ] + protected ?Role $roleObject = null; + + public function getSchoolYear(): DateTimeInterface + { + return $this->school_year; + } + + public function setSchoolYear(DateTimeInterface|int|null $school_year): void + { + /** @noinspection DuplicatedCode */ + if (is_int($school_year)) { + $school_year = (string) $school_year; + if ($school_year < 1000) { + do { + $school_year = "0$school_year"; + } while (strlen($school_year) < 4); + } + $school_year = substr($school_year, 0, 4); + $school_year = DateTimeImmutable::createFromFormat( + '!Y-m-d', + "$school_year-01-01" + )?:new DateTimeImmutable("$school_year-01-01 00:00:00"); + } + + $this->school_year = $school_year; + } + + public function getClassId(): ?int + { + return $this->class_id; + } + + /* + public function setClassId(?int $class_id): void + { + $this->class_id = $class_id; + } + + public function getClass(): ?Classes + { + return $this->class; + } + + public function setClass(?Classes $class): void + { + $this->class = $class; + $this->setClassId($class?->getId()); + } + */ + + public function getObjectRole(): Role + { + if (!$this->roleObject) { + $this->roleObject = new Role(); + $this->roleObject->setIdentity($this->getRole()); + $this->roleObject->setName($this->getRole()); + $entity = $this->getEntityManager(); + $entity && $this->roleObject->setEntityManager($entity); + } + return $this->roleObject; + } + + public function setRoleObject(Role $roleObject): void + { + $this->roleObject = $roleObject; + $this->setRole($roleObject->getIdentity()); + } + + public function getRelatedUserId(): ?int + { + return $this->related_user_id; + } + + public function setRelatedUserId(?int $related_user_id): void + { + $this->related_user_id = $related_user_id; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): void + { + $this->related_user = $related_user; + $this->setRelatedUserId($related_user?->getId()); + } + + public function getAttachment(): ?UserAttachment + { + return $this->attachment; + } + + public function setAttachment(?UserAttachment $attachment): void + { + $this->attachment = $attachment; + $this->setAttachmentId($attachment?->getId()); + } + + #[ + PreUpdate, + PostLoad, + PrePersist + ] + public function relationIdCheck( + PrePersistEventArgs|PostLoadEventArgs|PreUpdateEventArgs $event + ) : void { + if ($event instanceof PreUpdateEventArgs + && $event->hasChangedField('related_user_id') + && $event->getNewValue('related_user_id') === $this->getId() + ) { + $oldValue = $event->getOldValue('related_user_id'); + if ($oldValue !== null) { + /** + * @var self $parent + */ + $parent = $event + ->getObjectManager() + ->getRepository($this::class) + ->find($this->getId()) + ?->getRelatedUser(); + if ($parent?->getId() === $parent?->getRelatedUserId()) { + $parent = null; + $oldValue = null; + } + } + $this->setRelatedUser($parent??null); + $this->setRelatedUserId($oldValue); + $event->setNewValue('related_user_id', $oldValue); + } elseif (!$event instanceof PreUpdateEventArgs + && $this->getRelatedUserId() === $this->getId() + ) { + $parent = $event + ->getObjectManager() + ->getRepository($this::class) + ->find($this->getId()) + ?->getRelatedUser(); + if ($parent?->getId() === $parent?->getRelatedUserId()) { + $parent = null; + } + // prevent + $this->setRelatedUserId($parent?->getId()); + $this->setRelatedUser($parent); + $q = $event + ->getObjectManager() + ->createQueryBuilder() + ->update($this::class, 'x') + ->set('x.related_user_id', ':val') + ->where('x.id = :id') + ->setParameters([ + 'val' => null, + 'id' => $this->getId(), + ]); + $date = $this->getUpdatedAt(); + /** @noinspection PhpConditionAlreadyCheckedInspection */ + if ($date instanceof DateTimeInterface) { + $date = str_starts_with($date->format('Y'), '-') + ? '0000-00-00 00:00:00' + : $date->format('Y-m-d H:i:s'); + } + $q + ->set('x.updated_at', ':updated_at') + ->setParameter('updated_at', $date); + $q->getQuery()->execute(); + } + } +} diff --git a/Entities/UserAttachment.php b/Entities/UserAttachment.php new file mode 100644 index 0000000..41680cb --- /dev/null +++ b/Entities/UserAttachment.php @@ -0,0 +1,80 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Attachments created by common user', + ] +)] +#[UniqueConstraint( + name: 'unique_path_storage_type', + columns: ['path', 'storage_type'] +)] +#[Index( + columns: ['storage_type', 'mime_type'], + name: 'index_storage_type_mime_type' +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_attachments_user_id_users_id' +)] +#[Index( + columns: ['name', 'file_name', 'status', 'mime_type', 'storage_type'], + name: 'index_name_file_name_status_mime_type_storage_type' +)] +#[HasLifecycleCallbacks] +class UserAttachment extends AbstractAttachment +{ + const TABLE_NAME = 'user_attachments'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_user_attachments_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: User::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY', + ) + ] + protected ?User $user = null; + + public function setUser(?User $user): void + { + $this->user = $user; + $this->setUserId($user?->getId()); + } + + public function getUser(): ?User + { + return $this->user; + } +} diff --git a/Entities/UserLog.php b/Entities/UserLog.php new file mode 100644 index 0000000..11fd771 --- /dev/null +++ b/Entities/UserLog.php @@ -0,0 +1,79 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Common user logs', + ], +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_logs_user_id_users_id' +)] +#[Index( + columns: ['user_id', 'name', 'type'], + name: 'index_user_id_name_type' +)] +#[HasLifecycleCallbacks] +class UserLog extends AbstractUserBasedLog +{ + const TABLE_NAME = 'user_logs'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_logs_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: User::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected User $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function setUser(User $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): User + { + return $this->user; + } +} diff --git a/Entities/UserMeta.php b/Entities/UserMeta.php new file mode 100644 index 0000000..18f39bb --- /dev/null +++ b/Entities/UserMeta.php @@ -0,0 +1,113 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Common user metadata', + 'primaryKey' => [ + 'user_id', + 'name' + ] + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_meta_user_id_users_id' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $user_id + * @property-read User $user + */ +class UserMeta extends AbstractBasedMeta +{ + const TABLE_NAME = 'user_meta'; + + #[Id] + #[Column( + name: 'user_id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key composite identifier' + ] + )] + protected int $user_id; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_meta_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: User::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'EAGER' + ) + ] + protected User $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function getUserId(): int + { + return $this->user_id; + } + + public function setUserId(int $user_id): void + { + $this->user_id = $user_id; + } + + public function setUser(User $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): User + { + return $this->user; + } +} diff --git a/Entities/UserOnlineActivity.php b/Entities/UserOnlineActivity.php new file mode 100644 index 0000000..ccacff2 --- /dev/null +++ b/Entities/UserOnlineActivity.php @@ -0,0 +1,77 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Common user online activity', + ] +)] +#[Index( + columns: ['user_id', 'name', 'created_at', 'updated_at'], + name: 'index_user_id_name_created_at_updated_at' +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_online_activities_user_id_users_id' +)] +class UserOnlineActivity extends AbstractBasedOnlineActivity +{ + const TABLE_NAME = 'user_online_activities'; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_online_activities_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + OneToOne( + targetEntity: User::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'LAZY' + ) + ] + protected User $user; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function setUser(User $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getUser(): User + { + return $this->user; + } +} diff --git a/Entities/UserTerm.php b/Entities/UserTerm.php new file mode 100644 index 0000000..666f9c2 --- /dev/null +++ b/Entities/UserTerm.php @@ -0,0 +1,358 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'User terms grouping metadata like quiz sorting', + ] +)] +#[UniqueConstraint( + name: 'unique_name', + columns: ['name'] +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_terms_user_id_admins_id' +)] +#[Index( + columns: ['name', 'title', 'status'], + name: 'index_name_title_status' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $id + * @property-read ?int $user_id + * @property-read string $name + * @property-read ?string $title + * @property-read string $status + * @property-read ?string $password + * @property-read ?DateTimeInterface $published_at + * @property-read DateTimeInterface $created_at + * @property-read DateTimeInterface $updated_at + * @property-read ?DateTimeInterface $deleted_at + */ +class UserTerm extends AbstractEntity implements AvailabilityStatusEntityInterface +{ + const TABLE_NAME = 'user_terms'; + + use AvailabilityStatusTrait, + PasswordTrait; + + #[Id] + #[GeneratedValue('AUTO')] + #[Column( + name: 'id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key' + ] + )] + protected int $id; + + #[Column( + name: 'user_id', + type: Types::BIGINT, + length: 20, + nullable: true, + options: [ + 'unsigned' => true, + 'default' => null, + 'comment' => 'Admin id' + ] + )] + protected ?int $user_id = null; + + #[Column( + name: 'name', + type: Types::STRING, + length: 255, + unique: true, + nullable: false, + options: [ + 'comment' => 'Unique name' + ] + )] + protected string $name; + + #[Column( + name: 'title', + type: Types::STRING, + length: 255, + nullable: true, + options: [ + 'default' => null, + 'comment' => 'Term title' + ] + )] + protected ?string $title; + + #[Column( + name: 'content', + type: Types::TEXT, + length: 4294967295, + nullable: true, + options: [ + 'default' => null, + 'comment' => 'Term content' + ] + )] + protected ?string $content = null; + + #[Column( + name: 'status', + type: Types::STRING, + length: 64, + nullable: false, + options: [ + 'default' => self::DRAFT, + 'comment' => 'Term status' + ] + )] + protected string $status = self::DRAFT; + + #[Column( + name: 'password', + type: Types::STRING, + length: 255, + nullable: true, + updatable: true, + options: [ + 'default' => null, + 'comment' => 'Term password' + ] + )] + protected ?string $password = null; + + #[Column( + name: 'published_at', + type: Types::DATETIME_MUTABLE, + nullable: true, + options: [ + 'default' => null, + 'comment' => 'Published at' + ] + )] + protected ?DateTimeInterface $published_at; + + #[Column( + name: 'created_at', + type: Types::DATETIME_MUTABLE, + updatable: false, + options: [ + 'default' => 'CURRENT_TIMESTAMP', + 'comment' => 'User term created time' + ] + )] + protected DateTimeInterface $created_at; + + #[Column( + name: 'updated_at', + type: Types::DATETIME_IMMUTABLE, + unique: false, + updatable: false, + options: [ + 'attribute' => 'ON UPDATE CURRENT_TIMESTAMP', // this column attribute + 'default' => '0000-00-00 00:00:00', + 'comment' => 'User term update time' + ], + // columnDefinition: "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP" + )] + protected DateTimeInterface $updated_at; + + #[Column( + name: 'deleted_at', + type: Types::DATETIME_IMMUTABLE, + nullable: true, + options: [ + 'default' => null, + 'comment' => 'User term delete time' + ] + )] + protected ?DateTimeInterface $deleted_at = null; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'SET NULL', + options: [ + 'relation_name' => 'relation_user_terms_user_id_admins_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'SET NULL' + ], + ), + ManyToOne( + targetEntity: Admin::class, + cascade: [ + 'persist' + ], + fetch: 'LAZY' + ) + ] + protected ?Admin $user = null; + + public function __construct() + { + $this->user_id = null; + $this->title = null; + $this->content = ''; + $this->user = null; + $this->status = self::DRAFT; + $this->password = null; + $this->created_at = new DateTimeImmutable(); + $this->updated_at = new DateTimeImmutable('0000-00-00 00:00:00'); + $this->published_at = null; + $this->deleted_at = null; + } + + public function getId() : int + { + return $this->id; + } + + public function getUserId(): ?int + { + return $this->user_id; + } + + public function setUserId(?int $user_id): void + { + $this->user_id = $user_id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(?string $content): void + { + $this->content = $content; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): void + { + $this->status = $status; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(?string $password): void + { + $this->password = $password; + } + + public function getPublishedAt(): ?DateTimeInterface + { + return $this->published_at; + } + + public function setPublishedAt(?DateTimeInterface $published_at): void + { + $this->published_at = $published_at; + } + + public function getCreatedAt(): DateTimeInterface + { + return $this->created_at; + } + + public function getUpdatedAt(): DateTimeInterface + { + return $this->updated_at; + } + + public function getDeletedAt(): ?DateTimeInterface + { + return $this->deleted_at; + } + + public function setDeletedAt(?DateTimeInterface $deletedAt) : void + { + $this->deleted_at = $deletedAt; + } + + public function getUser(): ?Admin + { + return $this->user; + } + + public function setUser(?Admin $user): void + { + $this->user = $user; + $this->setUserId($user?->getId()); + } + + #[ + PreUpdate, + PostLoad, + PrePersist + ] + public function passwordCheck( + PrePersistEventArgs|PostLoadEventArgs|PreUpdateEventArgs $event + ) : void { + $this->passwordBasedIdUpdatedAt($event); + } +} diff --git a/Entities/UserTermGroup.php b/Entities/UserTermGroup.php new file mode 100644 index 0000000..01a9e7f --- /dev/null +++ b/Entities/UserTermGroup.php @@ -0,0 +1,209 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'Group terms user collection', + ] +)] +#[UniqueConstraint( + name: 'unique_user_id_term_id', + columns: ['user_id', 'term_id'] +)] +#[Index( + columns: ['term_id'], + name: 'relation_user_term_groups_term_id_user_terms_id' +)] +#[Index( + columns: ['user_id'], + name: 'relation_user_term_groups_user_id_users_id' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $id + * @property-read int $user_id + * @property-read int $term_id + * @property-read string $status + * @property-read UserTerm $term + */ +class UserTermGroup extends AbstractEntity implements AvailabilityStatusEntityInterface +{ + const TABLE_NAME = 'user_term_groups'; + + use AvailabilityStatusTrait; + + #[Id] + #[GeneratedValue('AUTO')] + #[Column( + name: 'id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key term group id' + ] + )] + protected int $id; + + #[Column( + name: 'user_id', + type: Types::BIGINT, + length: 20, + nullable: false, + options: [ + 'unsigned' => true, + 'comment' => 'User id' + ] + )] + protected int $user_id; + + #[Column( + name: 'term_id', + type: Types::BIGINT, + length: 20, + nullable: false, + options: [ + 'unsigned' => true, + 'comment' => 'User term id' + ] + )] + protected int $term_id; + + #[Column( + name: 'status', + type: Types::STRING, + length: 64, + nullable: false, + options: [ + 'comment' => 'Term group status' + ] + )] + protected string $status; + + #[ + JoinColumn( + name: 'user_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_term_groups_user_id_users_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ], + ), + ManyToOne( + targetEntity: User::class, + cascade: [ + 'persist' + ], + fetch: 'EAGER' + ) + ] + protected User $user; + + #[ + JoinColumn( + name: 'term_id', + referencedColumnName: 'id', + nullable: true, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_term_groups_term_id_user_terms_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ], + ), + ManyToOne( + targetEntity: UserTerm::class, + cascade: [ + 'persist' + ], + fetch: 'EAGER' + ) + ] + protected UserTerm $term; + + public function __construct() + { + } + + public function getId() : int + { + return $this->id; + } + + public function getUserId(): int + { + return $this->user_id; + } + + public function setUserId(int $user_id): void + { + $this->user_id = $user_id; + } + + public function getTermId(): int + { + return $this->term_id; + } + + public function setTermId(?int $term_id): void + { + $this->term_id = $term_id; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): void + { + $this->status = $status; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + $this->setUserId($user->getId()); + } + + public function getTerm(): UserTerm + { + return $this->term; + } + + public function setTerm(UserTerm $term): void + { + $this->term = $term; + $this->setTermId($term->getId()); + } +} diff --git a/Entities/UserTermGroupMeta.php b/Entities/UserTermGroupMeta.php new file mode 100644 index 0000000..3d8b1f9 --- /dev/null +++ b/Entities/UserTermGroupMeta.php @@ -0,0 +1,113 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'User term group metadata', + 'primaryKey' => [ + 'post_id', + 'name' + ] + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[Index( + columns: ['term_group_id'], + name: 'relation_user_term_group_meta_term_group_id_user_terms_group_id' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $term_group_id + * @property-read UserTermGroup $userTermGroup + */ +class UserTermGroupMeta extends AbstractBasedMeta +{ + const TABLE_NAME = 'user_term_group_meta'; + + #[Id] + #[Column( + name: 'term_group_id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key composite identifier' + ] + )] + protected int $term_group_id; + + #[ + JoinColumn( + name: 'term_group_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_term_group_meta_term_group_id_user_terms_group_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: UserTermGroup::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'EAGER' + ) + ] + protected UserTermGroup $userTermGroup; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function getTermGroupId(): int + { + return $this->term_group_id; + } + + public function setTermGroupId(int $term_group_id): void + { + $this->term_group_id = $term_group_id; + } + + public function getUserTermGroup(): UserTermGroup + { + return $this->userTermGroup; + } + + public function setUserTermGroup(UserTermGroup $userTermGroup): void + { + $this->userTermGroup = $userTermGroup; + $this->setTermGroupId($userTermGroup->getId()); + } +} diff --git a/Entities/UserTermMeta.php b/Entities/UserTermMeta.php new file mode 100644 index 0000000..77f96d8 --- /dev/null +++ b/Entities/UserTermMeta.php @@ -0,0 +1,113 @@ + 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => 'User term metadata', + 'primaryKey' => [ + 'post_id', + 'name' + ] + ] +)] +#[Index( + columns: ['name'], + name: 'index_name' +)] +#[Index( + columns: ['term_id'], + name: 'relation_user_term_metadata_term_id_user_terms_id' +)] +#[HasLifecycleCallbacks] +/** + * @property-read int $term_id + * @property-read UserTerm $userTerm + */ +class UserTermMeta extends AbstractBasedMeta +{ + const TABLE_NAME = 'user_term_meta'; + + #[Id] + #[Column( + name: 'term_id', + type: Types::BIGINT, + length: 20, + updatable: false, + options: [ + 'unsigned' => true, + 'comment' => 'Primary key composite identifier' + ] + )] + protected int $term_id; + + #[ + JoinColumn( + name: 'term_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: [ + 'relation_name' => 'relation_user_term_metadata_term_id_user_terms_id', + 'onUpdate' => 'CASCADE', + 'onDelete' => 'CASCADE' + ] + ), + ManyToOne( + targetEntity: UserTerm::class, + cascade: [ + "persist", + "remove", + "merge", + "detach" + ], + fetch: 'EAGER' + ) + ] + protected UserTerm $userTerm; + + /** + * Allow associations mapping + * @see jsonSerialize() + * + * @var bool + */ + protected bool $entityAllowAssociations = true; + + public function getTermId(): int + { + return $this->term_id; + } + + public function setTermId(int $term_id): void + { + $this->term_id = $term_id; + } + + public function getUserTerm(): UserTerm + { + return $this->userTerm; + } + + public function setUserTerm(UserTerm $userTerm): void + { + $this->userTerm = $userTerm; + $this->setTermId($userTerm->getId()); + } +} diff --git a/Factory/AdminEntityFactory.php b/Factory/AdminEntityFactory.php new file mode 100644 index 0000000..51e8133 --- /dev/null +++ b/Factory/AdminEntityFactory.php @@ -0,0 +1,32 @@ +connection->find( + Admin::class, + $id + ); + } + + public function findByUsername(string $username) : ?UserEntityInterface + { + return $this->connection->findOneBy( + Admin::class, + ['username' => $username] + ); + } +} diff --git a/Factory/CapabilityFactory.php b/Factory/CapabilityFactory.php new file mode 100644 index 0000000..eceee9e --- /dev/null +++ b/Factory/CapabilityFactory.php @@ -0,0 +1,41 @@ +getRepository( + Capability::class + )->find($identity); + } + + public function all( + EntityManagerInterface $entityManager + ) : iterable { + return $entityManager->getRepository( + Capability::class + )->findAll(); + } + + public function getCapabilityIdentities(EntityManagerInterface $entityManager) : array + { + return (array) $entityManager + ->getRepository(Capability::class) + ->createQueryBuilder('u') + ->select('u.identity') + ->getQuery() + ->getSingleColumnResult(); + } +} diff --git a/Factory/UserEntityFactory.php b/Factory/UserEntityFactory.php new file mode 100644 index 0000000..895fab7 --- /dev/null +++ b/Factory/UserEntityFactory.php @@ -0,0 +1,32 @@ +connection->find( + User::class, + $id + ); + } + + public function findByUsername(string $username) : ?UserEntityInterface + { + return $this->connection->findOneBy( + User::class, + ['username' => $username] + ); + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1fd273 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Languages/users-module.pot b/Languages/users-module.pot new file mode 100644 index 0000000..d22586f --- /dev/null +++ b/Languages/users-module.pot @@ -0,0 +1,26 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TrayDigita Users Module 1.0.0\n" +"POT-Creation-Date: 2023-10-16 00:01+0700\n" +"PO-Revision-Date: 2023-09-24 19:00+0700\n" +"Last-Translator: ArrayAccess\n" +"Language-Team: ArrayAccess\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4\n" +"X-Poedit-Basepath: ..\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: translate:1,3c;__;translate;trans;" +"translatePlural:1,2,5c;translatePlural:1,2;transN:1,2;" +"_n:1,2;translateContext:1,2c;transX:1,2c;_x:1,2c;" +"translatePluralContext:1,2,4c;transNX:1,2,4c;_nx:1,2,4c\n" +"X-Poedit-SearchPath-0: Entities\n" +"X-Poedit-SearchPath-1: Factory\n" +"X-Poedit-SearchPath-2: Media.php\n" +"X-Poedit-SearchPathExcluded-0: *.css\n" +"X-Poedit-SearchPathExcluded-1: *.html\n" +"X-Poedit-SearchPathExcluded-2: *.json\n" diff --git a/README.md b/README.md new file mode 100644 index 0000000..83d84e5 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +## TRAY DIGITA USERS MODULE + +Users modules for [Tray Digita](https://github.com/ArrayAccess/TrayDigita). + +This module for user & auth implementation + +> Entities + +See the [Entities Directory](Entities) diff --git a/Users.php b/Users.php new file mode 100644 index 0000000..80e7c8b --- /dev/null +++ b/Users.php @@ -0,0 +1,620 @@ + very important + */ + protected int $priority = PHP_INT_MIN; + + protected PermissionInterface $permission; + + private bool $didInit = false; + + const ADMIN_MODE = 'admin'; + + const USER_MODE = 'user'; + + private array $cookieNames = [ + self::USER_MODE => [ + 'name' => 'auth_user', + 'lifetime' => 0, + 'wildcard' => false + ], + self::ADMIN_MODE => [ + 'name' => 'auth_admin', + 'lifetime' => 0, + 'wildcard' => false + ] + ]; + + private ?User $userAccount = null; + + private ?Admin $adminAccount = null; + + private ?ServerRequestInterface $request = null; + + private bool $authProcessed = false; + + private string $currentMode = self::ADMIN_MODE; + + public function getName(): string + { + return $this->translateContext( + 'Users & Auth', + 'module', + 'users-module' + ); + } + + public function getDescription(): string + { + return $this->translateContext( + 'Core module that support users & authentication', + 'module', + 'users-module' + ); + } + + protected function doInit(): void + { + if ($this->didInit) { + return; + } + + $this->didInit = true; + foreach ($this->getTranslator()?->getAdapters()??[] as $adapter) { + if ($adapter instanceof PoMoAdapter) { + $adapter->registerDirectory( + __DIR__ .'/Languages', + 'users-module' + ); + } + } + + $this->doRegisterEntities(); + // $this->doResolvePermission(); + // $this->doResolveCookieName(); + $this->doAddMiddleware(); + // stop here if config error + if ($this->getKernel()?->getConfigError()) { + return; + } + $this->getManager()?->attach( + 'view.beforeRender', + [$this, 'viewBeforeRender'] + ); + $this->getManager()?->attach( + 'view.bodyAttributes', + [$this, 'viewBodyAttributes'] + ); + } + + /** + * Register entities + * @return void + */ + private function doRegisterEntities(): void + { + $metadata = ContainerHelper::use(Connection::class, $this->getContainer()) + ?->getDefaultConfiguration() + ->getMetadataDriverImpl(); + if ($metadata instanceof AttributeDriver) { + $metadata->addPaths([ + __DIR__ . '/Entities' + ]); + } + } + + private function viewBodyAttributes($attributes): array + { + $this->getManager()?->detach( + 'view.bodyAttributes', + [$this, 'viewBodyAttributes'] + ); + + $attributes = !is_array($attributes) ? $attributes : []; + $attributes['class'] = DataNormalizer::splitStringToArray($attributes['class']??null)??[]; + $user = $this->getUserAccount(); + $admin = $this->getAdminAccount(); + if (!$user && !$admin) { + return $attributes; + } + $attributes['data-user-logged-in'] = true; + return $attributes; + } + + /** @noinspection PhpUnusedParameterInspection */ + private function viewBeforeRender( + $path, + $parameters, + ViewInterface $view + ) { + $this->getManager()?->detach( + 'view.beforeRender', + [$this, 'viewBeforeRender'] + ); + $view->setParameter('user_user', $this->getUserAccount()); + $view->setParameter('admin_user', $this->getAdminAccount()); + return $path; + } + + private bool $permissionResolved = false; + + private function doResolvePermission(): self + { + if ($this->permissionResolved) { + return $this; + } + $this->permissionResolved = true; + $container = $this->getContainer(); + $connection = ContainerHelper::use(Connection::class, $container); + $manager = $this->getManager(); + if (!$container->has(CapabilityEntityFactoryInterface::class)) { + $container->set( + CapabilityEntityFactoryInterface::class, + static fn () => new CapabilityFactory() + ); + } + $hasPermission = $container->has(PermissionInterface::class); + if ($hasPermission) { + $permission = ContainerHelper::getNull( + PermissionInterface::class, + $container + ); + if (!$permission instanceof PermissionInterface) { + $container->remove(PermissionInterface::class); + $hasPermission = false; + } + } + if (!$hasPermission) { + $permission = new PermissionWrapper( + $connection, + $container, + $manager + ); + if ($container instanceof Container) { + $container->set(PermissionInterface::class, $permission); + } else { + $container->set(PermissionInterface::class, fn () => $permission); + } + } + + $permission ??= ContainerHelper::service( + PermissionInterface::class, + $container + ); + if (!$permission instanceof PermissionWrapper) { + $container->remove(PermissionInterface::class); + $permission = new PermissionWrapper( + $connection, + $container, + $manager, + $permission + ); + $container->set(PermissionInterface::class, $permission); + } + $this->permission = $permission; + if ($this->permission instanceof PermissionWrapper + && !$this->permission->getCapabilityEntityFactory() + ) { + $this->permission->setCapabilityEntityFactory(new CapabilityFactory()); + } + return $this; + } + + private function doAddMiddleware(): void + { + $container = $this->getContainer(); + ContainerHelper::use( + KernelInterface::class, + $container + )?->getHttpKernel()->addMiddleware( + new class($container, $this) extends AbstractMiddleware { + protected int $priority = PHP_INT_MAX - 10; + public function __construct( + ContainerInterface $container, + private readonly Users $auth + ) { + parent::__construct($container); + } + + protected function doProcess(ServerRequestInterface $request): ServerRequestInterface|ResponseInterface + { + $this->auth->setRequest($request); + return $request; + } + } + ); + } + + private bool $cookieResolved = false; + + /** + * @return self + */ + private function doResolveCookieName(): self + { + if ($this->cookieResolved) { + return $this; + } + + $this->cookieResolved = true; + $config = ContainerHelper::use(Config::class, $this->getContainer()); + $cookie = $config->get('cookie'); + if (!$cookie instanceof Config) { + $cookie = new Config(); + $config->set('cookie', $cookie); + } + foreach ($this->cookieNames as $key => $names) { + $cookieData = $cookie->get($key); + $cookieData = $cookieData instanceof Config + ? $cookieData + : new Config(); + // replace + $cookie->set($key, $cookieData); + $cookieName = $cookieData->get('name'); + $cookieName = is_string($cookieName) && trim($cookieName) !== '' + ? trim($cookieName) + : $names['name']; + $cookieName = preg_replace( + '~[^!#$%&\'*+-.^_`|\~a-z0-9]~i', + '', + $cookieName + ); + + $cookieName = $cookieName === '' ? $names['name'] : $cookieName; + $cookieLifetime = $cookieData->get('lifetime'); + $cookieLifetime = is_numeric($cookieLifetime) ? $cookieLifetime : 0; + $cookieLifetime = max((int) $cookieLifetime, 0); + $cookieWildcard = $cookieData->get('wildcard') === true; + $this->cookieNames[$key]['name'] = $cookieName; + $this->cookieNames[$key]['wildcard'] = $cookieWildcard; + $this->cookieNames[$key]['lifetime'] = $cookieLifetime; + } + + return $this; + } + + public function getRequest(): ?ServerRequestInterface + { + return $this->request; + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + public function isAuthProcessed(): bool + { + return $this->authProcessed; + } + + private function doProcessAuth(): self + { + if (!$this->request || $this->authProcessed) { + return $this; + } + + $this->authProcessed = true; + $container = $this->getContainer(); + $userAuth = ContainerHelper::service(UserAuth::class, $container); + + $request = $this->getManager()->dispatch('auth.request', $this->request); + $request = $request instanceof ServerRequestInterface + ? $request + : $this->request; + $userAuth->getHashIdentity()->setUserAgent( + $request->getHeaderLine('User-Agent') + ); + + $cookieNames = $this->getCookieNames(); + $cookieParams = $request->getCookieParams(); + $adminCookie = $cookieParams[$cookieNames[self::ADMIN_MODE]['name']]??null; + $adminCookie = !is_string($adminCookie) ? $adminCookie : null; + $userCookie = $cookieParams[$cookieNames[self::USER_MODE]['name']]??null; + $userCookie = is_string($userCookie) ? $userCookie : null; + + $this->userAccount = $userCookie ? $userAuth->getUser( + $userCookie, + $this->getUserEntityFactory() + ) : null; + $this->adminAccount = $adminCookie ? $userAuth->getUser( + $adminCookie, + $this->getAdminEntityFactory() + ) : null; + return $this; + } + + private function createEntityFactoryContainer(): self + { + $container = $this->getContainer(); + $hasUserEntity = $container->has(UserEntityFactory::class); + $hasAdminEntity = $container->has(AdminEntityFactory::class); + if ($hasUserEntity && $hasAdminEntity) { + return $this; + } + if ($container instanceof Container) { + if (!$hasUserEntity) { + $container->set(UserEntityFactory::class, UserEntityFactory::class); + } + if (!$hasUserEntity) { + $container->set(AdminEntityFactory::class, AdminEntityFactory::class); + } + return $this; + } + if (!$hasUserEntity) { + $container->set( + UserEntityFactory::class, + fn() => ContainerHelper::resolveCallable(UserEntityFactory::class, $container) + ); + } + if (!$hasAdminEntity) { + $container->set( + AdminEntityFactory::class, + fn() => ContainerHelper::resolveCallable(AdminEntityFactory::class, $container) + ); + } + return $this; + } + + public function getAdminEntityFactory() : AdminEntityFactory + { + try { + return $this + ->createEntityFactoryContainer() + ->getContainer() + ->get(AdminEntityFactory::class); + } catch (Throwable) { + return new AdminEntityFactory( + ContainerHelper::service(Connection::class, $this->getContainer()) + ); + } + } + + public function getUserEntityFactory() : UserEntityFactory + { + try { + return $this + ->createEntityFactoryContainer() + ->getContainer() + ->get(UserEntityFactory::class); + } catch (Throwable) { + return new UserEntityFactory( + ContainerHelper::service(Connection::class, $this->getContainer()) + ); + } + } + + public function getPermission(): PermissionInterface + { + $container = $this->doResolvePermission()->getContainer(); + $permission = ContainerHelper::service(PermissionInterface::class, $container); + return $permission instanceof PermissionWrapper + ? $permission + : $this->permission; + } + + public function setAsAdminMode(): void + { + $this->currentMode = self::ADMIN_MODE; + } + + public function setAsUserMode(): void + { + $this->currentMode = self::ADMIN_MODE; + } + + public function getCurrentMode(): string + { + return $this->currentMode; + } + + public function isLoggedIn() : bool + { + return match ($this->getCurrentMode()) { + self::ADMIN_MODE => $this->isAdminLoggedIn(), + self::USER_MODE => $this->isUserLoggedIn(), + default => false + }; + } + + public function getAccount() : User|Admin|null + { + return match ($this->getCurrentMode()) { + self::ADMIN_MODE => $this->getAdminAccount(), + self::USER_MODE => $this->getUserAccount(), + default => null + }; + } + + public function getUserAccount(): ?User + { + return $this->doProcessAuth()->userAccount; + } + + public function getAdminAccount(): ?Admin + { + return $this->doProcessAuth()->adminAccount; + } + + /** + * @return array{ + * user: array{name:string, lifetime: int, wildcard: bool}, + * admin: array{name:string, lifetime: int, wildcard: bool} + * } + */ + public function getCookieNames(): array + { + return $this->doResolveCookieName()->cookieNames; + } + + /** + * @param string $type + * @return ?array{name:string, lifetime: int, wildcard: bool} + */ + public function getCookieNameData(string $type): ?array + { + return $this->getCookieNames()[$type]??null; + } + + public function sendAuthCookie( + AbstractUser $userEntity, + ResponseInterface $response + ) : ResponseInterface { + $container = $this->getContainer(); + $userAuth = ContainerHelper::service(UserAuth::class, $container); + + if (!$userAuth instanceof UserAuth) { + throw new RuntimeException( + 'Can not determine use auth object' + ); + } + $cookieName = $userEntity instanceof Admin + ? 'admin' + : ($userEntity instanceof User ? 'user' : null); + $settings = $cookieName ? $this->getCookieNameData($cookieName) : null; + if ($settings === null) { + throw new RuntimeException( + 'Can not determine cookie type' + ); + } + $request = $this->request??ServerRequest::fromGlobals( + ContainerHelper::use( + ServerRequestFactory::class, + $container + ), + ContainerHelper::use( + StreamFactoryInterface::class, + $container + ) + ); + $domain = $request->getUri()->getHost(); + $newDomain = $this->getManager()?->dispatch( + 'auth.cookieDomain', + $domain + ); + + $domain = is_string($newDomain) && filter_var( + $newDomain, + FILTER_VALIDATE_DOMAIN + ) ? $newDomain : $domain; + + if ($settings['wildcard']) { + $domain = DataNormalizer::splitCrossDomain($domain); + } + $cookie = new SetCookie( + name: $settings['name'], + value: $userAuth->getHashIdentity()->generate($userEntity->getId()), + expiresAt: $settings['lifetime'] === 0 ? 0 : $settings['lifetime'] + time(), + path: '/', + domain: $domain + ); + $cookieObject = $this->getManager()?->dispatch( + 'auth.cookieObject', + $cookie + ); + $cookie = $cookieObject instanceof SetCookie + ? $cookieObject + : $cookie; + return $cookie->appendToResponse($response); + } + + public function isAdminLoggedIn(): bool + { + return $this->getAdminAccount() !== null; + } + + public function isUserLoggedIn(): bool + { + return $this->getUserAccount() !== null; + } + + /** + * @param int $id + * @return ?Admin + */ + public function getAdminById(int $id): ?UserEntityInterface + { + return $this->getAdminEntityFactory()->findById($id); + } + + /** + * @param string $username + * @return ?Admin + */ + public function getAdminByUsername(string $username): ?UserEntityInterface + { + return $this->getAdminEntityFactory()->findByUsername($username); + } + + /** + * @param int $id + * @return ?User + */ + public function getUserById(int $id): ?UserEntityInterface + { + return $this->getUserEntityFactory()->findById($id); + } + + /** + * @param string $username + * @return ?User + */ + public function getUserByUsername(string $username) : ?UserEntityInterface + { + return $this->getUserEntityFactory()->findByUsername($username); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5294eee --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "arrayaccess/traydigita-users-module", + "description": "Users module for tray digita", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "ArrayIterator", + "email": "arrayiterator@gmail.com", + "role": "developer" + } + ], + "require": { + "php": "^8.2|^8.3", + "ext-curl": "*", + "ext-json": "*", + "ext-pdo": "*", + "ext-openssl": "*", + "ext-pdo_mysql": "*", + "ext-fileinfo": "*" + }, + "require-dev": { + "squizlabs/php_codesniffer": "3.7.2", + "slevomat/coding-standard": "^8.13" + }, + "autoload": { + "psr-4": { + "ArrayAccess\\TrayDigita\\App\\Modules\\Users\\" : "" + } + }, + "config": { + "optimize-autoloader": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} \ No newline at end of file diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..6874ac2 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,59 @@ + + + TrayDigita Users Module Coding Standard + + + + + + + + + + + + + + + + + + + + + + + + Entities/ + Factory/ + Users.php + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file