MessageManager
extends Manager
in package
implements
ContentCreationLimitedInterface, TreeManagerInterface, CommentThreadGate
uses
BlacklistGuardTrait, SaveTrait, ContentCreationLimitTrait, AssetTrait, AssetProcessingTrait, CommentThreadCreatorTrait, RatingThreadCreatorTrait, InitialApprovalTrait, MessageConfirmationTrait, ManageKeyTrait, TreeTrait, ContentCreditRewardTrait
Base class of Api managers whose traits delegate to a typed `Service<TEntity>`.
Tags
Table of Contents
Interfaces
- ContentCreationLimitedInterface
- Manager that draws on the {@see ContentCreationLimiter} quota when creating content.
- TreeManagerInterface
- Manager of a nested-set tree entity.
- CommentThreadGate
- Lets the module that owns the commented content refuse new comments in a thread (e.g. Discussion: the author switched comments off with `show_comments = 0`). Base must not read foreign module tables, so the owner implements this interface; {@see CommentThreadGateRegistry} asks every implementation.
Constants
- META_MOVE_SUBTREE_PARENT : mixed = 'moveSubtreeToParentId'
- Meta key by which an admin/front form says "this entity changed its parent"; the value is the new `parentId` (`null`/0 = root). See {@see applyMetaSubtreeMove()}.
- DELETED_IDS_META : string = '__deletedIds'
- Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
- CONTENT_REWARD_VISIBILITY_PROPERTIES : array<string|int, mixed> = ['approvedAt', 'confirmedAt', 'banned', 'paid',...
- Entity properties whose change can publish a record; other `updateColumns()` calls are ignored.
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}.
- $blacklistedWordManager : BlacklistedWordManager|null
- Set from the constructor; a plain property so unit tests built without the constructor skip the check.
- $blacklistManager : BlacklistManager|null
- $cacheTagInvalidator : CacheTagInvalidator|null
- Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
- $defaultSkipFields : array<int, string>
- $graphDataKeys : array<int, string>
- Keys an external client may send nested in `data`; `internalActionSave` moves them to `meta`, where `afterSave` reads them unchanged (dropzone `orderedAssets`, classic mode `existingAssets` + `assets`, `categoryIds` for {@see syncCategories()}).
- $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
- $lastAssetRejectionCode : string|null
- Why the last {@see saveAssetVariants()} returned `null` ({@see UploadRejection} `REASON_*`); the caller passes it to the report of rejected items. `null` means the last save passed.
- $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
- $assetManager : AssetManager
- $commentThreadManager : CommentThreadManager
- $contentCreationLimiter : ContentCreationLimiter|null
- $emailManager : EmailManager
- $entityManager : EntityManagerInterface
- $esDocumentSource : MessageDocumentSource
- $esIndexer : EsEntityIndexer
- $pendingDeleteAssetIds : array<int, int>
- $pendingDeleteCommentThreadIds : array<int, int>
- $pendingDeleteMessageIds : array<int, int>
- $pendingDeleteRatingThreadIds : array<int, int>
- $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`.
- $userManager : UserManager|null
- $variantProvider : AssetVariantProvider
Methods
- __construct() : mixed
- acceptsComments() : bool|null
- The author can switch comments off (`show_comments = 0`, front form); the detail then hides them and a new comment into the post thread is refused (`CommentManager::beforeSave()`, decision of the audit 19. 10.).
- delete() : Result
- Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:
- deleteAll() : Result
- Bulk delete by filters (external admin `:Api:Discussion:Message:deleteAll`) as a series of {@see deleteNode()} calls.
- deleteNode() : Result
- Deletes a thread in the tree (admin grid `deleteNode`) with its reply subtree.
- find() : T|null
-
findAll()
: T> : list
>) - findBy() : T|null
- 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).
- getAppContext() : AppContext
- 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>
- Sets the owner (the scalar `userId`, mapped by a plain `#[ORM\Column]`) from the server on an external INSERT.
- getExternalRemoteIpData() : array<string, mixed>
- Sets the author's IP on an external INSERT (`ip` is `#[ExternalReadonly]`).
- 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
- 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
- initializeAllContexts() : void
- insertTreeNode() : Result
- Computes lft/rgt/level for a newly inserted node (after `save()`, when it has `parent_id` in the database but zero lft/rgt). It is inserted as the last sibling (after the largest `rgt` among nodes with the same `parent_id`).
- isOwnedBy() : bool
- Returns whether the post `$messageId` belongs to user `$userId`.
- markCategoriesPaid() : void
- Settled payment for categories of topics: marks the paid category relations and the topics paid (raw SQL on the module tables), then rewards and reindexes the topics that just became public. Called by the payment callback ({@see PaymentItemManager::updateStatusForPayment()}).
- moveDown() : Result
- Moves after the next sibling.
- moveSubtree() : Result
- Moves a whole subtree (root and descendants) under another parent.
- moveSubtreeTo() : Result
- Moves a whole subtree under a new parent at a specific position.
- moveUp() : Result
- Moves before the previous sibling.
- redeemManageKey() : Result
- Returns `OK` when the key matches the record and its current password and is still valid. An unknown record, a record with an owner, a wrong or expired key and a key after a password change all return the same code `USER_PASSWORD_RESET_KEY_INVALID`, so the action does not reveal which part did not match.
- registerEvent() : Result
- reorderChildren() : Result
- Reorders the direct children of a parent (with whole subtrees) into the given order, in one operation under the tree lock (the "sort children alphabetically" admin grid action; the caller decides the order). A mismatching set of children returns `TREE_STALE` and nothing is written; `data.changed` is the number of moved children.
- rewardPublishedContentIds() : void
- Rewards the authors of those given records that are published now.
- save() : Result
- setAppContext() : $this
- setAssetFormatRegistry() : static
- setCacheTagInvalidator() : static
- setContentCreationLimiter() : void
- setInternalCallContext() : static
- setLanguageContext() : $this
- setParamsConfig() : static
- setPresenterContext() : $this
- setSettingContext() : $this
- updateAllRelations() : Result
- updateColumn() : Result
- updateColumns() : Result
- Grid toggles (approved, banned) and the confirmation by key publish a post here, without the save hooks; the credits for content follow ({@see ContentCreditRewardTrait}).
- updateRelations() : Result
- afterDelete() : Result|null
- Deletes the comment and rating threads collected in {@see beforeDelete()}, inside the delete transaction (a failure rolls the message delete back).
- afterMoveSubtree() : Result
- Recomputes the thread root of the whole subtree after a move (grid "Move to...", drag and drop, parent change in the form). It runs in the transaction under the move lock, so a failure also rolls back the move itself.
- afterSave() : Result|null
- applyMetaSubtreeMove() : Result|null
- Recomputes the nested set after a parent change. Called from `afterSave()`, so inside the save transaction.
- assetCleanupRoots() : array<int, string>
- Asset roots inside which {@see removeEmptyAssetDirs()} may remove empty directories; never above them.
- assetDir() : string
- Returns the asset directory from the server neon `params` (through Config), not from `getSettings()`: the shared setting context may be filled by a client without server paths (see the in-process ApiBridge).
- assetIdOf() : int|null
- Normalizes the "current image" to an id. Channels with an ORM association return an entity (even a lazy proxy; `getId()` does not initialize it), channels with a scalar FK return an int.
- assetVariantFilePaths() : array<int, string>
- Returns the absolute paths of the files of the asset just written (variants and the original).
- beforeDelete() : Result|null
- Collects dependents on a direct `delete()` of a message (external Api), while the rows exist.
- beforeSave() : Result|null
- Sanitizes the post text on the server (HTMLPurifier), enforces the creation quota and applies initial approval, confirmation, thread root and the derived `reported` flag.
- 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).
- captureOwnedAssetIds() : array<int, int>
- Ids of the images the given entities own ({@see completedAssetCleanup()} requires the same opt-in `ownedAssetIds()`), captured before the rows are deleted. For paths that bypass {@see DeleteTrait::delete()} and its hooks, such as a subtree delete ({@see TreeTrait::deleteNode()}). Fail-soft: an error of one entity is logged and the others are processed.
- 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.
- completedAssetCleanup() : void
- Post-commit cleanup of the assets of an entity that was just deleted.
- completedDelete() : void
- Deletes the message from ES after commit (fail-open).
- completedSave() : void
- Indexes the message in ES after commit (fail-open).
- contentRewardAction() : string
- contentRewardCandidatesSql() : string
- Published = approved, confirmed, paid and not banned, deleted or deactivated.
- contentRewardConnection() : Connection
- contentRewardUserManager() : UserManager|null
- createAssetRel() : object
- createCommentThread() : Result|null
- Creates a new {@see CommentThread} and writes its id into `$column` of the entity with id `$entityId` (default `commentThreadId`, a scalar FK; pass the association name for an association). Returns a failed {@see Result} (the caller in `afterSave` may return it, which rolls back), or null when there is nothing to do or everything is fine.
- 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.
- deleteAssetFiles() : void
- Deletes the previously scanned files (see {@see listAssetFilesIn()}). Call only after the FK update to the new Asset succeeded.
- dropForeignExistingItems() : array<int, TItem>
- From the `type: existing` items keeps only those whose id is already linked to the record `$parentId` through `$relationProperty` (read from the database). Other types (`new`) stay unchanged. When the links cannot be determined (`null`), no existing item passes: losing the order is better than attaching a foreign file.
- failureOrNull() : Result|null
- A failed Result to propagate, success is null (the `afterSave` hook returns `?Result`).
- 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.
- getAssetDirAbs() : string
- getAssetDirRel() : string
- getAssetFormatKinds() : array<int, AssetFormatKind>
- Media kinds allowed in the dropzone gallery of this manager. The default is images and video. Vector is left out on purpose: an end-user svg is needless exposure even when the sanitizer passes it; enable it explicitly if needed.
- getAssetFormatRegistry() : AssetFormatRegistry
- Requires `$this->assetFormatRegistry` set by the decorator ({@see \App\Core\Base\Api\Models\Managers\Manager::setAssetFormatRegistry()}).
- getAssetMaxBytes() : int|null
- Upper size limit of a new gallery file in bytes, `null` = no limit. The default has none; adverts and company profiles return {@see \App\Core\Security\Upload\ImageUploadSizeLimit::bytes()}.
- getAssetSubDir() : string
- getAssetUsageKey() : string|null
- getAssetVariantType() : string
- 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).
- getMainAssetColumn() : string
- Name of the property the main (first) gallery image is written to. The default `asset` is an ORM ManyToOne to `Asset` (Message/Advert/Intro). Entities that keep the main image as a flat FK column without an association (Company `Reference::$imageId`) override the hook; otherwise `updateColumn()` would write to a nonexistent property.
- getService() : Service<string|int, TEntity>
- Returns the service backing the manager traits.
- listAssetFilesIn() : array<int, string>
- Lists the files lying in the target directory before a new image is written.
- orderedAssetsFromMeta() : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
- Turns `meta['orderedAssets']` of the request into the list of dropzone items in the shape {@see processOrderedAssets()} reads. Unknown keys and values of a wrong type are dropped; a present but non-text `path` stays as `''` so the file read rejects it the same way instead of silently skipping it.
- orderedFilesFromMeta() : array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>
- Turns `meta['files']` of the request into the list of files for `processFiles()`/`processComplaintFiles()`.
- processOrderedAssets() : Result|null
- Shared template method for processing an ordered array of dropzone assets.
- 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_*`).
- releaseAsset() : bool
- Releases the old asset after an image was cleared or replaced: deletes its `cms_system_images` row and files. Without this step every replacement or removal left an orphaned row.
- releaseDroppedAssets() : void
- Releases the images that disappeared from the record after the relations were replaced (removed in the gallery or form). Call only after the relations and the main image were written successfully; a shared image (still referenced) is not deleted by {@see releaseAsset()}, and a cleanup error does not fail the save.
- releaseOwnedAssets() : void
- Releases the captured images after the delete commit; a shared image stays ({@see releaseAsset()} → `AssetManager::releaseAssetRow()`), an error is only logged.
- removeEmptyAssetDirs() : void
- Removes the directories the delete left empty (`galleries/20/{big,original,thumb}`, `intros/14/1/…`), walking up while a directory is empty and strictly inside one of the asset roots ({@see assetCleanupRoots()}).
- resolveAssetExtension() : string|null
- Normalized extension of an uploaded file, or null when the format is not allowed in this context.
- rewardPublishedContentAfterUpdate() : void
- `updateColumns()` hook: rewards only when the write succeeded and touched a column that can publish the record (view counters and similar writes cost nothing).
- 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}.
- saveAssetVariants() : Asset|null
- Saves the files to disk (the format processor does the work) and creates the `Asset` entity with the variant plan and real dimensions. A variant `name` in the database equals the config key and the directory on disk.
- treeConfigForEntity() : TreeConfig
- `TreeConfig` used when `save()` computes lft/rgt of a new entity automatically ({@see SaveTrait}).
- applyDerivedReported() : void
- Keeps `reported` derived from reports (the post has at least one unresolved report).
- applyGuestManageSecret() : void
- A guest post gets a fresh random secret in `password` (a value from the payload is ignored). The "Manage post" link ({@see \App\Core\Traits\Api\Models\Managers\ManageKeyTrait}) and the edit capability are bound to it, like an advert to its password; nobody can type it, so a guest manages the post only through the link. A logged-in author's post is left as it is.
- applyInitialApproval() : void
- applyInitialMessageConfirmation() : void
- Called from `beforeSave()` on insert only. A logged-in author means the post has `userId` (front end: the logged-in identity; external path: the JWT, `userId` is `#[ExternalReadonly]`).
- applyInitialPaid() : bool
- Initial payment state of a new post, written to the row: a topic (level 1) with categories is paid when at least one of them is free; a reply or a topic without a category is always paid.
- applyThreadRoot() : void
- Sets `first_parent_id` to the thread root (id of the level-1 message; a root itself has null).
- blacklistRefusal() : Result|null
- buildManageLink() : string
- "Manage post" link for both insert e-mails. A guest post gets a link with a `manageKey` that opens the edit without login while it is valid (7 days); unlike an advert, the confirmation request carries it too, because a guest post has no password to fall back on. A logged-in author gets the plain edit link (requires login). An empty string means no link (the template then leaves the paragraph out).
- 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.
- 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.
- captureSubtreeAssetIds() : array<int, int>
- Returns the ids of images owned by the whole subtree; empty when the manager declares no image cleanup.
- collectMessageDependents() : void
- Remembers the comment and rating threads of the messages and their replies (deleted by {@see deletePendingMessageThreads()}) and the images to release after commit. Read by SQL while the message rows still exist.
- columnsInvalidateCache() : bool
- completedMessageDelete() : void
- Post-commit cleanup of deleted messages' images (best-effort; `releaseAsset()` leaves a shared asset and its files alone). An exception goes only to the log, a deleted message is not restored.
- 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.
- deletePendingMessageThreads() : Result
- Deletes the collected comment and rating threads through their managers (`DeleteTrait`); comments, ratings and votes go with the thread by DB cascade. Call inside the delete transaction.
- esDeleteBestEffort() : void
- guardAnonymousInsert() : Result|null
- Returns `RATE_LIMIT_EXCEEDED` when a guest visitor exceeded the quota of anonymous records of the kind `$scope` (per IP and per normalized `$email`), otherwise `null`. Only an insert by a guest (`$userId` null); a logged-in user is counted by {@see guardContentCreation()}.
- guardContentCreation() : Result|null
- Returns `RATE_LIMIT_EXCEEDED` when the quota is exceeded, otherwise `null`. Only an insert by a logged-in user is counted (see {@see ContentCreationLimiter::isAllowed()}).
- isActivationMailAllowed() : bool
- Tells whether another activation e-mail may be sent to `$email`. Unlike {@see guardContentCreation()} it applies to guests too (it protects the recipient); see {@see ContentCreationLimiter::isActivationMailAllowed()}.
- isAssetPathAllowed() : bool
- Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
- issueManageKey() : string|null
- Issues the key and returns it in readable form (only for the e-mail link); `null` means the record needs no link or the write failed, and the e-mail then carries a plain edit link (logged-in owner, password, reset).
- 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.
- messageService() : MessageService
- moveTreeNode() : Result
- removeEmptyMessageDirs() : void
- Removes empty image folders of deleted messages (`<messageDir>/Y/m/<messageId>/` plus variant subfolders, {@see getAssetSubDir()}). Never higher and only empty ones, like `AdvertHardDeleteTrait::removeEmptyAdvertDirs()`.
- rollbackBatch() : void
- runTreeTransaction() : Result
- The body of {@see treeTransaction()}: a transaction only when none is running, a tag bump after success, an exception means log plus rollback of the own transaction.
- saveWithHooks() : Result
- Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
- sendInsertEmail() : void
- Sends the author an e-mail after the insert commits: with the confirmation link, or just an insert notification. Sends nothing when confirmation was required but no key was issued (admin confirmed or no e-mail).
- sendMessageEmail() : void
- syncCategories() : void
- treeService() : TreeServiceInterface
- Loud guard against silent failure: tree operations run only when the underlying service implements the method (`TreeTrait` on service, mapper and repository). A missing one is a wiring bug, so a {@see \LogicException} is thrown (logged through `treeTransaction()` / `save()`) instead of a silent `INVALID_OPERATION` that did not show where the problem is.
- treeTransaction() : Result
- Wraps a tree operation in a transaction (when the service supports it): the operations rewrite lft/rgt on many rows and a partial write would break the tree.
- usesAssetPathTrait() : bool
Constants
META_MOVE_SUBTREE_PARENT
Meta key by which an admin/front form says "this entity changed its parent"; the value is the new `parentId` (`null`/0 = root). See {@see applyMetaSubtreeMove()}.
public
mixed
META_MOVE_SUBTREE_PARENT
= 'moveSubtreeToParentId'
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().
CONTENT_REWARD_VISIBILITY_PROPERTIES
Entity properties whose change can publish a record; other `updateColumns()` calls are ignored.
private
array<string|int, mixed>
CONTENT_REWARD_VISIBILITY_PROPERTIES
= ['approvedAt', 'confirmedAt', 'banned', 'paid', 'deleted', 'deactivated', 'sold']
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.
$blacklistedWordManager
Set from the constructor; a plain property so unit tests built without the constructor skip the check.
protected
BlacklistedWordManager|null
$blacklistedWordManager
= null
$blacklistManager
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
protected
array<int, string>
$defaultSkipFields
= ['assetRels']
$graphDataKeys
Keys an external client may send nested in `data`; `internalActionSave` moves them to `meta`, where `afterSave` reads them unchanged (dropzone `orderedAssets`, classic mode `existingAssets` + `assets`, `categoryIds` for {@see syncCategories()}).
protected
array<int, string>
$graphDataKeys
= ['orderedAssets', 'existingAssets', 'assets', 'categoryIds']
$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
$lastAssetRejectionCode
Why the last {@see saveAssetVariants()} returned `null` ({@see UploadRejection} `REASON_*`); the caller passes it to the report of rejected items. `null` means the last save passed.
protected
string|null
$lastAssetRejectionCode
= 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
$assetManager read-only
private
AssetManager
$assetManager
$commentThreadManager read-only
private
CommentThreadManager
$commentThreadManager
$contentCreationLimiter
private
ContentCreationLimiter|null
$contentCreationLimiter
= null
$emailManager read-only
private
EmailManager
$emailManager
$entityManager read-only
private
EntityManagerInterface
$entityManager
$esDocumentSource read-only
private
MessageDocumentSource
$esDocumentSource
$esIndexer read-only
private
EsEntityIndexer
$esIndexer
$pendingDeleteAssetIds
private
array<int, int>
$pendingDeleteAssetIds
= []
images of deleted messages to release after commit
$pendingDeleteCommentThreadIds
private
array<int, int>
$pendingDeleteCommentThreadIds
= []
comment threads of the deleted messages, deleted once the message rows are gone
$pendingDeleteMessageIds
private
array<int, int>
$pendingDeleteMessageIds
= []
ids of deleted messages (and replies) whose empty image folders are cleaned after commit
$pendingDeleteRatingThreadIds
private
array<int, int>
$pendingDeleteRatingThreadIds
= []
rating threads of the deleted messages, deleted once the message rows are gone
$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
= []
$userManager read-only
private
UserManager|null
$userManager
= null
$variantProvider read-only
private
AssetVariantProvider
$variantProvider
Methods
__construct()
public
__construct(MessageService $service, AssetManager $assetManager, CommentThreadManager $commentThreadManager, RatingThreadManager $ratingThreadManager, AssetVariantProvider $variantProvider, EntityManagerInterface $entityManager, EsEntityIndexer $esIndexer, MessageDocumentSource $esDocumentSource, EmailManager $emailManager[, UserManager|null $userManager = null ][, BlacklistManager|null $blacklistManager = null ][, BlacklistedWordManager|null $blacklistedWordManager = null ]) : mixed
Parameters
- $service : MessageService
- $assetManager : AssetManager
- $commentThreadManager : CommentThreadManager
- $ratingThreadManager : RatingThreadManager
- $variantProvider : AssetVariantProvider
- $entityManager : EntityManagerInterface
- $esIndexer : EsEntityIndexer
- $esDocumentSource : MessageDocumentSource
- $emailManager : EmailManager
- $userManager : UserManager|null = null
- $blacklistManager : BlacklistManager|null = null
- $blacklistedWordManager : BlacklistedWordManager|null = null
acceptsComments()
The author can switch comments off (`show_comments = 0`, front form); the detail then hides them and a new comment into the post thread is refused (`CommentManager::beforeSave()`, decision of the audit 19. 10.).
public
acceptsComments(ThreadType $type, int $threadId) : bool|null
Parameters
- $type : ThreadType
- $threadId : int
Return values
bool|null —false = refuse, true = allow, null = not my thread type (no opinion)
delete()
Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:
public
delete(int|array<string|int, mixed> $id[, array<string, mixed> $meta = [] ]) : Result
beforeDelete(array $entities, array $meta): ?Resultruns before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows intoafterDeleteunder$meta['__beforeDelete'].afterDelete(array $entities, Result $result, array $meta): ?Resultruns after the delete, still in the transaction. It is only for recomputing derived state from the remaining rows (toped_until, member_until, county), idempotent and reversible. A failure rolls back the delete itself (atomicity). It is also the place for the mandatory delete of dependents through a foreign manager (comment, rating and message threads): an owner still managed in the unit of work references them, so they must go after its row, never before it inbeforeDelete(collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only intocompletedDelete.completedDelete(array $entities, Result $result, array $meta): voidruns best effort after the commit, outside the transaction; an exception is only logged (the delete stays).completedAssetCleanup(array $entities, Result $result, array $meta): voidruns best effort after the commit and releases theAssetrows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in isownedAssetIds(object $entity): arrayon the manager, without it the hook does not run.
IMPORTANT: the cleanup has its own name instead of completedDelete() because many Api managers define
completedDelete() themselves and a class method overrides a trait method without a warning, so the
cleanup would silently be missing in them.
IMPORTANT: a soft delete (deleted = 1) goes through save(), not here; assets must not be released
there because the record can be restored.
$entities are the entities loaded before the delete (after the DELETE the row is gone, but the PHP
object keeps the data, such as FKs for the recomputation).
IMPORTANT: the id is the exception. Doctrine nulls the id of a removed instance on flush(), so a
hook must not rely on getId() once the delete ran (today only the read UoW::clear() in the
service find() hands the preloaded entities over detached and keeps their id). Every hook therefore
gets the ids captured before beforeDelete under the reserved key DELETED_IDS_META, keyed by
the loaded entity object (an input id that loaded nothing has no entry); read it through
getDeletedId(). The key is always set here, so a caller-supplied value never survives.
Without hooks it behaves exactly as before (no preloading, no transaction). NOTE: deleteAll() (bulk) and the tree deleteNode take another
path and do not trigger these hooks.
Parameters
- $id : int|array<string|int, mixed>
- $meta : array<string, mixed> = []
Return values
ResultdeleteAll()
Bulk delete by filters (external admin `:Api:Discussion:Message:deleteAll`) as a series of {@see deleteNode()} calls.
public
deleteAll([array<string|int, mixed> $filters = [] ]) : Result
The inherited deleteAll() deletes rows without delete hooks: comment and rating threads,
images with their files and the ES document would stay orphaned and the nested set would keep
gaps. Here every matched message goes through deleteNode() (shallowest first; a message
already removed with an ancestor's subtree is skipped), then the deleted ids leave ES. Each
node is its own transaction; on a failure the already deleted nodes stay deleted and the
failure carries deletedCount/deletedIds.
Parameters
- $filters : array<string|int, mixed> = []
Return values
ResultdeleteNode()
Deletes a thread in the tree (admin grid `deleteNode`) with its reply subtree.
public
deleteNode(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
IMPORTANT: tree delete goes through service->deleteTreeNode() and deleteAll(), so it runs
without delete hooks; comment and rating threads (the link goes from the message to the thread,
FK SET NULL) and images would be orphaned. The dependents are therefore collected under the
tree lock before the subtree goes (so a concurrent reply cannot slip through; see
AdvertHardDeleteTrait), the threads are deleted by
their managers after the message rows, still inside the transaction, and files after commit.
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
Resultfind()
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|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)
getAppContext()
public
getAppContext() : AppContext
Return values
AppContextgetDefaultSkipFields()
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()
Sets the owner (the scalar `userId`, mapped by a plain `#[ORM\Column]`) from the server on an external INSERT.
public
getExternalOwnerData(int $userId) : array<string, mixed>
Parameters
- $userId : int
-
identity from the JWT (never from the request)
Return values
array<string, mixed>getExternalRemoteIpData()
Sets the author's IP on an external INSERT (`ip` is `#[ExternalReadonly]`).
public
getExternalRemoteIpData(string $ip) : array<string, mixed>
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
PresenterContextgetServiceClass()
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|nullinitializeAllContexts()
public
initializeAllContexts() : void
insertTreeNode()
Computes lft/rgt/level for a newly inserted node (after `save()`, when it has `parent_id` in the database but zero lft/rgt). It is inserted as the last sibling (after the largest `rgt` among nodes with the same `parent_id`).
public
insertTreeNode(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultisOwnedBy()
Returns whether the post `$messageId` belongs to user `$userId`.
public
isOwnedBy(int $messageId, int $userId) : bool
The owner carrier is the scalar Message::$userId (nullable: a guest author), read from the DB
by id. Mirrors the front check in
MessageFormTrait.
NOTE: a post without an account (userId null) belongs to nobody, so this returns false and the
external Api never edits it. A guest edits through the "Manage post" link on the front end: the
internal bridge carries an edit capability bound to Message::$password, which the Api presenter
checks itself (MessagePresenter::ownsByCapability()), not through this method.
Parameters
- $messageId : int
- $userId : int
Return values
boolmarkCategoriesPaid()
Settled payment for categories of topics: marks the paid category relations and the topics paid (raw SQL on the module tables), then rewards and reindexes the topics that just became public. Called by the payment callback ({@see PaymentItemManager::updateStatusForPayment()}).
public
markCategoriesPaid(array<int, array<int, int>> $categoryIdsByMessage) : void
Parameters
- $categoryIdsByMessage : array<int, array<int, int>>
-
message id => paid category ids
moveDown()
Moves after the next sibling.
public
moveDown(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultmoveSubtree()
Moves a whole subtree (root and descendants) under another parent.
public
moveSubtree(int $id, int|null $newParentId[, TreeConfig $config = new TreeConfig() ]) : Result
Updates lft/rgt/level of the subtree and parent_id of its root.
Parameters
- $id : int
- $newParentId : int|null
-
null = move to the root level
- $config : TreeConfig = new TreeConfig()
Return values
ResultmoveSubtreeTo()
Moves a whole subtree under a new parent at a specific position.
public
moveSubtreeTo(int $id, int|null $newParentId, int $afterId[, TreeConfig $config = new TreeConfig() ][, TreeMoveExpectation|null $expectation = null ]) : Result
With $expectation (grid drag and drop) the old parent and the prev/next neighbours are
verified under the tree lock; a mismatch returns TREE_STALE and nothing is written. The
position is given by afterId and must equal expectation->prevId, otherwise
INVALID_ARGUMENT (the client would verify a different place than the one it moves to).
Parameters
- $id : int
- $newParentId : int|null
-
null = root level
- $afterId : int
-
-1 = as first child, 0 = as last child, >0 = after the sibling with this id
- $config : TreeConfig = new TreeConfig()
- $expectation : TreeMoveExpectation|null = null
Return values
ResultmoveUp()
Moves before the previous sibling.
public
moveUp(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultredeemManageKey()
Returns `OK` when the key matches the record and its current password and is still valid. An unknown record, a record with an owner, a wrong or expired key and a key after a password change all return the same code `USER_PASSWORD_RESET_KEY_INVALID`, so the action does not reveal which part did not match.
public
redeemManageKey(int $recordId, string $key) : Result
Parameters
- $recordId : int
- $key : string
Return values
ResultregisterEvent()
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
ResultreorderChildren()
Reorders the direct children of a parent (with whole subtrees) into the given order, in one operation under the tree lock (the "sort children alphabetically" admin grid action; the caller decides the order). A mismatching set of children returns `TREE_STALE` and nothing is written; `data.changed` is the number of moved children.
public
reorderChildren(int|null $parentId, array<int, int> $orderedIds[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $parentId : int|null
-
null = root level
- $orderedIds : array<int, int>
-
ids of all children of the parent in the new order
- $config : TreeConfig = new TreeConfig()
Return values
ResultrewardPublishedContentIds()
Rewards the authors of those given records that are published now.
public
rewardPublishedContentIds(array<string|int, mixed> $ids) : void
Parameters
- $ids : array<string|int, mixed>
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
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
staticsetContentCreationLimiter()
public
setContentCreationLimiter(ContentCreationLimiter $contentCreationLimiter) : void
Parameters
- $contentCreationLimiter : ContentCreationLimiter
setInternalCallContext()
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()
Grid toggles (approved, banned) and the confirmation by key publish a post here, without the save hooks; the credits for content follow ({@see ContentCreditRewardTrait}).
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
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
ResultafterDelete()
Deletes the comment and rating threads collected in {@see beforeDelete()}, inside the delete transaction (a failure rolls the message delete back).
protected
afterDelete(array<int, Message> $entities, Result $result[, array<string, mixed> $meta = [] ]) : Result|null
Parameters
Return values
Result|nullafterMoveSubtree()
Recomputes the thread root of the whole subtree after a move (grid "Move to...", drag and drop, parent change in the form). It runs in the transaction under the move lock, so a failure also rolls back the move itself.
protected
afterMoveSubtree(int $id, Result $result) : Result
Parameters
- $id : int
- $result : Result
Return values
ResultafterSave()
protected
afterSave(bool $isInsert, Message &$entity[, Result|null $result = null ][, array<string, mixed> $meta = [] ]) : Result|null
Parameters
Return values
Result|nullapplyMetaSubtreeMove()
Recomputes the nested set after a parent change. Called from `afterSave()`, so inside the save transaction.
protected
applyMetaSubtreeMove(bool $isInsert, BaseEntity $entity, int|null $entityId, array<string, mixed> $meta) : Result|null
IMPORTANT: it belongs in afterSave() because a form that moved the subtree as a separate
operation after a successful save() left a new parent_id with unrecomputed lft/rgt
when the move failed: a broken tree that could not be repaired from the UI. From here a
failure is returned as a Result and the shared SaveTrait also rolls back the
parent_id save.
The scope of several trees in one table (menu items) is taken from the entity through treeConfigForEntity(); the caller passes nothing.
Returns null when nothing is moved (insert, missing key, entity without id) or when the
move succeeded; otherwise the failed Result.
$entityId is passed separately: BaseEntity declares no id getter (each entity has its
own), so only the concrete manager knows it.
Parameters
- $isInsert : bool
- $entity : BaseEntity
- $entityId : int|null
- $meta : array<string, mixed>
Return values
Result|nullassetCleanupRoots()
Asset roots inside which {@see removeEmptyAssetDirs()} may remove empty directories; never above them.
protected
assetCleanupRoots() : array<int, string>
The gallery root by default. A manager writing another channel into its own root (a company logo next to the gallery, an advert attachment next to the images) lists that root too, otherwise the directories of that channel stayed after a delete (admin walkthrough B8).
Return values
array<int, string> —absolute directories
assetDir()
Returns the asset directory from the server neon `params` (through Config), not from `getSettings()`: the shared setting context may be filled by a client without server paths (see the in-process ApiBridge).
protected
assetDir(int $module, string $key, string $type) : string
An empty path is a configuration error and throws loudly instead of silently writing to a relative path.
Requires $this->config on the manager (BaseManager, set by the decorator).
Parameters
- $module : int
-
module in params (1=System, 3=Blog, 5=Bazaar, 6=Comcat, 8=Discussion)
- $key : string
-
directory key (
advertDir,categoryDir,imgDir...) - $type : string
-
absorrel
Return values
stringassetIdOf()
Normalizes the "current image" to an id. Channels with an ORM association return an entity (even a lazy proxy; `getId()` does not initialize it), channels with a scalar FK return an int.
protected
assetIdOf(mixed $current) : int|null
Parameters
- $current : mixed
Return values
int|nullassetVariantFilePaths()
Returns the absolute paths of the files of the asset just written (variants and the original).
protected
assetVariantFilePaths(string $absBase, string $fileName, array<string, mixed> $variantConfigs) : array<int, string>
Variants are written to disk inside the transaction, before it is certain the change holds, and
rollback() knows nothing about files. On a failed path (a Result failure after which SaveTrait
rolls back) the caller therefore deletes what it just wrote (see the orphan variant cleanup in
app/UI/Admin/Eshop/Components/ProductSetEditComponent/Traits/FormTrait.php).
The search is by $fileName (12 random characters), so files of another asset in the same
directory are never touched.
Parameters
- $absBase : string
- $fileName : string
- $variantConfigs : array<string, mixed>
-
key = variant name = subdirectory
Return values
array<int, string>beforeDelete()
Collects dependents on a direct `delete()` of a message (external Api), while the rows exist.
protected
beforeDelete(array<int, Message> $entities[, array<string, mixed> $meta = [] ]) : Result|null
The DB cascade on parent_id also deletes replies, so dependents are collected for the whole
subtree, as in deleteNode(). The threads themselves go in afterDelete().
Parameters
- $entities : array<int, Message>
- $meta : array<string, mixed> = []
Return values
Result|nullbeforeSave()
Sanitizes the post text on the server (HTMLPurifier), enforces the creation quota and applies initial approval, confirmation, thread root and the derived `reported` flag.
protected
beforeSave(bool $isInsert, Message &$entity[, array<string, mixed> &$meta = [] ]) : Result|null
IMPORTANT: the text is written in a rich editor (Form::addSimpleEditor() in
MessageFormComponent) and printed with |noescape (detail and list style 3), so without
sanitization it is stored XSS. The valid_elements whitelist in rich-editor.js only lives in
the browser and any POST outside the form (or the external Api) bypasses it; the check therefore
sits here, the one place all write paths pass: front form, admin form and external Api.
The allowed tag list is deliberately the same as for advert parameters
(EditorHtmlSanitizer::ALLOWED_HTML) and the client editor: p/br/strong/em/u/span/ul/ol/li
plus tables, with style limited to alignment, colors and borders. No links, images, scripts
or event attributes.
NOTE: subject is deliberately not sanitized: it is plain text always printed escaped, so
purifying would only mangle legitimate texts containing <.
Parameters
- $isInsert : bool
- $entity : Message
- $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> = []
captureOwnedAssetIds()
Ids of the images the given entities own ({@see completedAssetCleanup()} requires the same opt-in `ownedAssetIds()`), captured before the rows are deleted. For paths that bypass {@see DeleteTrait::delete()} and its hooks, such as a subtree delete ({@see TreeTrait::deleteNode()}). Fail-soft: an error of one entity is logged and the others are processed.
protected
captureOwnedAssetIds(iterable<string|int, object> $entities) : array<int, int>
Parameters
- $entities : iterable<string|int, object>
Return values
array<int, int>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>completedAssetCleanup()
Post-commit cleanup of the assets of an entity that was just deleted.
protected
completedAssetCleanup(array<int, object> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void
DeleteTrait::delete() calls it next to completedDelete()
but under its own name, on purpose: many Api managers define completedDelete() themselves (e.g. Blog
ArticleManager, Bazaar/Comcat AdvertManager, Discussion MessageManager, Eshop ProductSetManager)
and a class method overrides a trait method without any warning, so the shared cleanup would silently
vanish in them.
Opt-in: the hook runs only for managers that define ownedAssetIds(object $entity): array, i.e. that
can tell which assets the entity owns. A generic hook cannot guess it, because the FK carrier differs
(asset / imageId / logoId / photoImageId / a relation collection for a gallery).
Post-commit is right here (unlike releaseAsset() on image replacement): the entity is already
deleted, its FK is gone (SET NULL/CASCADE) and the cleanup must not resurrect it. Inside the
transaction it would still see the FK alive and never release the asset.
Fail-soft: the whole body is in try/catch and returns nothing; a failed cleanup must not bring
back the deleted entity.
IMPORTANT: a shared asset is not judged here; AssetManager::releaseAssetRow()
decides, and when anything still points to the asset it stays with its files (releaseAsset()
returns false).
IMPORTANT: a soft delete does not get here. It goes through save(), not delete(), so this hook does
not run, and it must not: a soft-deleted record can be restored and would come back without its image.
Parameters
- $entities : array<int, object>
-
entities loaded before the delete (
DeleteTraitholds them) - $result : Result
- $meta : array<string, mixed> = []
completedDelete()
Deletes the message from ES after commit (fail-open).
protected
completedDelete(array<int, Message> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void
NOTE: tree delete of threads goes through TreeTrait::deleteNode, service->deleteTreeNode and
deleteAll, which does not fire this hook (DeleteTrait hooks run only in delete(id)). Deleted
threads are cleared from the index by the sync orphan cleanup (id in ES without a DB row) or the
nightly reindex. This hook covers only a direct delete() of one message; the bulk
deleteAll() deletes from ES itself.
Parameters
completedSave()
Indexes the message in ES after commit (fail-open).
protected
completedSave(bool $isInsert, Message &$entity[, Result|null $result = null ][, array<string, mixed> $meta = [] ]) : void
Deliberately not in afterSave(): it runs in the transaction and updateColumn() still changes
the DB afterwards (asset, categories by raw SQL, threads); the indexer reads the message fresh
itself (fetchFresh()). MessageDocumentSource indexes only thread roots (level=1) and drops replies.
Parameters
contentRewardAction()
protected
contentRewardAction() : string
Return values
stringcontentRewardCandidatesSql()
Published = approved, confirmed, paid and not banned, deleted or deactivated.
protected
contentRewardCandidatesSql() : string
Return values
stringcontentRewardConnection()
protected
contentRewardConnection() : Connection
Return values
ConnectioncontentRewardUserManager()
protected
contentRewardUserManager() : UserManager|null
Return values
UserManager|nullcreateAssetRel()
protected
createAssetRel(object $parent, Asset $asset, int $order, bool $isMain) : object
Parameters
- $parent : object
- $asset : Asset
- $order : int
- $isMain : bool
Return values
objectcreateCommentThread()
Creates a new {@see CommentThread} and writes its id into `$column` of the entity with id `$entityId` (default `commentThreadId`, a scalar FK; pass the association name for an association). Returns a failed {@see Result} (the caller in `afterSave` may return it, which rolls back), or null when there is nothing to do or everything is fine.
protected
createCommentThread(int|null $entityId, ThreadType $type[, string $column = 'commentThreadId' ]) : Result|null
Parameters
- $entityId : int|null
- $type : ThreadType
- $column : string = 'commentThreadId'
Return values
Result|nullcreateRatingThread()
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
deleteAssetFiles()
Deletes the previously scanned files (see {@see listAssetFilesIn()}). Call only after the FK update to the new Asset succeeded.
protected
deleteAssetFiles(array<int, string> $paths) : void
Parameters
- $paths : array<int, string>
dropForeignExistingItems()
From the `type: existing` items keeps only those whose id is already linked to the record `$parentId` through `$relationProperty` (read from the database). Other types (`new`) stay unchanged. When the links cannot be determined (`null`), no existing item passes: losing the order is better than attaching a foreign file.
protected
dropForeignExistingItems(int $parentId, array<int, TItem> $items, string $relationProperty, string $targetAssociation, string $idKey) : array<int, TItem>
Shared by images (assetRels/asset/imageId) and advert files
(fileRels/file/fileId, AdvertAssetTrait::processFiles()).
Parameters
- $parentId : int
- $items : array<int, TItem>
- $relationProperty : string
- $targetAssociation : string
- $idKey : string
Tags
Return values
array<int, TItem>failureOrNull()
A failed Result to propagate, success is null (the `afterSave` hook returns `?Result`).
protected
failureOrNull(Result $result) : Result|null
Parameters
- $result : Result
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()
getAssetDirAbs()
protected
getAssetDirAbs() : string
Return values
stringgetAssetDirRel()
protected
getAssetDirRel() : string
Return values
stringgetAssetFormatKinds()
Media kinds allowed in the dropzone gallery of this manager. The default is images and video. Vector is left out on purpose: an end-user svg is needless exposure even when the sanitizer passes it; enable it explicitly if needed.
protected
getAssetFormatKinds() : array<int, AssetFormatKind>
Return values
array<int, AssetFormatKind>getAssetFormatRegistry()
Requires `$this->assetFormatRegistry` set by the decorator ({@see \App\Core\Base\Api\Models\Managers\Manager::setAssetFormatRegistry()}).
protected
getAssetFormatRegistry() : AssetFormatRegistry
Return values
AssetFormatRegistrygetAssetMaxBytes()
Upper size limit of a new gallery file in bytes, `null` = no limit. The default has none; adverts and company profiles return {@see \App\Core\Security\Upload\ImageUploadSizeLimit::bytes()}.
protected
getAssetMaxBytes() : int|null
Return values
int|nullgetAssetSubDir()
protected
getAssetSubDir(int $parentId) : string
Parameters
- $parentId : int
Return values
stringgetAssetUsageKey()
protected
getAssetUsageKey() : string|null
Return values
string|nullgetAssetVariantType()
protected
getAssetVariantType() : string
Return values
stringgetDeletedId()
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|nullgetMainAssetColumn()
Name of the property the main (first) gallery image is written to. The default `asset` is an ORM ManyToOne to `Asset` (Message/Advert/Intro). Entities that keep the main image as a flat FK column without an association (Company `Reference::$imageId`) override the hook; otherwise `updateColumn()` would write to a nonexistent property.
protected
getMainAssetColumn() : string
Return values
stringgetService()
Returns the service backing the manager traits.
protected
getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>listAssetFilesIn()
Lists the files lying in the target directory before a new image is written.
protected
listAssetFilesIn(string $absBase) : array<int, string>
Single-image channels (company logo, price list group photo) delete the old file. Deleting it before
the new Asset is saved and the FK updated is a trap: if anything later fails, the database still points
to the old Asset whose files no longer exist. So the old state is only scanned and deleted after
success (see deleteAssetFiles()). Name collisions cannot happen because saveAssetVariants()
generates a random fileName, so the new image lies next to the old one.
Parameters
- $absBase : string
Return values
array<int, string> —absolute paths
orderedAssetsFromMeta()
Turns `meta['orderedAssets']` of the request into the list of dropzone items in the shape {@see processOrderedAssets()} reads. Unknown keys and values of a wrong type are dropped; a present but non-text `path` stays as `''` so the file read rejects it the same way instead of silently skipping it.
protected
static orderedAssetsFromMeta(mixed $value) : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
Parameters
- $value : mixed
Return values
array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>orderedFilesFromMeta()
Turns `meta['files']` of the request into the list of files for `processFiles()`/`processComplaintFiles()`.
protected
static orderedFilesFromMeta(mixed $value) : array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>
An item without a text type is dropped (processing cannot place it anyway); other keys as in
orderedAssetsFromMeta().
Parameters
- $value : mixed
Return values
array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>processOrderedAssets()
Shared template method for processing an ordered array of dropzone assets.
protected
processOrderedAssets(object $entity, array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}> $orderedAssets) : Result|null
IMPORTANT: every database step (soft-remove rels, full replace of rels, main image FK) returns a
Result; a failure is returned to the caller and the next step is skipped. The caller (afterSave)
must pass it on so that SaveTrait::save() rolls back the whole save; otherwise a gallery error is
dropped and the admin gets success with an unchanged or half-changed gallery. null means the gallery
is unchanged or everything passed.
Parameters
- $entity : object
- $orderedAssets : array<int, array{type?: string, imageId?: int, name?: string, data?: string, path?: string, order?: int}>
Return values
Result|nullreadUploadedItem()
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
releaseAsset()
Releases the old asset after an image was cleared or replaced: deletes its `cms_system_images` row and files. Without this step every replacement or removal left an orphaned row.
protected
releaseAsset(int|object|null $current[, int|null $keepAssetId = null ]) : bool
IMPORTANT: call only after a successful updateColumn(), i.e. when the FK no longer points to $assetId.
Shared assets: this method judges nothing, AssetManager::releaseAssetRow()
decides; it derives the references from information_schema and deletes nothing when in doubt.
It runs inside the save transaction of the caller (afterSave), not post-commit:
- the row delete is atomic with clearing or switching the FK; if anything later in
afterSavefails, the rollback restores both, while a post-commit variant is irreversible, - the reference check runs on the same connection, so it sees the FK just written (a post-commit hook would have to read again and could miss another edit),
- several affected managers already define the post-commit
completedSave()themselves (BlogArticleManager, DiscussionMessageManager, EshopProductSetManager), so a trait version would silently not run in them. Files are deleted right after the row, wheredeleteAssetFiles($stalePaths)already stands for a replacement, so no new window appears.
Fail-soft: any cleanup error is only logged (channel assets) and the main save does not fail;
an orphaned row is a smaller defect than a broken edit.
Parameters
- $current : int|object|null
-
old FK (id or
Assetentity); null or 0 means nothing to release - $keepAssetId : int|null = null
-
id that must not be released (the newly written asset)
Return values
boolreleaseDroppedAssets()
Releases the images that disappeared from the record after the relations were replaced (removed in the gallery or form). Call only after the relations and the main image were written successfully; a shared image (still referenced) is not deleted by {@see releaseAsset()}, and a cleanup error does not fail the save.
protected
releaseDroppedAssets(array<int, int> $previousAssetIds, array<int, int> $keptAssetIds) : void
Parameters
- $previousAssetIds : array<int, int>
-
images of the record before the save
- $keptAssetIds : array<int, int>
-
images the record holds after the save
releaseOwnedAssets()
Releases the captured images after the delete commit; a shared image stays ({@see releaseAsset()} → `AssetManager::releaseAssetRow()`), an error is only logged.
protected
releaseOwnedAssets(array<int, int> $assetIds) : void
Parameters
- $assetIds : array<int, int>
removeEmptyAssetDirs()
Removes the directories the delete left empty (`galleries/20/{big,original,thumb}`, `intros/14/1/…`), walking up while a directory is empty and strictly inside one of the asset roots ({@see assetCleanupRoots()}).
protected
removeEmptyAssetDirs(array<int, string> $dirs) : void
Without it every deleted gallery or intro left its empty per-record folders behind. The root itself
and anything outside it are never touched; a directory that is not empty (another asset, a race
with a new upload) simply stays, rmdir() refuses it. A failure is logged, never thrown.
Parameters
- $dirs : array<int, string>
-
directories of the deleted files
resolveAssetExtension()
Normalized extension of an uploaded file, or null when the format is not allowed in this context.
protected
resolveAssetExtension(string $fileName[, array<int, AssetFormatKind> $kinds = [AssetFormatKind::Raster] ]) : string|null
Replaces copied whitelists with a silent fallback to jpg, which would save an svg under the
extension .jpg with xml content.
Parameters
- $fileName : string
- $kinds : array<int, AssetFormatKind> = [AssetFormatKind::Raster]
Return values
string|nullrewardPublishedContentAfterUpdate()
`updateColumns()` hook: rewards only when the write succeeded and touched a column that can publish the record (view counters and similar writes cost nothing).
protected
rewardPublishedContentAfterUpdate(int|array<string|int, mixed> $entityId, array<string, mixed> $values, Result $result) : void
Parameters
- $entityId : int|array<string|int, mixed>
- $values : array<string, mixed>
- $result : Result
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)
saveAssetVariants()
Saves the files to disk (the format processor does the work) and creates the `Asset` entity with the variant plan and real dimensions. A variant `name` in the database equals the config key and the directory on disk.
protected
saveAssetVariants(string $rawData, string $extension, string $absBase, string $relBase, array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array, useWatermark: bool}> $variantConfigs[, array<int, AssetFormatKind> $kinds = [AssetFormatKind::Raster] ]) : Asset|null
The variant plan is always stored, even when the variants are not on disk yet (video without ffmpeg),
so the catch-up cron knows what to add; reality is held by the flag thumbnails_generated.
Returns null when the format is unsupported or the processor rejected it (corrupt image, svg that failed sanitization). The caller skips such a file.
Parameters
- $rawData : string
- $extension : string
- $absBase : string
- $relBase : string
-
$variantConfigs
: array<string, array{maxWidth: int, maxHeight: int, resizeFlags: array
, useWatermark: bool}> - $kinds : array<int, AssetFormatKind> = [AssetFormatKind::Raster]
-
media kinds allowed in this context
Return values
Asset|nulltreeConfigForEntity()
`TreeConfig` used when `save()` computes lft/rgt of a new entity automatically ({@see SaveTrait}).
protected
treeConfigForEntity(BaseEntity $entity) : TreeConfig
The scope is read from the entity through ScopedTreeNodeInterface::getTreeScope():
several trees in one table (for example menu items told apart by menuId) are solved by
implementing the interface on the entity, not by overriding this method. The test targets
the interface, not a concrete class, so it also works for an entity touched through the
admin bridge (which persists the Admin variant, not the Api one; the Api SaveTrait
rewrites only \Front\ to \Api\ in entityClass). A tree with a single numbering per
table (pages) does not implement the interface and gets an empty scope.
Parameters
- $entity : BaseEntity
Return values
TreeConfigapplyDerivedReported()
Keeps `reported` derived from reports (the post has at least one unresolved report).
private
applyDerivedReported(bool $isInsert, Message $entity) : void
MessageReportManager::syncReportedFlags() realigns it after report changes, so saving a
post (the admin form carries the whole entity as loaded, the front edit by the author) must not
overwrite it with a manual or stale value: a new post cannot be reported (false), an existing
one is computed from the reports table. Uses a direct connection query (same transaction as the
save), not the report manager (which depends on this one) and not the stale query cache.
Parameters
- $isInsert : bool
- $entity : Message
applyGuestManageSecret()
A guest post gets a fresh random secret in `password` (a value from the payload is ignored). The "Manage post" link ({@see \App\Core\Traits\Api\Models\Managers\ManageKeyTrait}) and the edit capability are bound to it, like an advert to its password; nobody can type it, so a guest manages the post only through the link. A logged-in author's post is left as it is.
private
applyGuestManageSecret(Message $message) : void
Parameters
- $message : Message
applyInitialApproval()
private
applyInitialApproval(Advert|Advert|Message<mixed, mixed> $entity, int $module) : void
Parameters
applyInitialMessageConfirmation()
Called from `beforeSave()` on insert only. A logged-in author means the post has `userId` (front end: the logged-in identity; external path: the JWT, `userId` is `#[ExternalReadonly]`).
private
applyInitialMessageConfirmation(Message $message, array<string, mixed> &$meta) : void
Parameters
- $message : Message
- $meta : array<string, mixed>
applyInitialPaid()
Initial payment state of a new post, written to the row: a topic (level 1) with categories is paid when at least one of them is free; a reply or a topic without a category is always paid.
private
applyInitialPaid(int $messageId) : bool
Parameters
- $messageId : int
Return values
boolapplyThreadRoot()
Sets `first_parent_id` to the thread root (id of the level-1 message; a root itself has null).
private
applyThreadRoot(Message $entity) : void
Computed here, the one place all write paths pass, from the parent by nested set (the parent is in the DB even on insert). Without it a reply created in the admin or by the external Api would have null and look like a root. A parent change through the form also recomputes the subtree in afterMoveSubtree().
Parameters
- $entity : Message
blacklistRefusal()
private
blacklistRefusal(string|null $email[, string|null $phone = null ][, string|null $ip = null ]) : Result|null
Parameters
- $email : string|null
- $phone : string|null = null
- $ip : string|null = null
Return values
Result|nullbuildManageLink()
"Manage post" link for both insert e-mails. A guest post gets a link with a `manageKey` that opens the edit without login while it is valid (7 days); unlike an advert, the confirmation request carries it too, because a guest post has no password to fall back on. A logged-in author gets the plain edit link (requires login). An empty string means no link (the template then leaves the paragraph out).
private
buildManageLink(Message $message, PresenterContext $presenterContext) : string
Parameters
- $message : Message
- $presenterContext : PresenterContext
Return values
stringbumpDeleteCacheTags()
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>
captureDeletedIds()
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>captureSubtreeAssetIds()
Returns the ids of images owned by the whole subtree; empty when the manager declares no image cleanup.
private
captureSubtreeAssetIds(int $id, TreeConfig $config) : array<int, int>
Parameters
- $id : int
- $config : TreeConfig
Return values
array<int, int>collectMessageDependents()
Remembers the comment and rating threads of the messages and their replies (deleted by {@see deletePendingMessageThreads()}) and the images to release after commit. Read by SQL while the message rows still exist.
private
collectMessageDependents(array<int, int> $messageIds) : void
Parameters
- $messageIds : array<int, int>
columnsInvalidateCache()
private
columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
- $columns : array<int, string>
Return values
boolcompletedMessageDelete()
Post-commit cleanup of deleted messages' images (best-effort; `releaseAsset()` leaves a shared asset and its files alone). An exception goes only to the log, a deleted message is not restored.
private
completedMessageDelete(Result $result) : void
Parameters
- $result : Result
deleteBatch()
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
ResultdeletePendingMessageThreads()
Deletes the collected comment and rating threads through their managers (`DeleteTrait`); comments, ratings and votes go with the thread by DB cascade. Call inside the delete transaction.
private
deletePendingMessageThreads() : Result
IMPORTANT: only after the message rows are deleted. A message still managed in the unit of work
keeps its ManyToOne reference to the thread; once a thread manager removes and flushes the
thread, the next flush would find that reference as a new entity and fail. Deleting the owner
first (FK SET NULL, so the order is free for the DB) makes the reference go with it.
Return values
ResultesDeleteBestEffort()
private
esDeleteBestEffort(array<int, int> $ids) : void
Parameters
- $ids : array<int, int>
guardAnonymousInsert()
Returns `RATE_LIMIT_EXCEEDED` when a guest visitor exceeded the quota of anonymous records of the kind `$scope` (per IP and per normalized `$email`), otherwise `null`. Only an insert by a guest (`$userId` null); a logged-in user is counted by {@see guardContentCreation()}.
private
guardAnonymousInsert(bool $isInsert, string $scope, int|null $userId, string $email) : Result|null
Parameters
- $isInsert : bool
- $scope : string
- $userId : int|null
- $email : string
Return values
Result|nullguardContentCreation()
Returns `RATE_LIMIT_EXCEEDED` when the quota is exceeded, otherwise `null`. Only an insert by a logged-in user is counted (see {@see ContentCreationLimiter::isAllowed()}).
private
guardContentCreation(bool $isInsert, string $scope, int|null $userId) : Result|null
Parameters
- $isInsert : bool
- $scope : string
- $userId : int|null
Return values
Result|nullisActivationMailAllowed()
Tells whether another activation e-mail may be sent to `$email`. Unlike {@see guardContentCreation()} it applies to guests too (it protects the recipient); see {@see ContentCreationLimiter::isActivationMailAllowed()}.
private
isActivationMailAllowed(string $scope, string $email) : bool
Parameters
- $scope : string
- $email : string
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
boolissueManageKey()
Issues the key and returns it in readable form (only for the e-mail link); `null` means the record needs no link or the write failed, and the e-mail then carries a plain edit link (logged-in owner, password, reset).
private
issueManageKey(ManageKeyAwareInterface $record) : string|null
Parameters
- $record : ManageKeyAwareInterface
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>messageService()
private
messageService() : MessageService
Return values
MessageServicemoveTreeNode()
private
moveTreeNode(int $id, int $direction, TreeConfig $config) : Result
Parameters
- $id : int
- $direction : int
- $config : TreeConfig
Return values
ResultremoveEmptyMessageDirs()
Removes empty image folders of deleted messages (`<messageDir>/Y/m/<messageId>/` plus variant subfolders, {@see getAssetSubDir()}). Never higher and only empty ones, like `AdvertHardDeleteTrait::removeEmptyAdvertDirs()`.
private
removeEmptyMessageDirs(array<int, int> $messageIds) : void
Parameters
- $messageIds : array<int, int>
rollbackBatch()
private
rollbackBatch() : void
runTreeTransaction()
The body of {@see treeTransaction()}: a transaction only when none is running, a tag bump after success, an exception means log plus rollback of the own transaction.
private
runTreeTransaction(Closure $operation) : Result
Parameters
- $operation : Closure
Return values
ResultsaveWithHooks()
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
ResultsendInsertEmail()
Sends the author an e-mail after the insert commits: with the confirmation link, or just an insert notification. Sends nothing when confirmation was required but no key was issued (admin confirmed or no e-mail).
private
sendInsertEmail(Message $message, array<string, mixed> $meta) : void
Parameters
- $message : Message
- $meta : array<string, mixed>
sendMessageEmail()
private
sendMessageEmail(Message $message, string $emailKey, string|null $plainKey) : void
Parameters
- $message : Message
- $emailKey : string
- $plainKey : string|null
syncCategories()
private
syncCategories(int $messageId, array<string|int, int> $categoryIds) : void
Parameters
- $messageId : int
- $categoryIds : array<string|int, int>
treeService()
Loud guard against silent failure: tree operations run only when the underlying service implements the method (`TreeTrait` on service, mapper and repository). A missing one is a wiring bug, so a {@see \LogicException} is thrown (logged through `treeTransaction()` / `save()`) instead of a silent `INVALID_OPERATION` that did not show where the problem is.
private
treeService(string $method) : TreeServiceInterface
Parameters
- $method : string
Return values
TreeServiceInterfacetreeTransaction()
Wraps a tree operation in a transaction (when the service supports it): the operations rewrite lft/rgt on many rows and a partial write would break the tree.
private
treeTransaction(Closure $operation[, TreeConfig|null $lockConfig = null ]) : Result
IMPORTANT: opens its own transaction only when none is running. A nested call (typically
applyMetaSubtreeMove() from afterSave inside
SaveTrait::save()) runs in the caller's
transaction and only returns the result; the caller handles commit/rollback.
That way the caller keeps control over commit/rollback and returns the concrete Result
(TREE_NODE_CORRUPTED) instead of a generic save error.
IMPORTANT: tree lock ($lockConfig): when passed, it is taken before the transaction opens
and released only after commit/rollback, so no other operation ever reads bounds this one
has not committed yet, and waiting for the lock does not open a transaction (a timeout
returns TREE_LOCKED, nothing started). The repository takes the lock again under it
(re-entrant GET_LOCK). Without $lockConfig (older callers, for example PageManager)
only the repository locks.
Parameters
- $operation : Closure
- $lockConfig : TreeConfig|null = null
Return values
ResultusesAssetPathTrait()
private
static usesAssetPathTrait(string $class) : bool
Parameters
- $class : string