UserManager
extends Manager
in package
uses
SaveTrait, AssetTrait, RatingThreadCreatorTrait, OwnedThreadDeleteTrait, CountryCodeDerivationTrait, UserAccountDeletionTrait, UserReferralTrait, UserContentRewardTrait
Base class of Api managers whose traits delegate to a typed `Service<TEntity>`.
Tags
Table of Contents
Constants
- ACCOUNT_DELETION_PAGE_SLUG : int = \App\Core\Routers\PageId::SYSTEM_ACCOUNT_DELETION
- Page "Account deletion" (`Page:form` with the `accountDeletion` component) — target of the e-mailed links.
- CONTENT_REWARD_ACTIONS : array<string|int, mixed> = [self::CONTENT_REWARD_BAZAAR_ADVERT, self::CONT...
- Actions rewarded for content, in the order of the admin screen.
- CONTENT_REWARD_BAZAAR_ADVERT : string = 'bazaarAdvert'
- CONTENT_REWARD_COMCAT_ADVERT : string = 'comcatAdvert'
- CONTENT_REWARD_DISCUSSION_MESSAGE : string = 'discussionMessage'
- CONTENT_REWARD_SKIP_MONTHLY_CAP : string = 'monthly_cap'
- CONTENT_REWARD_SKIP_USER_INACTIVE : string = 'user_inactive'
- PROFILE_REWARD_ACTION : string = 'profileComplete'
- Complete profile: once per account, `subject_id` = the user's own id.
- REFERRAL_SKIP_DISABLED : string = 'disabled'
- REFERRAL_SKIP_MONTHLY_CAP : string = 'monthly_cap'
- REFERRAL_SKIP_REFERRER_INACTIVE : string = 'referrer_inactive'
- REFERRAL_SKIP_SAME_IP : string = 'same_ip'
- DELETED_IDS_META : string = '__deletedIds'
- Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
- LOST_PASSWORD_PAGE_SLUG : int = \App\Core\Routers\PageId::SYSTEM_LOST_PASSWORD
- Page "Lost password" (`Page:form` with the `lostPasswordForm` component) — target of the reset link.
Properties
- $service : Service<string|int, TEntity>
- Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
- $appContext : AppContext
- $assetFormatRegistry : AssetFormatRegistry|null
- Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.
- $blacklistManager : BlacklistManager|null
- Blacklist check of a new account ({@see blacklistRefusal()}). A plain property with a default, not a promoted one: unit tests build the manager without the constructor and the check is then skipped.
- $cacheTagInvalidator : CacheTagInvalidator|null
- Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
- $defaultSkipFields : array<string|int, string>
- Privilege escalation guard. `User.userRoles` is many-to-many (the hydrator always does clear+add), so a client could assign roles through `data.userRoles` regardless of the admin path (`meta['roleIds']` handled by `UserRelationManager::syncRoles()` in `afterSave()`). Skipping the field makes `meta['roleIds']` the only way to change roles.
- $emailManager : EmailManager
- $internalCallContext : InternalCallContext|null
- Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).
- $languageContext : LanguageContext|null
- $nonInvalidatingColumns : array<string|int, string>
- Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.
- $paramsConfig : Config|null
- Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.
- $presenterContext : PresenterContext
- $settingContext : SettingContext
- $totalCount : int|null
- $useTransactionForDelete : bool
- $useTransactionForSave : bool
- $banStateListeners : UserBanStateListenerRegistry|null
- $countryManager : CountryManager
- $fieldAudienceContext : FieldAudienceContext|null
- $passwordHasher : PasswordHasher
- $paymentRegionManager : PaymentRegionManager
- $permissionResolver : PermissionResolver|null
- $ratingThreadManager : RatingThreadManager
- $rejectedUploads : array<int, array{name: string, reason: string}>
- New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.
- $userRefreshTokenManager : UserRefreshTokenManager
- $userRelationManager : UserRelationManager
- $userService : UserService
- $vatIdAutoCheckManager : VatIdAutoCheckManager
- $vatIdCheckManager : VatIdCheckManager
Methods
- __construct() : mixed
- activateUser() : Result
- Activates an account from the e-mailed link.
- anonymizeAccountRow() : Result
- Wipes the personal data of the account row (the row stays — orders, documents and posts refer to it). Placeholder e-mail/username free the originals for a new registration, the password becomes unusable, the account inactive; sessions are revoked and deleted.
- cancelAccountDeletion() : bool
- Compare-and-set cancel of the pending request the caller has read. `false` = refused (anonymized, claimed by a running anonymization, cancelled or requested again in the meantime) — nothing was written.
- claimAccountDeletion() : bool
- Atomic claim of the pending request the caller has just read (time + nonce). Only the run that gets `true` may take irreversible steps; it then owns the account by `$claimNonce` ({@see self::holdsAccountDeletionClaim()}).
- createFromExternalLogin() : Result
- Creates a user from an external (OAuth) login; called from {@see \App\UI\Api\System\Presenters\AuthPresenter::internalActionLoginExternal()}.
- creditPoints() : void
- Credits loyalty points of an order and recomputes the discount band (owner of `cms_system_users` and `cms_system_user_discount_levels`).
- debitCreditsForPayment() : CreditDebitOutcome
- Atomically debits the price of a credit payment, at most once per payment (credit payment gateway), see {@see \App\UI\Api\System\Models\Repositories\Db\UserRepository::debitCreditsForPayment()}.
- delete() : Result
- Refuses to delete the caller's own account and the last active holder of a protected role (`protect_last_holder`, currently the administrator), for a grid row, a bulk delete (the whole batch is refused, nothing is deleted) and a direct Api call.
- deleteAll() : Result
- Same gate for deleting by filters, applied to the ids the filter selects.
- deliverAccountDeletionConfirm() : Result
- `SY_ACCOUNT_DELETION_CONFIRM` — an account without a password confirms its deletion request by this link.
- deliverAccountDeletionRequested() : Result
- `SY_ACCOUNT_DELETION_REQUESTED` — "your account will be deleted on the scheduled date, if it was not you, cancel it: <link>". The only place the plain cancel token leaves the server.
- deliverActivationEmail() : Result
- Sends `SY_USER_ACTIVATION` to one specific (inactive) account, without a lookup by name.
- deliverExternalLinkConfirmation() : Result
- E-mails the owner of a password account the one-time link that confirms pairing it with the provider identity (`SY_EXTERNAL_LINK_CONFIRM`). The token is already stored (hashed) by {@see UserExternalLoginManager::requestLinkConfirmation()}; this is the only place the plain token leaves the server.
- emailExists() : bool
- Whether the e-mail is already used by another user (than `$excludeId`).
- find() : T|null
-
findAll()
: T> : list
>) - findBy() : T|null
- findEmailsByIds() : array<int, string>
- Batch lookup of e-mails by user id (owner of `cms_system_users`).
- findFresh() : User|null
- Current row straight from the DB (bypasses the query cache) — for one-time token flows.
- findRelatedIds() : array<int, int>|null
- Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).
- findRoleNames() : array<int, string>
- Returns role names of the user for the informational `role` JWT claim (login, token refresh, new pair after a password change, OAuth). The claim is not a source of permissions; {@see PermissionResolver} decides fresh from the DB.
- generateNewPassword() : Result
- Admin/internal password regeneration.
- getAccountDeletionGraceDays() : int
- Grace period from the setting `account.deletion.graceDays` (module 1), default 7 days.
- getAppContext() : AppContext
- getContentRewardCredits() : int
- Credits of the action; 0 = the action is not rewarded (also for an unknown action).
- getDefaultSkipFields() : array<string|int, string>
- Returns keys that the generic `service->save()` must not copy onto the managed entity.
- getEntityClass() : TEntity>
- Returns the root Doctrine entity class managed by this manager.
- getExternalMetaAllowlist() : array<string|int, string>
- Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).
- getExternalOwnerData() : array<string, mixed>
- Returns `data` keys that fill the owner from the server when an external save creates a record.
- getExternalRemoteIpData() : array<string, mixed>
- Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
- getExternalUpdateColumnsAllowlist() : array<string|int, string>
- Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
- getGraphDataKeys() : array<string|int, string>
- Returns the keys an external client may send nested in `data`.
- getLanguageContext() : LanguageContext|null
- getPresenterContext() : PresenterContext
- getReferralOverview() : array{enabled: bool, code: ?string, registered: int, activated: int, credits: int, rewardCredits: int}
- Referral data of the user for the "My account" page.
- getReferralRewardCredits() : int
- Credits of the referral reward; 0 = the affiliate program is off.
- getServiceClass() : class-string
- Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).
- getSettingContext() : SettingContext
- getSettings() : array<int, array<string, mixed>>
- getTotalCount() : int|null
- holdsAccountDeletionClaim() : bool
- Whether the run still owns the account (claim not lost).
- initializeAllContexts() : void
- isOwnedBy() : bool
- Whether `$id` is the caller's own account.
- isSignInAllowedNow() : bool
- Fresh scalar check of the stored row right before tokens are handed out.
- registerEvent() : Result
- resetPassword() : Result
- Sets a new password from the "lost password" link.
- rewardCompletedProfile() : void
- Marks the first complete profile of the account (one-way flag `profile_completed_at`) and pays `reward.profileComplete.credits` at that moment. Called after every user save and after the account activation.
- rewardPublishedContent() : void
- Pays the credits of the action to the author of a record that has just been published. Safe to call repeatedly for the same record (each save, moderation toggle or payment): only the first call records a decision.
- save() : Result
- sendActivationEmail() : Result
- sendNewPassword() : Result
- Whole "lost password" flow in one place.
- setAppContext() : $this
- setAssetFormatRegistry() : static
- setCacheTagInvalidator() : static
- setInternalCallContext() : static
- setLanguageContext() : $this
- setParamsConfig() : static
- setPresenterContext() : $this
- setSettingContext() : $this
- updateAllRelations() : Result
- updateColumn() : Result
- updateColumns() : Result
- IMPORTANT: `updateColumns()` does not run the save hooks (see `Core\Traits\Api\Models\Managers\SaveTrait`). Without this override a ban through the API would not cancel sessions and the sign-in gate could be bypassed by waiting for a token refresh. The `banned` column is writable through the API (`#[UpdatableColumn]`), so this path is real.
- updateOwnPassword() : Result
- Changes the password of the caller's own account.
- updatePasswordForUser() : Result
- Changes the password of another account: admin and system paths (currently the lazy rehash in {@see \App\UI\Api\System\Presenters\AuthPresenter::internalActionLogin()}).
- updateRelations() : Result
- usernameExists() : bool
- Whether the username is already used by another user (than `$excludeId`).
- afterSave() : Result|null
- beforeSave() : Result|null
- beginRejectedUploads() : array<int, array{name: string, reason: string}>
- Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
- bumpCacheTags() : void
- Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
- bumpCacheTagsExplicit() : void
- Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
- bumpCacheTagsForClass() : void
- Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
- classCacheTags() : array<string|int, string>
- Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.
- completedSave() : void
- Automatic VAT id check after a registration (`$meta['verifyVatId']`, sent by the Front registration form). Post-commit and best effort (SaveTrait `completedSave`): the registered account stays even when VIES is down, and the up to 3 s VIES call does not hold the save transaction. The registering visitor has no identity yet, so the Front cannot ask `VatIdCheck:verify` itself; the per-IP safety cap applies.
- createRatingThread() : Result|null
- Creates a new {@see RatingThread} of the given section and writes its id into `$column` of the entity `$entityId` (default `ratingThreadId`, a scalar FK; pass the association name for an association). Returns a failed {@see Result} (the caller may return it from `afterSave` to roll back), or null.
- deleteAsset() : void
- Deletes an asset file by its absolute path.
- deleteOwnedThreads() : Result|null
- deriveCountryText() : Result|null
- finishRejectedUploads() : void
- Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.
- getDeletedId() : int|null
- Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).
- getService() : Service<string|int, TEntity>
- Returns the service backing the manager traits.
-
ownedThreadIds()
: array{comment: list
, rating: list } - Reads the thread ids from the entities preloaded before the delete (`getCommentThreadId()` / `getRatingThreadId()`).
- readUploadedItem() : string|null
- Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.
- recordRejectedUpload() : void
- Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).
- saveAsset() : string|null
- Saves one base64 image; `null` means the image did not pass {@see ImageUploadPolicy}.
- saveAssets() : array<int, string>
- Saves base64 images into a directory, raster images only, per {@see ImageUploadPolicy}.
- assignReferralCode() : void
- attachReferrerByCode() : void
- Links a new account to the owner of the referral code. An unknown, malformed or own code is ignored; the registration itself always goes on.
- blacklistRefusal() : Result|null
- A new account of a non-privileged caller (front registration, another client) is refused when its e-mail, phone or registration IP (`meta.ip`, sent by the front; external meta is stripped) is on the blacklist. The front form checks the same values first and reports them at the fields; this is the gate for every other path ({@see BlacklistManager::refusalFor()} skips the administrator).
- buildAccountDeletionLink() : string|null
- bumpDeleteCacheTags() : void
- Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
- callerCanSetRoles() : bool|null
- Whether the caller may set roles (`:Api:System:User:setRoles`). `null` means the audience was not resolved (cron/CLI calls the manager directly); internal code without a caller is not checked. The identity comes only from the Api gate ({@see FieldAudienceContext}), never from data. Fail-closed without the resolver.
- captureDeletedIds() : WeakMap<object, int>
- Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.
- changedContactBlacklistRefusal() : Result|null
- Profile edit: a non-privileged caller may not change the e-mail or phone to a blacklisted value. Only the changed values are checked, so an account whose current value was blacklisted later can still save the rest.
- columnsInvalidateCache() : bool
- contentRewardSince() : DateTimeImmutable|null
- Moment the reward of the action was switched on; null = never (nothing is rewarded).
- contentRewardSkipReason() : string|null
- deactivatesAccount() : bool
- Whether `updateColumns` writes a state in which the account stops being active.
- deleteBatch() : Result
- Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.
- deliverPasswordResetLink() : Result
- Generates and stores the token and sends `SY_LOST_PASSWORD` with the link to one specific account.
- derivePaymentRegionLink() : Result|null
- `regionId` is derived from the payment region; the front and the admin form fill it in themselves and never offer it separately. An external `save` could send one that does not match the payment region, so it is always aligned here, whatever path or value the caller used: - no payment region: both are null (same as the front when no region is found), - an unknown payment region: an error (otherwise the save would fail on the FK).
- fieldTakenByOther() : bool
- findByUsernameOrEmail() : User|null
- Finds an account by username or e-mail; the single implementation, shared by {@see self::sendActivationEmail()} and {@see self::sendNewPassword()}.
- getUserImageDir() : string
- guardedDeactivation() : Result
- guardedDelete() : Result
- invalidateCredentialsAfterPasswordChange() : Result
- After a password change by a path other than the own change or reset (admin, {@see self::generateNewPassword()}): invalidates all refresh tokens and issued access tokens of the account (same method and reason as {@see self::updateOwnPassword()}) and clears a pending "lost password" link.
- isActiveState() : bool
- Whether the account stays active after saving in the sense of protected roles (can sign in).
- isAssetPathAllowed() : bool
- Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
- issueActivationLink() : string|null
- Generates a new activation key, stores its sha256 and expiry (72 h) and returns the link with the readable key (the only place the readable key exists). Returns `null` when the account has no id or e-mail, no presenter is available for the link, or the key could not be stored.
- loadEntitiesForCacheTags() : array<int, object>
- Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.
- notifyBanState() : void
- Tells the content modules the resulting ban state of the account (idempotent, like {@see revokeSessionsIfBlocked()}); they hide or restore the user's public content.
- onboardNewAccount() : void
- Steps after a new account got its code: links the referrer and, for an account already active, pays the rewards right away. Fail-open.
- positiveIds() : array<int, int>
- processAvatar() : Result|null
- referralSkipReason() : string|null
- resolveAvatarAbsPath() : string|null
- Converts the stored relative avatar path to an absolute disk path.
- restoreTokenIfStillIssued() : void
- Called when the e-mail with the new link was not queued, so the user holds only the previous one: restores the previous hash and expiry. Conditional: only while the row still carries the hash issued here, so a concurrent successful resend (another hash) is not overwritten. `$previous` comes from the state loaded from the DB before issuing (Api `findBy`/`find` reads the DB, not the query cache).
- revokeSessionsIfBlocked() : void
- Invalidates all refresh tokens of a blocked account. It deliberately looks only at the resulting state, not at the transition: this is idempotent (re-saving a banned account has nothing to revoke) and avoids needing the previous value, which the Doctrine identity map may already have overwritten.
- rewardActivatedAccount() : void
- Registration bonus for the account and the reward for its referrer, once the account is active.
- roleChangeRefusal() : Result|null
- Applies the role and account state gates ({@see UserRolePolicy::assignmentRefusal()}, {@see UserRolePolicy::lastHolderRefusal()}) to data from the DB.
- rollbackBatch() : void
- saveWithHooks() : Result
- Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
- sendAccountDeletionEmail() : Result
- sendRegistrationEmails() : void
- updateColumnsTargetIds() : array<int, int>
- Returns account ids from the `updateColumns` argument (an id, a list of ids, or rows `[0 => id, …]`).
- userTypeDataRefusal() : Result|null
- The user type may require company data (reg. id and company name) and the VAT id (`requires_company_data`, `requires_vat_id`); the front forms (registration, profile) require them, a direct Api save did not. Only a non-privileged caller is checked (the administrator may save an incomplete account), and only when the save carries the user type.
- usesAssetPathTrait() : bool
- writePassword() : Result
- The only password write implementation; the public variants above differ only in contract.
Constants
ACCOUNT_DELETION_PAGE_SLUG
Page "Account deletion" (`Page:form` with the `accountDeletion` component) — target of the e-mailed links.
public
int
ACCOUNT_DELETION_PAGE_SLUG
= \App\Core\Routers\PageId::SYSTEM_ACCOUNT_DELETION
CONTENT_REWARD_ACTIONS
Actions rewarded for content, in the order of the admin screen.
public
array<string|int, mixed>
CONTENT_REWARD_ACTIONS
= [self::CONTENT_REWARD_BAZAAR_ADVERT, self::CONTENT_REWARD_COMCAT_ADVERT, self::CONTENT_REWARD_DISCUSSION_MESSAGE]
CONTENT_REWARD_BAZAAR_ADVERT
public
string
CONTENT_REWARD_BAZAAR_ADVERT
= 'bazaarAdvert'
CONTENT_REWARD_COMCAT_ADVERT
public
string
CONTENT_REWARD_COMCAT_ADVERT
= 'comcatAdvert'
CONTENT_REWARD_DISCUSSION_MESSAGE
public
string
CONTENT_REWARD_DISCUSSION_MESSAGE
= 'discussionMessage'
CONTENT_REWARD_SKIP_MONTHLY_CAP
public
string
CONTENT_REWARD_SKIP_MONTHLY_CAP
= 'monthly_cap'
CONTENT_REWARD_SKIP_USER_INACTIVE
public
string
CONTENT_REWARD_SKIP_USER_INACTIVE
= 'user_inactive'
PROFILE_REWARD_ACTION
Complete profile: once per account, `subject_id` = the user's own id.
public
string
PROFILE_REWARD_ACTION
= 'profileComplete'
REFERRAL_SKIP_DISABLED
public
string
REFERRAL_SKIP_DISABLED
= 'disabled'
REFERRAL_SKIP_MONTHLY_CAP
public
string
REFERRAL_SKIP_MONTHLY_CAP
= 'monthly_cap'
REFERRAL_SKIP_REFERRER_INACTIVE
public
string
REFERRAL_SKIP_REFERRER_INACTIVE
= 'referrer_inactive'
REFERRAL_SKIP_SAME_IP
public
string
REFERRAL_SKIP_SAME_IP
= 'same_ip'
DELETED_IDS_META
Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
protected
string
DELETED_IDS_META
= '__deletedIds'
Read it only through getDeletedId().
LOST_PASSWORD_PAGE_SLUG
Page "Lost password" (`Page:form` with the `lostPasswordForm` component) — target of the reset link.
private
int
LOST_PASSWORD_PAGE_SLUG
= \App\Core\Routers\PageId::SYSTEM_LOST_PASSWORD
Properties
$service
Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
public
Service<string|int, TEntity>
$service
$appContext
protected
AppContext
$appContext
$assetFormatRegistry
Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.
protected
AssetFormatRegistry|null
$assetFormatRegistry
= null
Set by a decorator (decorators.neon) like setParamsConfig(): more than ten managers
process assets and each has its own constructor, so constructor injection would touch all
of them.
$blacklistManager
Blacklist check of a new account ({@see blacklistRefusal()}). A plain property with a default, not a promoted one: unit tests build the manager without the constructor and the check is then skipped.
protected
BlacklistManager|null
$blacklistManager
= null
$cacheTagInvalidator
Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
protected
CacheTagInvalidator|null
$cacheTagInvalidator
= null
Set by the decorator (decorators.neon) like setParamsConfig(). Nullable and
fail-open: without wiring (or without Redis) a bump is a no-op.
$defaultSkipFields
Privilege escalation guard. `User.userRoles` is many-to-many (the hydrator always does clear+add), so a client could assign roles through `data.userRoles` regardless of the admin path (`meta['roleIds']` handled by `UserRelationManager::syncRoles()` in `afterSave()`). Skipping the field makes `meta['roleIds']` the only way to change roles.
protected
array<string|int, string>
$defaultSkipFields
= ['userRoles', 'ratingThreadId']
ratingThreadId is the scalar FK to the profile rating thread. Skipping it means the
object-graph save never overwrites it: a client must not redirect the profile to a
foreign thread (taking over its ratings) or clear it. Only afterSave() creates or
heals it, through updateColumn().
$emailManager read-only
protected
EmailManager
$emailManager
$internalCallContext
Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).
protected
InternalCallContext|null
$internalCallContext
= null
Only ApiBridge::internalCall() sets it, never the payload and not
the RBAC guard, so the emergency apiRbac.internalMode: off cannot disable it. Set by a
decorator like setAssetFormatRegistry(). Without it (direct manager call from cron or a
test) the call counts as external.
$languageContext
protected
LanguageContext|null
$languageContext
= NULL
$nonInvalidatingColumns
Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.
protected
array<string|int, string>
$nonInvalidatingColumns
= ['views', 'viewsList', 'viewsDetail']
Covers all view counters across modules: views (Blog Article), viewsList (list
impression of Advert/Message) and viewsDetail (detail view of Advert/Message,
registerEvent() in the *DetailTraits). IMPORTANT: viewsDetail must stay in the
list, otherwise every open of an advert/post detail would invalidate all lists of the
class.
$paramsConfig
Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.
protected
Config|null
$paramsConfig
= null
Write logic (asset directories) reads paths from here, not from getSettings() or the
setting context, which may be filled by the client without server paths. The property
cannot be named $config: it collides with the promoted $config in some managers.
$presenterContext
protected
PresenterContext
$presenterContext
$settingContext
protected
SettingContext
$settingContext
$totalCount
protected
int|null
$totalCount
= null
$useTransactionForDelete
protected
bool
$useTransactionForDelete
= true
$useTransactionForSave
protected
bool
$useTransactionForSave
= true
$banStateListeners read-only
private
UserBanStateListenerRegistry|null
$banStateListeners
= null
$countryManager read-only
private
CountryManager
$countryManager
$fieldAudienceContext read-only
private
FieldAudienceContext|null
$fieldAudienceContext
= null
$passwordHasher read-only
private
PasswordHasher
$passwordHasher
$paymentRegionManager read-only
private
PaymentRegionManager
$paymentRegionManager
$permissionResolver read-only
private
PermissionResolver|null
$permissionResolver
= null
$ratingThreadManager read-only
private
RatingThreadManager
$ratingThreadManager
$rejectedUploads
New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.
private
array<int, array{name: string, reason: string}>
$rejectedUploads
= []
$userRefreshTokenManager read-only
private
UserRefreshTokenManager
$userRefreshTokenManager
$userRelationManager read-only
private
UserRelationManager
$userRelationManager
$userService read-only
private
UserService
$userService
$vatIdAutoCheckManager read-only
private
VatIdAutoCheckManager
$vatIdAutoCheckManager
$vatIdCheckManager read-only
private
VatIdCheckManager
$vatIdCheckManager
Methods
__construct()
public
__construct(UserService $userService, EmailManager $emailManager, PasswordHasher $passwordHasher, UserRelationManager $userRelationManager, RatingThreadManager $ratingThreadManager, UserRefreshTokenManager $userRefreshTokenManager, CountryManager $countryManager, VatIdAutoCheckManager $vatIdAutoCheckManager, VatIdCheckManager $vatIdCheckManager, PaymentRegionManager $paymentRegionManager[, FieldAudienceContext|null $fieldAudienceContext = null ][, PermissionResolver|null $permissionResolver = null ][, UserBanStateListenerRegistry|null $banStateListeners = null ][, BlacklistManager|null $blacklistManager = null ]) : mixed
Parameters
- $userService : UserService
- $emailManager : EmailManager
- $passwordHasher : PasswordHasher
- $userRelationManager : UserRelationManager
- $ratingThreadManager : RatingThreadManager
- $userRefreshTokenManager : UserRefreshTokenManager
- $countryManager : CountryManager
- $vatIdAutoCheckManager : VatIdAutoCheckManager
- $vatIdCheckManager : VatIdCheckManager
- $paymentRegionManager : PaymentRegionManager
- $fieldAudienceContext : FieldAudienceContext|null = null
- $permissionResolver : PermissionResolver|null = null
- $banStateListeners : UserBanStateListenerRegistry|null = null
- $blacklistManager : BlacklistManager|null = null
activateUser()
Activates an account from the e-mailed link.
public
activateUser(string $email, string $activationKey) : Result
Contract:
- the account is looked up by the e-mail from the link; the key is compared with
hash_equals()(constant time, the key is a one-time secret, not a public id), - an empty
activationKeyon the account never matches, even if the client sends an empty string: that is the "already activated / key consumed" state, - success consumes (clears) the key, the link is one-time and reuse ends with
USER_ACTIVATION_KEY_INVALID, - an already active account returns
USER_ALREADY_ACTIVE(not a key error), so a user clicking the link twice sees "already done", not "invalid link".
IMPORTANT: an unknown e-mail and a wrong key share the single code
USER_ACTIVATION_KEY_INVALID; distinguishing them would make the action an oracle on
account existence (same principle as the constant response of self::sendNewPassword()).
Parameters
- $email : string
- $activationKey : string
Return values
ResultanonymizeAccountRow()
Wipes the personal data of the account row (the row stays — orders, documents and posts refer to it). Placeholder e-mail/username free the originals for a new registration, the password becomes unusable, the account inactive; sessions are revoked and deleted.
public
anonymizeAccountRow(User $user, DateTimeInterface $now, string $claimNonce) : Result
The avatar file is removed only after its path was cleared.
IMPORTANT: one conditional UPDATE guarded by the claim of the run ($claimNonce). A
cancel that won the race, a new request or another run that took over a stale claim
leaves the row untouched and the result is INVALID_STATE.
Parameters
- $user : User
- $now : DateTimeInterface
- $claimNonce : string
Return values
ResultcancelAccountDeletion()
Compare-and-set cancel of the pending request the caller has read. `false` = refused (anonymized, claimed by a running anonymization, cancelled or requested again in the meantime) — nothing was written.
public
cancelAccountDeletion(User $user, DateTimeInterface $now) : bool
Parameters
- $user : User
- $now : DateTimeInterface
Return values
boolclaimAccountDeletion()
Atomic claim of the pending request the caller has just read (time + nonce). Only the run that gets `true` may take irreversible steps; it then owns the account by `$claimNonce` ({@see self::holdsAccountDeletionClaim()}).
public
claimAccountDeletion(User $user, string $claimNonce, DateTimeInterface $now) : bool
Parameters
- $user : User
- $claimNonce : string
- $now : DateTimeInterface
Return values
boolcreateFromExternalLogin()
Creates a user from an external (OAuth) login; called from {@see \App\UI\Api\System\Presenters\AuthPresenter::internalActionLoginExternal()}.
public
createFromExternalLogin(User $user[, string|null $referralCode = null ]) : Result
IMPORTANT: deliberately does not call self::save(). Its beforeSave()/afterSave()
hooks would send the registration and activation e-mails on an OAuth registration. The
entity goes through the own Service, Mapper and Repository chain, where save() on an
unpersisted entity does persist + flush.
Exceptions are the profile rating thread and the default roles (set_after_registration):
since afterSave() does not run here, they are created explicitly below (fail-open).
Roles are written set-based outside the entity; the presenter builds the role JWT
claim from self::findRoleNames() (reads links from the DB), not from the entity.
Parameters
- $user : User
- $referralCode : string|null = null
Return values
ResultcreditPoints()
Credits loyalty points of an order and recomputes the discount band (owner of `cms_system_users` and `cms_system_user_discount_levels`).
public
creditPoints(Connection $conn, int $userId, int $points) : void
IMPORTANT: $conn is the caller's connection and the method never opens its own
transaction. The shop credits points in one mini-transaction together with the one-way
gate orders.user_is_updated; only that atomicity makes the credit happen exactly once.
Same contract as EmailQueueManager::enqueue().
No hooks and no cache bump; it is a counter, not a user edit.
Parameters
- $conn : Connection
- $userId : int
- $points : int
debitCreditsForPayment()
Atomically debits the price of a credit payment, at most once per payment (credit payment gateway), see {@see \App\UI\Api\System\Models\Repositories\Db\UserRepository::debitCreditsForPayment()}.
public
debitCreditsForPayment(int $userId, int $paymentId, int $amount) : CreditDebitOutcome
On success the user and payment caches are bumped like updateColumn(), because lists and the
profile read the balance and the payment date from the query cache.
Parameters
- $userId : int
- $paymentId : int
- $amount : int
Return values
CreditDebitOutcomedelete()
Refuses to delete the caller's own account and the last active holder of a protected role (`protect_last_holder`, currently the administrator), for a grid row, a bulk delete (the whole batch is refused, nothing is deleted) and a direct Api call.
public
delete(int|array<string|int, mixed> $id[, array<string, mixed> $meta = [] ]) : Result
The check and the delete run in one transaction and holders are read with a locking
query, so two concurrent deletes of the last two administrators serialize and the second
is refused.
A bulk delete with a non-existent id stays a partial success: the data carry
deletedCount/totalCount (+ errors) and the admin reports how many were deleted.
Parameters
- $id : int|array<string|int, mixed>
- $meta : array<string, mixed> = []
Return values
ResultdeleteAll()
Same gate for deleting by filters, applied to the ids the filter selects.
public
deleteAll([array<string|int, mixed> $filters = [] ]) : Result
Parameters
- $filters : array<string|int, mixed> = []
Return values
ResultdeliverAccountDeletionConfirm()
`SY_ACCOUNT_DELETION_CONFIRM` — an account without a password confirms its deletion request by this link.
public
deliverAccountDeletionConfirm(User $user, string $plainToken) : Result
Parameters
- $user : User
- $plainToken : string
Return values
ResultdeliverAccountDeletionRequested()
`SY_ACCOUNT_DELETION_REQUESTED` — "your account will be deleted on the scheduled date, if it was not you, cancel it: <link>". The only place the plain cancel token leaves the server.
public
deliverAccountDeletionRequested(User $user, DateTimeInterface $scheduledAt, string $plainToken) : Result
Parameters
- $user : User
- $scheduledAt : DateTimeInterface
- $plainToken : string
Return values
ResultdeliverActivationEmail()
Sends `SY_USER_ACTIVATION` to one specific (inactive) account, without a lookup by name.
public
deliverActivationEmail(User $user) : Result
Also called by OAuth sign-in for a freshly created account with an unconfirmed e-mail (AuthPresenter::internalActionLoginExternal()), where a lookup through the query cache might not see the new row yet.
Every send issues a new key (the old readable key cannot be recovered from the hash), so only the last sent link is valid.
Parameters
- $user : User
Return values
ResultdeliverExternalLinkConfirmation()
E-mails the owner of a password account the one-time link that confirms pairing it with the provider identity (`SY_EXTERNAL_LINK_CONFIRM`). The token is already stored (hashed) by {@see UserExternalLoginManager::requestLinkConfirmation()}; this is the only place the plain token leaves the server.
public
deliverExternalLinkConfirmation(User $user, string $provider, string $plainToken) : Result
Parameters
- $user : User
- $provider : string
- $plainToken : string
Return values
ResultemailExists()
Whether the e-mail is already used by another user (than `$excludeId`).
public
emailExists(string $email[, int|null $excludeId = null ]) : bool
Parameters
- $email : string
- $excludeId : int|null = null
Return values
boolfind()
public
find(int $id[, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
- $id : int
- $includes : string|array<string|int, mixed> = []
Return values
T|nullfindAll()
public
findAll([array<string|int, mixed>|null $includes = [] ][, array<string|int, mixed>|null $filters = [] ][, array<string|int, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ]) : T> : list>)
Parameters
- $includes : array<string|int, mixed>|null = []
- $filters : array<string|int, mixed>|null = []
- $orderBys : array<string|int, mixed>|null = null
- $offset : int|null = null
- $limit : int|null = null
- $columns : array<string|int, mixed>|null = null
- $computeTotalCount : bool = false
Return values
T> : listentities, or raw rows when $columns is set
findBy()
public
findBy([array<string|int, mixed>|null $filters = [] ][, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
- $filters : array<string|int, mixed>|null = []
- $includes : string|array<string|int, mixed> = []
Return values
T|nullfindEmailsByIds()
Batch lookup of e-mails by user id (owner of `cms_system_users`).
public
findEmailsByIds(array<string|int, int> $userIds) : array<int, string>
Module managers (e.g. watchdogs) take user ids from their own tables and get the recipient e-mail fallback here in one query instead of joining a foreign table.
A user without an e-mail is missing from the map, so the caller can tell undeliverable recipients.
Parameters
- $userIds : array<string|int, int>
Return values
array<int, string> —user_id => email
findFresh()
Current row straight from the DB (bypasses the query cache) — for one-time token flows.
public
findFresh(int $userId) : User|null
Parameters
- $userId : int
Return values
User|nullfindRelatedIds()
Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).
public
findRelatedIds(int $parentId, string $property, string $targetAssociation) : array<int, int>|null
Parameters
- $parentId : int
- $property : string
- $targetAssociation : string
Return values
array<int, int>|null —null when it cannot be determined (collection mapping missing / database error)
findRoleNames()
Returns role names of the user for the informational `role` JWT claim (login, token refresh, new pair after a password change, OAuth). The claim is not a source of permissions; {@see PermissionResolver} decides fresh from the DB.
public
findRoleNames(int $userId) : array<int, string>
IMPORTANT: do not use User::getUserRoles() for this, it returns an unmapped (empty) collection.
Parameters
- $userId : int
Return values
array<int, string>generateNewPassword()
Admin/internal password regeneration.
public
generateNewPassword(int $userId) : Result
IMPORTANT: returns the plaintext password to the caller and sends no e-mail, so the admin
can hand it to the user through another channel. It is not the "lost password" path
(that is self::sendNewPassword()). Externally the key
:Api:System:User:generateNewPassword must be granted only to the admin role;
granting it to guest lets anyone reset any password.
Parameters
- $userId : int
Return values
ResultgetAccountDeletionGraceDays()
Grace period from the setting `account.deletion.graceDays` (module 1), default 7 days.
public
getAccountDeletionGraceDays() : int
Return values
intgetAppContext()
public
getAppContext() : AppContext
Return values
AppContextgetContentRewardCredits()
Credits of the action; 0 = the action is not rewarded (also for an unknown action).
public
getContentRewardCredits(string $action) : int
Parameters
- $action : string
Return values
intgetDefaultSkipFields()
Returns keys that the generic `service->save()` must not copy onto the managed entity.
public
getDefaultSkipFields() : array<string|int, string>
The manager's own afterSave()/beforeSave() sync trait handles them through $meta,
not through the object graph. Backed by an optional protected per-manager property.
The write Api endpoint (SaveTrait::internalActionSave())
also uses this accessor to strip the same keys from $data before hydration, because
defaultSkipFields alone does not protect hydration.
Return values
array<string|int, string>getEntityClass()
Returns the root Doctrine entity class managed by this manager.
public
getEntityClass() : TEntity>
The write Api endpoint (SaveTrait) takes it from here instead of the request, so a client cannot choose the instantiated PHP class.
Return values
TEntity>getExternalMetaAllowlist()
Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).
public
getExternalMetaAllowlist() : array<string|int, string>
The internal path (ApiBridge::internalCall() calling internalActionSave() directly, admin and
front) is not limited by this allowlist and reads $meta unchanged. The default is an
empty array, so external meta is dropped entirely; actionSave() is reachable through
HTTP routing regardless of ApiBridge, hence the strip. Override per manager with the
protected property $externalMetaAllowlist, analogous to self::getDefaultSkipFields().
Return values
array<string|int, string>getExternalOwnerData()
Returns `data` keys that fill the owner from the server when an external save creates a record.
public
getExternalOwnerData(int $userId) : array<string, mixed>
The default [] means the domain has no owner or must not be created through external
save (Order uses the dedicated checkout action, User registers another way). On
INSERT there is nothing to own, so the owner cannot be verified, only determined: the owner
field is #[ExternalReadonly] (the strip removes it from the payload) and
SaveTrait::applyExternalOwner() then fills it from
the JWT, so the client value never reaches hydration.
IMPORTANT: return the key that matches the column mapping, otherwise the owner is silently not written:
Watchdog/Comment/Rating/Message: scalar column,['userId' => $userId];Advert(Bazaar/Comcat): mapped column,['insertUserId' => $userId];- Bazaar
Response/AdvertFollower: the scalaruserIdhas an#[ORM\AttributeOverride]withinsertable: false, updatable: falsein the Api leaf and the column belongs to the association$user, so['user' => ['id' => $userId]]; ComcatResponsehas no$userassociation, so there it is['userId' => $userId]; Complaint: the owner is the association$user,['user' => ['id' => $userId]].
['id' => X] is a reference that hydrateForPersist() looks up in the DB
(OrmEntityHydrator::isReference()). Called only on the
external path and only on INSERT; on UPDATE it would make whoever saves a foreign record
its owner.
Parameters
- $userId : int
-
identity from the JWT (never from the request)
Return values
array<string, mixed>getExternalRemoteIpData()
Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
public
getExternalRemoteIpData(string $ip) : array<string, mixed>
The IP field is #[ExternalReadonly], so the external strip removes it and the client
cannot forge it. Without filling, an external insert would have no IP and the
"anonymous by IP" deduplication and per-IP manager limits would silently stop working.
SaveTrait::applyExternalRemoteIp() fills it from
IRequest::getRemoteAddress(), never from the request. The default [] means the entity
carries no IP; overrides return the entity field name (['insertIp' => $ip]).
Parameters
- $ip : string
Return values
array<string, mixed>getExternalUpdateColumnsAllowlist()
Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
public
getExternalUpdateColumnsAllowlist() : array<string|int, string>
updateColumn on a to-many relation is DELETE-all plus INSERT, so without this filter an
external POST with values={userRoles: [...]} would bypass the strip/guard of the save
path (privilege escalation). The internal path (ApiBridge::internalCall() calling
internalActionUpdateColumns() directly) is not limited. The default is an empty array, so
external updateColumns is rejected entirely. Override with the protected property
$externalUpdateColumnsAllowlist.
Return values
array<string|int, string>getGraphDataKeys()
Returns the keys an external client may send nested in `data`.
public
getGraphDataKeys() : array<string|int, string>
Before hydration the write Api endpoint
(SaveTrait::internalActionSave()) moves them from $data to
$meta, where the manager's own afterSave()/beforeSave() sync trait reads them (the
same mechanism as internal meta). When the client also sends the same key explicitly in
meta, meta wins (compatibility with admin, e.g. the categoryRels collision on
Advert). Backed by an optional protected per-manager property, like
self::getDefaultSkipFields().
IMPORTANT: promoting a key here does not make its content safe to write as received. When
the key carries a sensitive field that the server otherwise dictates (typically a payment
or approval flag), the manager must overwrite or verify it server-side on the external
path in beforeSave()/afterSave() before its sync trait reads it; otherwise the client
sets it itself (privilege escalation). The external path is recognized by
$meta['__external'] === true, which only SaveTrait::actionSave() sets (never
internalActionSave() alone), so internal calls never get the marker. Example: Bazaar
Advert.categoryRels is persisted through the object graph (owned collection outside
$graphDataKeys); a client could mark paid itself and get a paid advert without
payment, so AdvertManager::beforeSave() overwrites it through
CategoryRelsPaidSanitizationTrait::sanitizeExternalCategoryRelsPaid() before flush.
Return values
array<string|int, string>getLanguageContext()
public
getLanguageContext() : LanguageContext|null
Return values
LanguageContext|nullgetPresenterContext()
public
getPresenterContext() : PresenterContext
Return values
PresenterContextgetReferralOverview()
Referral data of the user for the "My account" page.
public
getReferralOverview(int $userId) : array{enabled: bool, code: ?string, registered: int, activated: int, credits: int, rewardCredits: int}
Parameters
- $userId : int
Return values
array{enabled: bool, code: ?string, registered: int, activated: int, credits: int, rewardCredits: int}getReferralRewardCredits()
Credits of the referral reward; 0 = the affiliate program is off.
public
getReferralRewardCredits() : int
Return values
intgetServiceClass()
Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).
public
getServiceClass() : class-string
The cache is controlled at the Api presenter boundary through this identity.
Return values
class-stringgetSettingContext()
public
getSettingContext() : SettingContext
Return values
SettingContextgetSettings()
public
getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>getTotalCount()
public
getTotalCount() : int|null
Return values
int|nullholdsAccountDeletionClaim()
Whether the run still owns the account (claim not lost).
public
holdsAccountDeletionClaim(int $userId, string $claimNonce) : bool
Parameters
- $userId : int
- $claimNonce : string
Return values
boolinitializeAllContexts()
public
initializeAllContexts() : void
isOwnedBy()
Whether `$id` is the caller's own account.
public
isOwnedBy(int $id, int $userId) : bool
The owner carrier here is the primary key itself (cms_system_users.user_id), there is
no userId field to compare, so the method runs no DB query and compares the identity
from the token with the target id. This is not a tautology: $userId comes only from
the signed JWT (RbacCheckTrait) and $id from
the payload, so two different sides are compared.
NOTE: creating a user through the external save stays closed: an INSERT has no
data.id, so verifyOwnership() in the presenter returns false. Registration is a
different path.
Parameters
- $id : int
- $userId : int
Return values
boolisSignInAllowedNow()
Fresh scalar check of the stored row right before tokens are handed out.
public
isSignInAllowedNow(int $userId) : bool
Parameters
- $userId : int
Return values
boolregisterEvent()
public
registerEvent(array<string|int, int> $ids, class-string $entityClass, string $property) : Result
Parameters
- $ids : array<string|int, int>
- $entityClass : class-string
-
entity used to translate the property into a column in the expression
- $property : string
Return values
ResultresetPassword()
Sets a new password from the "lost password" link.
public
resetPassword(string $email, string $key, string $newPassword) : Result
Contract:
- the account is looked up by the e-mail from the link, the token is compared with the
sha256 in the DB by PasswordResetToken::matches() (
hash_equals, expiry), - an unknown e-mail and a wrong, expired or already used token return one code,
USER_PASSWORD_RESET_KEY_INVALID; the action is public and must not be an account oracle, - a short password (
PasswordPolicy::USER_MIN_LENGTH) returnsINVALID_LENGTHand does not consume the token (a typo must not kill the link); it is checked only after the token so the length is not an oracle, - success writes the password hash, clears the token (the link is one-time) and invalidates all refresh tokens of the account, so a stolen session is dropped.
Parameters
- $email : string
- $key : string
- $newPassword : string
Return values
ResultrewardCompletedProfile()
Marks the first complete profile of the account (one-way flag `profile_completed_at`) and pays `reward.profileComplete.credits` at that moment. Called after every user save and after the account activation.
public
rewardCompletedProfile(int $userId) : void
IMPORTANT: the flag is set at the first complete save even while the reward is off (0 credits), and it is never cleared: a profile completed once never earns credits, not after a later switch-on, nor after it is emptied and filled in again. Profiles complete before the feature got the flag by the migration. An account that cannot sign in yet is left alone, so a profile filled in the registration form is flagged and paid after the activation.
Parameters
- $userId : int
rewardPublishedContent()
Pays the credits of the action to the author of a record that has just been published. Safe to call repeatedly for the same record (each save, moderation toggle or payment): only the first call records a decision.
public
rewardPublishedContent(string $action, int $subjectId, int $userId, DateTimeInterface $insertedAt) : void
Parameters
- $action : string
- $subjectId : int
- $userId : int
- $insertedAt : DateTimeInterface
save()
public
save(TEntity $entity[, array<string, mixed> $meta = [] ][, array<int, string> $skipFields = [] ]) : Result
Parameters
- $entity : TEntity
- $meta : array<string, mixed> = []
- $skipFields : array<int, string> = []
Return values
ResultsendActivationEmail()
public
sendActivationEmail(string $username) : Result
Parameters
- $username : string
Return values
ResultsendNewPassword()
Whole "lost password" flow in one place.
public
sendNewPassword(string $username) : Result
Looks the account up by username or e-mail (shared self::findByUsernameOrEmail(),
same as self::sendActivationEmail()), generates a one-time token, stores its hash
with an expiry and sends SY_LOST_PASSWORD with the link itself. The password is not
changed here; self::resetPassword() does that when the user follows the link and
enters a new one. Callers (the external Api action
UserPresenter::actionSendNewPassword() and the front
component LostPasswordFormComponent) get only a success.
IMPORTANT: guarantees that must not be broken:
- The token never leaves this method except in the e-mail, neither in
datanor inmessage; whoever got it in a response would take over the account. - The response is constant. A missing account, an existing account and an internal
failure (DB/e-mail) all return the byte-identical
Result::success(OK); the failure goes to theauthlog instead. Otherwise the success/failure difference would let anyone enumerate registered e-mails, so the front must not reportmessages.noUser. - A stranger cannot lock the account. Only a pending token is created; the old password stays valid until the owner uses the link. A new request overwrites the token, so only the last sent link is valid.
Parameters
- $username : string
Return values
ResultsetAppContext()
public
setAppContext(AppContext $appContext) : $this
Parameters
- $appContext : AppContext
Return values
$thissetAssetFormatRegistry()
public
setAssetFormatRegistry(AssetFormatRegistry $assetFormatRegistry) : static
Parameters
- $assetFormatRegistry : AssetFormatRegistry
Return values
staticsetCacheTagInvalidator()
public
setCacheTagInvalidator(CacheTagInvalidator $cacheTagInvalidator) : static
Parameters
- $cacheTagInvalidator : CacheTagInvalidator
Return values
staticsetInternalCallContext()
public
setInternalCallContext(InternalCallContext $internalCallContext) : static
Parameters
- $internalCallContext : InternalCallContext
Return values
staticsetLanguageContext()
public
setLanguageContext(LanguageContext|null $languageContext) : $this
Parameters
- $languageContext : LanguageContext|null
Return values
$thissetParamsConfig()
public
setParamsConfig(Config $paramsConfig) : static
Parameters
- $paramsConfig : Config
Return values
staticsetPresenterContext()
public
setPresenterContext(PresenterContext $presenterContext) : $this
Parameters
- $presenterContext : PresenterContext
Return values
$thissetSettingContext()
public
setSettingContext(SettingContext $settingContext) : $this
Parameters
- $settingContext : SettingContext
Return values
$thisupdateAllRelations()
public
updateAllRelations(int|array<string|int, mixed> $entityId, string $property, array<string, mixed> $values) : Result
Parameters
- $entityId : int|array<string|int, mixed>
-
id, or a list of ids / composite-id arrays
- $property : string
- $values : array<string, mixed>
-
relation property name => value set on every row
Return values
ResultupdateColumn()
public
updateColumn(int|array<string|int, mixed> $entityId, string $property, mixed $value) : Result
Parameters
- $entityId : int|array<string|int, mixed>
-
id, or a list of ids / composite-id arrays
- $property : string
- $value : mixed
Return values
ResultupdateColumns()
IMPORTANT: `updateColumns()` does not run the save hooks (see `Core\Traits\Api\Models\Managers\SaveTrait`). Without this override a ban through the API would not cancel sessions and the sign-in gate could be bypassed by waiting for a token refresh. The `banned` column is writable through the API (`#[UpdatableColumn]`), so this path is real.
public
updateColumns(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result
Parameters
- $entityId : int|array<string|int, mixed>
- $values : array<string, mixed>
Return values
ResultupdateOwnPassword()
Changes the password of the caller's own account.
public
updateOwnPassword(int $userId, string $currentPassword, string $newPassword) : Result
IMPORTANT: $userId must come only from the verified identity, never from the request:
- external path: from the JWT payload
(UserPresenter::actionUpdatePassword(), which has
no
userIdparameter), - internal path: from the front session (
UserPasswordFormComponent).
It is separate from self::updatePasswordForUser() as two methods, not a
bool $isAdmin flag; a bool parameter can be swapped by mistake, a method name cannot.
A stolen session (session, access/refresh token) must not be enough to take the account over permanently, therefore:
$currentPasswordis verified against the DB hash (INVALID_PASSWORDon mismatch),- the new password needs at least
PasswordPolicy::USER_MIN_LENGTHcharacters (INVALID_LENGTH), - all refresh tokens of the account are invalidated (reason
password_change); other sessions drop at the latest when their access token expires (jwt.ttl, 15 min). The caller renews the current session with a new token pair issued by the presenter (UserPresenter::internalActionUpdatePassword()).
Parameters
- $userId : int
- $currentPassword : string
- $newPassword : string
Return values
ResultupdatePasswordForUser()
Changes the password of another account: admin and system paths (currently the lazy rehash in {@see \App\UI\Api\System\Presenters\AuthPresenter::internalActionLogin()}).
public
updatePasswordForUser(int $userId, string $password) : Result
IMPORTANT: not reachable from an external HTTP path. :Api:System:User:updatePassword
points to self::updateOwnPassword() and the presenter has no other password
action with a userId parameter. An external endpoint for this needs its own key;
adding userId back to the existing action lets any caller change any password.
Parameters
- $userId : int
- $password : string
Return values
ResultupdateRelations()
public
updateRelations(int|array<string|int, mixed> $entityId, string $property, array<string|int, object> $relationEntities) : Result
Parameters
- $entityId : int|array<string|int, mixed>
-
id, or a list of ids / composite-id arrays
- $property : string
- $relationEntities : array<string|int, object>
-
relation entities with updated values
Return values
ResultusernameExists()
Whether the username is already used by another user (than `$excludeId`).
public
usernameExists(string $username[, int|null $excludeId = null ]) : bool
Parameters
- $username : string
- $excludeId : int|null = null
Return values
boolafterSave()
protected
afterSave(bool $isInsert, User &$entity[, Result|null $result = null ][, array<string, mixed> $meta = [] ]) : Result|null
Parameters
Return values
Result|nullbeforeSave()
protected
beforeSave(bool $isInsert, User &$entity[, array<string, mixed> $meta = [] ]) : Result|null
Parameters
- $isInsert : bool
- $entity : User
- $meta : array<string, mixed> = []
Return values
Result|nullbeginRejectedUploads()
Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
protected
beginRejectedUploads() : array<int, array{name: string, reason: string}>
Return values
array<int, array{name: string, reason: string}> —the outer save's list
bumpCacheTags()
Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
protected
bumpCacheTags(object $entity) : void
Fail-open (the invalidator swallows exceptions). Called from the manager Save/Delete/Tree traits after the commit.
Parameters
- $entity : object
bumpCacheTagsExplicit()
Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
protected
bumpCacheTagsExplicit(array<string|int, string> $classes) : void
Parameters
- $classes : array<string|int, string>
bumpCacheTagsForClass()
Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
protected
bumpCacheTagsForClass(class-string $rootClass[, array<string|int, string> $extraTags = [] ]) : void
Bumps the root class and its owned-cascade classes.
Parameters
- $rootClass : class-string
- $extraTags : array<string|int, string> = []
classCacheTags()
Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.
protected
classCacheTags() : array<string|int, string>
IMPORTANT: additionalCacheTags(object) receives an entity, so a class-level bump cannot
use it; without this hook a delete would lose the cross-entity tag (Comment to
CommentThread). A manager whose cross-entity tag does not depend on a particular
entity returns it here.
NOTE: unlike additionalCacheTags(), this hook is declared (with an empty default)
instead of being an opt-in through method_exists(); method_exists() is always true in
a manager that has the method, which phpstan reports as alreadyNarrowedType.
Return values
array<string|int, string>completedSave()
Automatic VAT id check after a registration (`$meta['verifyVatId']`, sent by the Front registration form). Post-commit and best effort (SaveTrait `completedSave`): the registered account stays even when VIES is down, and the up to 3 s VIES call does not hold the save transaction. The registering visitor has no identity yet, so the Front cannot ask `VatIdCheck:verify` itself; the per-IP safety cap applies.
protected
completedSave(bool $isInsert, User $entity[, Result|null $result = null ][, array<string, mixed> $meta = [] ]) : void
Profile / admin saves check through the bridge after the save instead (they show the state).
Parameters
createRatingThread()
Creates a new {@see RatingThread} of the given section and writes its id into `$column` of the entity `$entityId` (default `ratingThreadId`, a scalar FK; pass the association name for an association). Returns a failed {@see Result} (the caller may return it from `afterSave` to roll back), or null.
protected
createRatingThread(int|null $entityId, ThreadType $type[, string $column = 'ratingThreadId' ]) : Result|null
Parameters
- $entityId : int|null
- $type : ThreadType
- $column : string = 'ratingThreadId'
Return values
Result|nulldeleteAsset()
Deletes an asset file by its absolute path.
protected
deleteAsset(string|null $absolutePath) : void
Parameters
- $absolutePath : string|null
-
Full absolute path to the file
deleteOwnedThreads()
protected
deleteOwnedThreads(array<int, int> $commentThreadIds, array<int, int> $ratingThreadIds, CommentThreadManager|null $commentThreadManager, RatingThreadManager|null $ratingThreadManager) : Result|null
Parameters
- $commentThreadIds : array<int, int>
- $ratingThreadIds : array<int, int>
- $commentThreadManager : CommentThreadManager|null
- $ratingThreadManager : RatingThreadManager|null
Return values
Result|null —a failed Result rolls the delete back; null = done
deriveCountryText()
protected
deriveCountryText(string|null $countryCode, callable(Array): mixed $setCode, callable(string): mixed $setText[, int $textLength = 80 ]) : Result|null
Parameters
- $countryCode : string|null
- $setCode : callable(Array): mixed
- $setText : callable(string): mixed
- $textLength : int = 80
Return values
Result|nullfinishRejectedUploads()
Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.
protected
finishRejectedUploads(Result|null $result, array<int, array{name: string, reason: string}> $outer) : void
Parameters
- $result : Result|null
- $outer : array<int, array{name: string, reason: string}>
-
list from beginRejectedUploads()
getDeletedId()
Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).
protected
getDeletedId(array<string, mixed> $meta, object $entity) : int|null
Parameters
- $meta : array<string, mixed>
- $entity : object
Return values
int|nullgetService()
Returns the service backing the manager traits.
protected
getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>ownedThreadIds()
Reads the thread ids from the entities preloaded before the delete (`getCommentThreadId()` / `getRatingThreadId()`).
protected
static ownedThreadIds(array<string|int, object> $entities) : array{comment: list, rating: list}
Parameters
- $entities : array<string|int, object>
Return values
array{comment: listreadUploadedItem()
Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.
protected
readUploadedItem(array<string|int, mixed> $item[, bool $strictBase64 = true ][, int|null $maxBytes = null ]) : string|null
Call it right before processing and release the result before the next item, so that
files never meet in memory. A path is accepted only during an internal bridge call
($internalCallContext) and only inside the dropzone root. A rejected path is logged
to the auth channel and null is returned; callers treat it like unreadable data (skip
the item or refuse the save).
Parameters
- $item : array<string|int, mixed>
- $strictBase64 : bool = true
-
false for callers that decode loosely (
base64_decode($x)) - $maxBytes : int|null = null
-
size limit; for
pathit is checked before reading
Return values
string|nullrecordRejectedUpload()
Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).
protected
recordRejectedUpload(mixed $name, string $reason) : void
Called by the places that drop an item (processOrderedAssets(), advert attachments,
product set gallery).
Parameters
- $name : mixed
- $reason : string
saveAsset()
Saves one base64 image; `null` means the image did not pass {@see ImageUploadPolicy}.
protected
saveAsset(array<string|int, mixed> $asset, string $dir) : string|null
Parameters
- $asset : array<string|int, mixed>
-
['name' => 'filename.jpg', 'data' => 'base64...']
- $dir : string
-
Absolute path to target directory
Return values
string|nullsaveAssets()
Saves base64 images into a directory, raster images only, per {@see ImageUploadPolicy}.
protected
saveAssets(array<string|int, mixed> $assets, string $dir) : array<int, string>
IMPORTANT: the type is detected from the content, a decompression bomb is rejected by its header and
the image is re-saved through GD (orientation applied, metadata stripped). Writing raw bytes with the
extension from the name would let a new internal caller store an svg with a script and keep EXIF
(including GPS) in every photo. A rejected file is skipped (written to the assets log).
Parameters
- $assets : array<string|int, mixed>
-
[['name' => 'filename.jpg', 'data' => 'base64...'], ...] (from the Api meta; the shape is verified here)
- $dir : string
-
Absolute path to target directory
Return values
array<int, string> —saved file names (without the rejected ones)
assignReferralCode()
private
assignReferralCode(int $userId) : void
Parameters
- $userId : int
attachReferrerByCode()
Links a new account to the owner of the referral code. An unknown, malformed or own code is ignored; the registration itself always goes on.
private
attachReferrerByCode(int $userId, string $code) : void
IMPORTANT: a code is also ignored while the program is off or the referrer has the affiliate disabled. The cookie lives 30 days, so it can carry a code from before the switch-off; such a registration must not count in the referrer's statistics on "My account".
Parameters
- $userId : int
- $code : string
blacklistRefusal()
A new account of a non-privileged caller (front registration, another client) is refused when its e-mail, phone or registration IP (`meta.ip`, sent by the front; external meta is stripped) is on the blacklist. The front form checks the same values first and reports them at the fields; this is the gate for every other path ({@see BlacklistManager::refusalFor()} skips the administrator).
private
blacklistRefusal(User $entity, array<string, mixed> $meta) : Result|null
Parameters
- $entity : User
- $meta : array<string, mixed>
Return values
Result|nullbuildAccountDeletionLink()
private
buildAccountDeletionLink(array<string, string> $params) : string|null
Parameters
- $params : array<string, string>
Return values
string|nullbumpDeleteCacheTags()
Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
private
bumpDeleteCacheTags(array<int, object> $entities) : void
Parameters
- $entities : array<int, object>
callerCanSetRoles()
Whether the caller may set roles (`:Api:System:User:setRoles`). `null` means the audience was not resolved (cron/CLI calls the manager directly); internal code without a caller is not checked. The identity comes only from the Api gate ({@see FieldAudienceContext}), never from data. Fail-closed without the resolver.
private
callerCanSetRoles() : bool|null
Return values
bool|nullcaptureDeletedIds()
Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.
private
captureDeletedIds(array<int, object> $entities) : WeakMap<object, int>
Parameters
- $entities : array<int, object>
Return values
WeakMap<object, int>changedContactBlacklistRefusal()
Profile edit: a non-privileged caller may not change the e-mail or phone to a blacklisted value. Only the changed values are checked, so an account whose current value was blacklisted later can still save the rest.
private
changedContactBlacklistRefusal(User $entity) : Result|null
Parameters
- $entity : User
Return values
Result|nullcolumnsInvalidateCache()
private
columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
- $columns : array<int, string>
Return values
boolcontentRewardSince()
Moment the reward of the action was switched on; null = never (nothing is rewarded).
private
contentRewardSince(string $action) : DateTimeImmutable|null
Parameters
- $action : string
Return values
DateTimeImmutable|nullcontentRewardSkipReason()
private
contentRewardSkipReason(string $action, int $userId) : string|null
Parameters
- $action : string
- $userId : int
Return values
string|nulldeactivatesAccount()
Whether `updateColumns` writes a state in which the account stops being active.
private
static deactivatesAccount(array<string, mixed> $values) : bool
Parameters
- $values : array<string, mixed>
Return values
booldeleteBatch()
Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.
private
deleteBatch(array<int, int|string|array<string|int, mixed>> $ids) : Result
Transaction ownership: the wrapper only opens its own level; inside a caller's transaction a rollback
here marks the caller's transaction rollback-only, so its final commit rolls the whole scope back
(TransactionManager counter contract, the same as the hooked delete() path).
Parameters
- $ids : array<int, int|string|array<string|int, mixed>>
Return values
ResultdeliverPasswordResetLink()
Generates and stores the token and sends `SY_LOST_PASSWORD` with the link to one specific account.
private
deliverPasswordResetLink(User $user) : Result
IMPORTANT: private on purpose. The only entry point is self::sendNewPassword(), which keeps the response constant; making this method public brings the oracle back.
The link targets the "Lost password" page (Page:form with the lostPasswordForm
component); by the email and key parameters the component shows the new password form.
Parameters
- $user : User
Return values
ResultderivePaymentRegionLink()
`regionId` is derived from the payment region; the front and the admin form fill it in themselves and never offer it separately. An external `save` could send one that does not match the payment region, so it is always aligned here, whatever path or value the caller used: - no payment region: both are null (same as the front when no region is found), - an unknown payment region: an error (otherwise the save would fail on the FK).
private
derivePaymentRegionLink(User $entity) : Result|null
Parameters
- $entity : User
Return values
Result|nullfieldTakenByOther()
private
fieldTakenByOther(string $field, string $value, int|null $excludeId) : bool
Parameters
- $field : string
- $value : string
- $excludeId : int|null
Return values
boolfindByUsernameOrEmail()
Finds an account by username or e-mail; the single implementation, shared by {@see self::sendActivationEmail()} and {@see self::sendNewPassword()}.
private
findByUsernameOrEmail(string $username) : User|null
Parameters
- $username : string
Return values
User|nullgetUserImageDir()
private
getUserImageDir(int $userId, DateTimeInterface $datetime[, bool $absolute = true ]) : string
Parameters
- $userId : int
- $datetime : DateTimeInterface
- $absolute : bool = true
Return values
stringguardedDeactivation()
private
guardedDeactivation(array<int, int> $ids, callable(): Result $write) : Result
Parameters
- $ids : array<int, int>
- $write : callable(): Result
Return values
ResultguardedDelete()
private
guardedDelete(array<string|int, mixed> $ids, callable(): Result $delete) : Result
Parameters
- $ids : array<string|int, mixed>
- $delete : callable(): Result
-
the delete itself (parent implementation; opens only a nested level)
Return values
ResultinvalidateCredentialsAfterPasswordChange()
After a password change by a path other than the own change or reset (admin, {@see self::generateNewPassword()}): invalidates all refresh tokens and issued access tokens of the account (same method and reason as {@see self::updateOwnPassword()}) and clears a pending "lost password" link.
private
invalidateCredentialsAfterPasswordChange(int $userId) : Result
NOTE: an admin changing the password of their own account signs themselves out too (their token is also older than the stamp); they sign in again after the change.
Parameters
- $userId : int
Return values
ResultisActiveState()
Whether the account stays active after saving in the sense of protected roles (can sign in).
private
static isActiveState(User $user) : bool
Parameters
- $user : User
Return values
boolisAssetPathAllowed()
Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
private
isAssetPathAllowed(BaseEntity $entity) : bool
Parameters
- $entity : BaseEntity
Return values
boolissueActivationLink()
Generates a new activation key, stores its sha256 and expiry (72 h) and returns the link with the readable key (the only place the readable key exists). Returns `null` when the account has no id or e-mail, no presenter is available for the link, or the key could not be stored.
private
issueActivationLink(User $user[, string|null &$issuedHash = null ]) : string|null
IMPORTANT: the link targets RegistrationPresenter, not :Front:System:Page:registration
(PagePresenter has no such action).
Parameters
- $user : User
- $issuedHash : string|null = null
Tags
Return values
string|nullloadEntitiesForCacheTags()
Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.
private
loadEntitiesForCacheTags(int|array<string|int, mixed> $id) : array<int, object>
Parameters
- $id : int|array<string|int, mixed>
Return values
array<int, object>notifyBanState()
Tells the content modules the resulting ban state of the account (idempotent, like {@see revokeSessionsIfBlocked()}); they hide or restore the user's public content.
private
notifyBanState(User $user) : void
Parameters
- $user : User
onboardNewAccount()
Steps after a new account got its code: links the referrer and, for an account already active, pays the rewards right away. Fail-open.
private
onboardNewAccount(int $userId, string|null $referralCode, bool $activated) : void
Parameters
- $userId : int
- $referralCode : string|null
- $activated : bool
positiveIds()
private
positiveIds(array<string|int, mixed> $ids) : array<int, int>
Parameters
- $ids : array<string|int, mixed>
Return values
array<int, int>processAvatar()
private
processAvatar(User &$entity, array<string, mixed> $meta) : Result|null
Parameters
- $entity : User
- $meta : array<string, mixed>
Return values
Result|nullreferralSkipReason()
private
referralSkipReason(array{ip: ?string, referrerUserId: ?int, referrerIp: ?string, referrerActive: bool, referrerAffiliateDisabled: bool} $state, int $referrerId) : string|null
Parameters
- $state : array{ip: ?string, referrerUserId: ?int, referrerIp: ?string, referrerActive: bool, referrerAffiliateDisabled: bool}
- $referrerId : int
Return values
string|nullresolveAvatarAbsPath()
Converts the stored relative avatar path to an absolute disk path.
private
resolveAvatarAbsPath(string|null $relPath) : string|null
Swaps only the configured root (userDir.rel to userDir.abs) and keeps the rest, so
it also works for avatars stored under another year/month than the user's registration
has today. Returns null when there is nothing to delete.
Parameters
- $relPath : string|null
Return values
string|nullrestoreTokenIfStillIssued()
Called when the e-mail with the new link was not queued, so the user holds only the previous one: restores the previous hash and expiry. Conditional: only while the row still carries the hash issued here, so a concurrent successful resend (another hash) is not overwritten. `$previous` comes from the state loaded from the DB before issuing (Api `findBy`/`find` reads the DB, not the query cache).
private
restoreTokenIfStillIssued(int $userId, string $hashProperty, string $issuedHash, array<string, mixed> $previous) : void
Parameters
- $userId : int
- $hashProperty : string
- $issuedHash : string
- $previous : array<string, mixed>
revokeSessionsIfBlocked()
Invalidates all refresh tokens of a blocked account. It deliberately looks only at the resulting state, not at the transition: this is idempotent (re-saving a banned account has nothing to revoke) and avoids needing the previous value, which the Doctrine identity map may already have overwritten.
private
revokeSessionsIfBlocked(User $user) : void
Best effort: revokeAllForUser() is fail-open, a revocation failure must not fail the user save.
Parameters
- $user : User
rewardActivatedAccount()
Registration bonus for the account and the reward for its referrer, once the account is active.
private
rewardActivatedAccount(int $userId) : void
Fail-open.
Parameters
- $userId : int
roleChangeRefusal()
Applies the role and account state gates ({@see UserRolePolicy::assignmentRefusal()}, {@see UserRolePolicy::lastHolderRefusal()}) to data from the DB.
private
roleChangeRefusal(int|null $userId, array<string|int, mixed>|null $requestedRoleIds[, bool $staysActive = true ]) : Result|null
Parameters
- $userId : int|null
- $requestedRoleIds : array<string|int, mixed>|null
-
nullmeans roles do not change - $staysActive : bool = true
Return values
Result|nullrollbackBatch()
private
rollbackBatch() : void
saveWithHooks()
Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
private
saveWithHooks(TEntity $entity, array<string, mixed> $meta, array<int, string> $skipFields) : Result
Parameters
- $entity : TEntity
- $meta : array<string, mixed>
- $skipFields : array<int, string>
Return values
ResultsendAccountDeletionEmail()
private
sendAccountDeletionEmail(User $user, string $emailKey, array<string, string> $values) : Result
Parameters
- $user : User
- $emailKey : string
- $values : array<string, string>
Return values
ResultsendRegistrationEmails()
private
sendRegistrationEmails(User &$entity, array<string, mixed> $meta) : void
Parameters
- $entity : User
- $meta : array<string, mixed>
updateColumnsTargetIds()
Returns account ids from the `updateColumns` argument (an id, a list of ids, or rows `[0 => id, …]`).
private
static updateColumnsTargetIds(int|array<string|int, mixed> $entityId) : array<int, int>
Parameters
- $entityId : int|array<string|int, mixed>
Return values
array<int, int>userTypeDataRefusal()
The user type may require company data (reg. id and company name) and the VAT id (`requires_company_data`, `requires_vat_id`); the front forms (registration, profile) require them, a direct Api save did not. Only a non-privileged caller is checked (the administrator may save an incomplete account), and only when the save carries the user type.
private
userTypeDataRefusal(User $entity) : Result|null
Parameters
- $entity : User
Return values
Result|nullusesAssetPathTrait()
private
static usesAssetPathTrait(string $class) : bool
Parameters
- $class : string
Return values
boolwritePassword()
The only password write implementation; the public variants above differ only in contract.
private
writePassword(int $userId, string $password) : Result
Parameters
- $userId : int
- $password : string