CoolCMS API

AdvertManager extends Manager
in package
implements UserBanStateListener uses AdvertParameterPolicyTrait, BlacklistGuardTrait, AdvertCategoryPolicyTrait, SaveTrait, AssetTrait, AssetProcessingTrait, AdvertRegionAddressTrait, AdvertEmailTrait, ManageKeyTrait, AdvertExpirationTrait, AdvertExpirationNoticeTrait, AdvertAssetTrait, AdvertRelSyncTrait, AdvertModerationNoticeTrait, CategoryRelsPaidSanitizationTrait, RatingThreadCreatorTrait, AdvertHardDeleteTrait, AdvertCurrencyGuardTrait, AdvertInitialApprovalTrait, InitialConfirmationTrait, AdvertParameterEditorHtmlTrait, ContentCreditRewardTrait

Base class of Api managers whose traits delegate to a typed `Service<TEntity>`.

Tags
extends

Table of Contents

Interfaces

UserBanStateListener
Listener notified of the ban state of a user account (decision 19. 10. 2026: the public content of a banned user disappears from the website, the profile only says the user was banned).

Constants

PRECISE_ADDRESS_SETTING_KEY  : string = 'geocoding.precise_address'
Module setting key (dot convention).
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.
EXPIRATION_NOTICE_QUEUE  : string = 'comcat.advert.expiration-notice'
GEOCODING_MISS_META_KEY  : string = '__geocodingMiss'
META_CONFIRMATION_REQUIRED  : string = '__confirmationRequired'
`$meta` key: bool, whether author confirmation was required at insert.

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
Words for {@see AdvertParameterPolicyTrait}; a plain property, unit tests build the manager without the constructor.
$blacklistManager  : BlacklistManager|null
$cacheTagInvalidator  : CacheTagInvalidator|null
Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
$defaultSkipFields  : array<int, string>
$emailManager  : EmailManager
$graphDataKeys  : array<int, string>
Keys an external client may send nested in `data`.
$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
$watchdogManager  : WatchdogManager
$advertService  : AdvertService
$advertStatisticsManager  : AdvertStatisticsManager
$assetManager  : AssetManager
$categoryManager  : CategoryManager
$cronFailureManager  : CronFailureManager
$em  : EntityManagerInterface
$esDocumentSource  : AdvertDocumentSource
$esIndexer  : EsEntityIndexer
$fieldAudienceContext  : FieldAudienceContext|null
$fileManager  : FileManager
$geocodingManager  : GeocodingManager
$messageThreadManager  : MessageThreadManager
$moderationNoticesSuppressed  : bool
Set only for the duration of {@see updateColumnsWithoutNotices()}.
$moderationStateBefore  : array<int, array{approved: bool, banned: bool, deleted: bool, deactivated: bool}>
$parameterManager  : ParameterManager
$pendingHardDeleteFilePaths  : array<int, string>
$pendingHardDeletePlan  : AdvertHardDeletePlan|null
Delete plan in progress, passed from `beforeDelete()` to `afterDelete()` and the post-commit step (same manager instance).
$ratingThreadManager  : RatingThreadManager
$regionManager  : RegionManager
$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
$variantProvider  : AssetVariantProvider
$watchdogEmailRelManager  : WatchdogEmailRelManager

Methods

__construct()  : mixed
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
deleteLongExpired()  : Result
Silently marks (deleted=1) adverts that expired more than M days ago.
extendExpiration()  : Result
Extends the advert's expiration.
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>
Returns the owner data for an external INSERT: the profile owner is the scalar `insertUserId` (mapped by `#[ORM\Column(name: 'insert_user_id')]`), set by the server.
getExternalRemoteIpData()  : array<string, mixed>
Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
getExternalUpdateColumnsAllowlist()  : array<string|int, string>
Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
getGraphDataKeys()  : array<string|int, string>
Returns the keys an external client may send nested in `data`.
getLanguageContext()  : LanguageContext|null
getPresenterContext()  : PresenterContext
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
isOwnedBy()  : bool
Tells whether the profile `$advertId` belongs to user `$userId`.
moderationNoticeKey()  : string|null
Template key of the notice for a state change, or `null` when nothing is sent.
notifyUpcomingExpirations()  : array{notified: int, failed: int, skipped: int, markerFailed: int}
Finds active adverts expiring within N days that were not notified yet, sends the notice e-mail and sets `notice_expiration=1`.
onUserBanState()  : void
Owner ban (decision 19. 10.): the records of a banned user disappear from the website. The generated `active` contains `owner_banned = 0`, so lists, ES, sitemap, watchdogs and counts follow on their own; the column is DB-only (not in the entity), an admin save of the record cannot reset it.
ownershipMatches()  : bool
Tells whether the record owner matches the user (pure, stateless check).
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
rewardPublishedContentIds()  : void
Rewards the authors of those given records that are published now.
sanitizedCategoryRelPaid()  : bool
Pure (stateless) sanitization derivation, extracted so it is testable without a DB.
save()  : Result
setAppContext()  : $this
setAssetFormatRegistry()  : static
setCacheTagInvalidator()  : static
setInternalCallContext()  : static
setLanguageContext()  : $this
setParamsConfig()  : static
setPresenterContext()  : $this
setSettingContext()  : $this
updateAllRelations()  : Result
updateColumn()  : Result
updateColumns()  : Result
Sends the moderation notice also for grid toggles (approved, banned).
updateColumnsWithoutNotices()  : Result
`updateColumns()` without moderation e-mails, for system steps that are not a decision about the advert (account anonymization deactivates the adverts of a deleted account; its owner must not get any more e-mails).
updateRelations()  : Result
advertCategoryTable()  : string
advertHardDeleteSource()  : AdvertHardDeleteSourceInterface
{@see AdvertHardDeleteTrait}: the module service implements the read contract of deletion.
afterDelete()  : Result|null
Runs inside the transaction, after the advert rows are deleted (the cascade already removed `*_advert_file_rels` and the replies). Deletes the reply conversations, the rating threads and the attachment rows; a failure rolls the whole delete back.
afterRegisterEvent()  : Result|null
Records the daily statistics after a view counter on the profile is incremented.
afterSave()  : Result|null
assetCleanupRoots()  : array<int, string>
{@see AssetProcessingTrait::assetCleanupRoots()}: images and attachments of an profile live in two roots (`advertDir`, `advertFileDir`, both `Y/m/d/<id>/`), so a delete clears the empty date folders of both.
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
beforeSave()  : Result|null
beginRejectedUploads()  : array<int, array{name: string, reason: string}>
Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
bumpCacheTags()  : void
Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
bumpCacheTagsExplicit()  : void
Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
bumpCacheTagsForClass()  : void
Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
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.
completedAdvertHardDelete()  : void
Post-commit cleanup, best effort: an exception only goes to the log because a deleted advert is not brought back. Called from the manager's `completedDelete()`.
completedAssetCleanup()  : void
Post-commit cleanup of the assets of an entity that was just deleted.
completedDelete()  : void
Runs after a hard delete commits: attachment and image files, ES index, category counts and cache of cascade-deleted children, all in {@see AdvertHardDeleteTrait::completedAdvertHardDelete()} (best-effort, `DeleteTrait` only logs an exception).
completedSave()  : Result|null
Runs side effects after the transaction commits: e-mails and notifications that must not break an already saved profile.
contentRewardAction()  : string
contentRewardCandidatesSql()  : string
Published = the generated `active` column (approved, confirmed, paid, not banned/deleted/deactivated).
contentRewardConnection()  : Connection
contentRewardUserManager()  : UserManager|null
createAssetRel()  : object
Creates the concrete `*AssetRel` entity linked to the parent and the asset.
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.
getAdvertModuleName()  : string
getAdvertSettingModule()  : int
{@see AdvertRegionAddressTrait::isPreciseAddressEnabled()}: settings module of Comcat is 6.
getAssetDirAbs()  : string
Absolute prefix of the asset directory. Implement via {@see assetDir()} (reads neon `params` through Config), e.g. `return $this->assetDir(5, 'advertDir', 'abs');`.
getAssetDirRel()  : string
Relative prefix of the asset directory. Implement via {@see assetDir()}, e.g. `return $this->assetDir(5, 'advertDir', 'rel');`.
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
The same limit as the front and admin uploader: parameter `max_size`, then setting `maxImageSize`, then 50 MB.
getAssetSubDir()  : string
Relative subdirectory within the asset dir (typically Y/m/d/parentId/ or Y/m/parentId/).
getAssetUsageKey()  : string|null
getAssetVariantType()  : string
Key in assetVariants.neon (parameters.assetVariants), e.g. 'bazaar-adverts', 'discussion-messages', 'catalog-firms'.
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()`.
parameterRuleContextFor()  : array{items: array, rules: list, conditions: list}>, targets: list}>, options: array>}
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.
upsertRegionAddressValue()  : void
{@see AdvertRegionAddressTrait}: type gate over {@see upsertParameterRelValue()}.
advertCategoryIds()  : array<int, int>
Category ids the save writes: `meta[categoryIds]` (front form) and the category relations of the entity.
advertCurrencyRefused()  : Result
applyCoordinates()  : void
Computes the advert coordinates (`latitude`/`longitude` parameters) from the selected municipality, street and postcode through `GeocodingManager`. Runs only when the form sent the parameters (`parametersTouched`); otherwise an external geocoder would be called even for a quick-action save without a form. When nothing is found, the old values stay and the reason is stored in `$meta` for {@see logGeocodingMiss()} (the id of a new advert is known only after the save; a canceled save logs nothing).
applyInitialConfirmation()  : bool
applyRegionAddress()  : void
attachmentAbsolutePath()  : string|null
Returns the absolute path of an attachment from the relative path stored in the database (`/www/modules/<module>/files/items/Y/m/d/<id>/`): the relative attachment directory prefix is replaced by the absolute one (server params, not `getSettings()`).
blacklistRefusal()  : Result|null
bumpDeleteCacheTags()  : void
Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
calculateExpirationDate()  : DateTime|null
Computes the new expiration date.
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.
captureModerationState()  : void
collectParameterRelValues()  : array<int, string>
Returns the first value of each parameter from the merged `parameterRels` collection (`beforeSave` runs after hydration, so the collection already carries the submitted values).
columnsInvalidateCache()  : bool
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.
deleteExistingRatingThreads()  : Result
fetchCategoryForFreeMap()  : array<int, bool>
Raw DBAL: reads `Category::getForFree()` (column `for_free`) for the given categories.
fetchExistingCategoryRelPaidMap()  : array<int, bool>
Raw DBAL: reads `paid` of already existing rel rows (advertId + categoryId), i.e. the pre-save DB state (`beforeSave` runs before flush). Empty on insert (advertId null) and without categories.
guardAdvertPriceCurrency()  : Result|null
guardCategoryPolicy()  : Result|null
guardInsertHasCategory()  : Result|null
Rejects an INSERT of a profile without any category.
guardNewFiles()  : Result|null
Gate for new attachments, called from `beforeSave()`, before the advert and files are written.
guardParameterValues()  : Result|null
Value and option rules apply to every save; required ones only when the save carries the parameters (`$checkRequired`: insert, or the form marked them as sent), since an update without them does not hold the full set.
hardDeleteWatchdogEmailRels()  : Result
initialConfirmationEmailKind()  : string|null
Returns the e-mail kind to send the author after insert commit: `'activation'` (with the confirmation link), `'insert'` (insert notice only) or `null` for nothing (confirmation was required but no key was issued: an admin confirmed or the e-mail is missing).
isAssetPathAllowed()  : bool
Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
isExtensionPaymentRequired()  : bool
Whether a non-privileged caller extends a record that lies in a paid category.
isPreciseAddressEnabled()  : bool
Switch of the module setting `geocoding.precise_address` (off by default in Bazaar, on in Comcat).
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).
loadCategoriesFromMeta()  : array<string|int, Category>
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.
logGeocodingMiss()  : void
Writes a row to `log/geocoding-miss.log` (channel `geocoding-miss`); the host calls it at the start of `completedSave` (after the commit, the advert id already exists). For manual review: advertiser typo versus a municipality missing from the data.
notifyWatchdogs()  : void
Prepares the watchdog e-mail of a new profile and hands it to the matching watchdogs.
parameterRefusal()  : Result
prepareAdvertForAdd()  : Result
prepareAdvertForUpdate()  : Result
processFiles()  : Result|null
Processes the ordered file list (existing and new in one array).
purifyEditorParameterRels()  : void
readModerationState()  : array<int, array{approved: bool, banned: bool, deleted: bool, deactivated: bool}>
reattachRelReferences()  : void
Replaces the reference (non-owner) association of every owned rel row with a fresh managed reference (`em->getReference()`: no query, only an identity map entry) by the same id: `parameterRels[].parameter` and `categoryRels[].category`.
releaseAttachmentRows()  : Result
Deletes the attachment rows nobody else holds and remembers their files for the post-commit cleanup.
reloadInsertedAdvert()  : Advert
Loads the just inserted profile with its parameters for the e-mail and the watchdog.
removeEmptyAdvertDirs()  : void
Removes the empty advert directories after the files are deleted (`…/Y/m/d/<advertId>/` and its variant subdirectories): a directory named by the id of a deleted advert and its direct subdirectories, only when empty. Then the date folders above them (`Y/m/d`) that became empty, never the module root ({@see AssetProcessingTrait::removeEmptyAssetDirs()} with the advert roots; without it an emptied `items/2026/10/22/` stayed, admin walkthrough B8).
resolveExpirationInterval()  : int
Returns the longest category expiration interval; falls back to the default setting (setting 6, key 'advert.expiration.days') when there are no categories or all have interval 0.
rollbackBatch()  : void
sanitizeExternalCategoryRelsPaid()  : void
Entry point called from `beforeSave`. No-op on the internal path (marker missing) and when the `categoryRels` collection is empty (the external client sent no such key).
saveWithHooks()  : Result
Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
sendEditationNotice()  : void
Sends the "advert was edited" e-mail to the advertiser after a successful edit.
sendEmail()  : void
sendExpirationNoticeEmail()  : Result
Sends `CC_ADVERT_EXPIRATION_NOTICE` for one advert. The language is taken primarily from the advert's `insertLanguageId`; a cron run has no front `languageContext` like a regular request.
sendModerationNotices()  : void
syncCategoryRels()  : void
upsertParameterRelValue()  : void
Upserts a parameter value into `$advert->getParameterRels()`: finds an existing rel by the managed reference `$parameter`, otherwise adds a new one.
usesAssetPathTrait()  : bool
watchesModerationColumns()  : bool
Whether `updateColumns()` with these values should capture the state for notices.

Constants

PRECISE_ADDRESS_SETTING_KEY

Module setting key (dot convention).

public string PRECISE_ADDRESS_SETTING_KEY = 'geocoding.precise_address'

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']

META_CONFIRMATION_REQUIRED

`$meta` key: bool, whether author confirmation was required at insert.

private string META_CONFIRMATION_REQUIRED = '__confirmationRequired'

Properties

$service

Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).

public Service<string|int, TEntity> $service

$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

Words for {@see AdvertParameterPolicyTrait}; a plain property, unit tests build the manager without the constructor.

protected BlacklistedWordManager|null $blacklistedWordManager = 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', 'fileRels', 'ratingThreadId', 'viewsDetail', 'viewsList']

$graphDataKeys

Keys an external client may send nested in `data`.

protected array<int, string> $graphDataKeys = ['categoryIds', 'assets', 'files']

internalActionSave() moves them to meta, where the existing sync traits read them unchanged (AdvertRelSyncTrait::syncCategoryRels, AssetProcessingTrait::processOrderedAssets/ AdvertAssetTrait::processFiles, AdvertEmailTrait).

Neither parameterRels nor categoryRels is in this list: they go through a real Doctrine object-graph persist (owned collections, cascade + orphanRemoval), not a meta sync trait. The client sends both in the hydrator shape nested in data:

  • data.parameterRels: [{parameter:{id:{id:X}}, value:'...', value2?:'...'}],
  • data.categoryRels: [{category:{id:{id:X}}, paid:bool}] and hydrateForPersist merges them directly (update by composite PK, insert new ones, orphanRemoval deletes missing ones). paid in categoryRels is server-sanitized on the external path (CategoryRelsPaidSanitizationTrait).

IMPORTANT: an empty array [] is a full replace and orphanRemoval deletes everything; an absent key leaves the collection untouched. Admin always sends categoryRels (empty = legitimate remove-all); front and admin do not send parameterRels when there are no values. Front editing does not send categories as new data: the clone of the loaded profile carries the collection (categoryRels.category.translations in includes) unchanged, so the merge is a no-op. categoryIds (in this list) is the insert-only path of the front form and is ignored on UPDATE.

$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.

$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.

$useTransactionForDelete

protected bool $useTransactionForDelete = true

$useTransactionForSave

protected bool $useTransactionForSave = true

$moderationNoticesSuppressed

Set only for the duration of {@see updateColumnsWithoutNotices()}.

private bool $moderationNoticesSuppressed = false

$moderationStateBefore

private array<int, array{approved: bool, banned: bool, deleted: bool, deactivated: bool}> $moderationStateBefore = []

$pendingHardDeleteFilePaths

private array<int, string> $pendingHardDeleteFilePaths = []

attachment files whose row was actually deleted in the transaction

$pendingHardDeletePlan

Delete plan in progress, passed from `beforeDelete()` to `afterDelete()` and the post-commit step (same manager instance).

private AdvertHardDeletePlan|null $pendingHardDeletePlan = null

$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 = []

Methods

__construct()

public __construct(AdvertService $advertService, CategoryManager $categoryManager, AssetManager $assetManager, FileManager $fileManager, AssetVariantProvider $variantProvider, ParameterManager $parameterManager, EmailManager $emailManager, WatchdogManager $watchdogManager, AdvertStatisticsManager $advertStatisticsManager, RatingThreadManager $ratingThreadManager, EntityManagerInterface $em, EsEntityIndexer $esIndexer, AdvertDocumentSource $esDocumentSource, RegionManager $regionManager, GeocodingManager $geocodingManager, WatchdogEmailRelManager $watchdogEmailRelManager, MessageThreadManager $messageThreadManager, CronFailureManager $cronFailureManager, UserManager $userManager[, FieldAudienceContext|null $fieldAudienceContext = null ][, BlacklistManager|null $blacklistManager = null ][, BlacklistedWordManager|null $blacklistedWordManager = null ]) : mixed
Parameters
$advertService : AdvertService
$categoryManager : CategoryManager
$assetManager : AssetManager
$fileManager : FileManager
$variantProvider : AssetVariantProvider
$parameterManager : ParameterManager
$emailManager : EmailManager
$watchdogManager : WatchdogManager
$advertStatisticsManager : AdvertStatisticsManager
$ratingThreadManager : RatingThreadManager
$em : EntityManagerInterface
$esIndexer : EsEntityIndexer
$esDocumentSource : AdvertDocumentSource
$regionManager : RegionManager
$geocodingManager : GeocodingManager
$watchdogEmailRelManager : WatchdogEmailRelManager
$messageThreadManager : MessageThreadManager
$cronFailureManager : CronFailureManager
$userManager : UserManager
$fieldAudienceContext : FieldAudienceContext|null = null
$blacklistManager : BlacklistManager|null = null
$blacklistedWordManager : BlacklistedWordManager|null = null

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): ?Result runs before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows into afterDelete under $meta['__beforeDelete'].
  • afterDelete(array $entities, Result $result, array $meta): ?Result runs 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 in beforeDelete (collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only into completedDelete.
  • completedDelete(array $entities, Result $result, array $meta): void runs best effort after the commit, outside the transaction; an exception is only logged (the delete stays).
  • completedAssetCleanup(array $entities, Result $result, array $meta): void runs best effort after the commit and releases the Asset rows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in is ownedAssetIds(object $entity): array on 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
Result

deleteAll()

public deleteAll([array<string|int, mixed> $filters = [] ]) : Result
Parameters
$filters : array<string|int, mixed> = []
Return values
Result

deleteLongExpired()

Silently marks (deleted=1) adverts that expired more than M days ago.

public deleteLongExpired([int $limit = 200 ]) : Result

One bulk UPDATE per batch.

IMPORTANT: returns Result; success carries data = ['deleted' => int]. A failed bulk updateColumn() is a failure (the cron must not report ok when nothing was marked) and no reindex runs.

Parameters
$limit : int = 200
Return values
Result

extendExpiration()

Extends the advert's expiration.

public extendExpiration(int $advertId[, bool $afterPayment = false ]) : Result

calculateExpirationDate() computes the new expiration from the current one and the intervals of the advert's categories.

Parameters
$advertId : int
$afterPayment : bool = false
Return values
Result

find()

public find(int $id[, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
$id : int
$includes : string|array<string|int, mixed> = []
Return values
T|null

findAll()

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> : list>) —

entities, 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|null

findRelatedIds()

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)

getDefaultSkipFields()

Returns keys that the generic `service->save()` must not copy onto the managed entity.

public getDefaultSkipFields() : array<string|int, string>

The manager's own afterSave()/beforeSave() sync trait handles them through $meta, not through the object graph. Backed by an optional protected per-manager property. The write Api endpoint (SaveTrait::internalActionSave()) also uses this accessor to strip the same keys from $data before hydration, because defaultSkipFields alone does not protect hydration.

Return values
array<string|int, string>

getEntityClass()

Returns the root Doctrine entity class managed by this manager.

public getEntityClass() : TEntity>

The write Api endpoint (SaveTrait) takes it from here instead of the request, so a client cannot choose the instantiated PHP class.

Return values
TEntity>

getExternalMetaAllowlist()

Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).

public getExternalMetaAllowlist() : array<string|int, string>

The internal path (ApiBridge::internalCall() calling internalActionSave() directly, admin and front) is not limited by this allowlist and reads $meta unchanged. The default is an empty array, so external meta is dropped entirely; actionSave() is reachable through HTTP routing regardless of ApiBridge, hence the strip. Override per manager with the protected property $externalMetaAllowlist, analogous to self::getDefaultSkipFields().

Return values
array<string|int, string>

getExternalOwnerData()

Returns the owner data for an external INSERT: the profile owner is the scalar `insertUserId` (mapped by `#[ORM\Column(name: 'insert_user_id')]`), set by the server.

public getExternalOwnerData(int $userId) : array<string, mixed>

See Manager::getExternalOwnerData() and step 5 in prepareAdvertForAdd() above.

Parameters
$userId : int

identity from the JWT (never from the request)

Return values
array<string, mixed>

getExternalRemoteIpData()

Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.

public getExternalRemoteIpData(string $ip) : array<string, mixed>

The IP field is #[ExternalReadonly], so the external strip removes it and the client cannot forge it. Without filling, an external insert would have no IP and the "anonymous by IP" deduplication and per-IP manager limits would silently stop working. SaveTrait::applyExternalRemoteIp() fills it from IRequest::getRemoteAddress(), never from the request. The default [] means the entity carries no IP; overrides return the entity field name (['insertIp' => $ip]).

Parameters
$ip : string
Return values
array<string, mixed>

getExternalUpdateColumnsAllowlist()

Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.

public getExternalUpdateColumnsAllowlist() : array<string|int, string>

updateColumn on a to-many relation is DELETE-all plus INSERT, so without this filter an external POST with values={userRoles: [...]} would bypass the strip/guard of the save path (privilege escalation). The internal path (ApiBridge::internalCall() calling internalActionUpdateColumns() directly) is not limited. The default is an empty array, so external updateColumns is rejected entirely. Override with the protected property $externalUpdateColumnsAllowlist.

Return values
array<string|int, string>

getGraphDataKeys()

Returns the keys an external client may send nested in `data`.

public getGraphDataKeys() : array<string|int, string>

Before hydration the write Api endpoint (SaveTrait::internalActionSave()) moves them from $data to $meta, where the manager's own afterSave()/beforeSave() sync trait reads them (the same mechanism as internal meta). When the client also sends the same key explicitly in meta, meta wins (compatibility with admin, e.g. the categoryRels collision on Advert). Backed by an optional protected per-manager property, like self::getDefaultSkipFields().

IMPORTANT: promoting a key here does not make its content safe to write as received. When the key carries a sensitive field that the server otherwise dictates (typically a payment or approval flag), the manager must overwrite or verify it server-side on the external path in beforeSave()/afterSave() before its sync trait reads it; otherwise the client sets it itself (privilege escalation). The external path is recognized by $meta['__external'] === true, which only SaveTrait::actionSave() sets (never internalActionSave() alone), so internal calls never get the marker. Example: Bazaar Advert.categoryRels is persisted through the object graph (owned collection outside $graphDataKeys); a client could mark paid itself and get a paid advert without payment, so AdvertManager::beforeSave() overwrites it through CategoryRelsPaidSanitizationTrait::sanitizeExternalCategoryRelsPaid() before flush.

Return values
array<string|int, string>

getServiceClass()

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-string

getSettings()

public getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>

getTotalCount()

public getTotalCount() : int|null
Return values
int|null

isOwnedBy()

Tells whether the profile `$advertId` belongs to user `$userId`.

public isOwnedBy(int $advertId, int $userId) : bool

The single definition of profile ownership on the Api side, consumed by the RBAC gate (AdvertPresenter::verifyOwnership()). The owner carrier is insertUserId, read only from the DB by id.

NOTE: session/password ownership is not and must not be here. A profile without insertUserId (guest author + profile password) belongs to nobody, so the result is false. The front end authorizes it itself with a session hash of the password (EditAdvertFormTrait::checkAutorization()) on the internal path, which the gate does not reach; this method therefore does not restrict anonymous editing on the front end, it only keeps it off the external Api.

Parameters
$advertId : int
$userId : int
Return values
bool

moderationNoticeKey()

Template key of the notice for a state change, or `null` when nothing is sent.

public static moderationNoticeKey(array{approved: bool, banned: bool, deleted: bool, deactivated: bool} $was, array{approved: bool, banned: bool, deleted: bool, deactivated: bool} $now) : string|null
Parameters
$was : array{approved: bool, banned: bool, deleted: bool, deactivated: bool}
$now : array{approved: bool, banned: bool, deleted: bool, deactivated: bool}
Return values
string|null

notifyUpcomingExpirations()

Finds active adverts expiring within N days that were not notified yet, sends the notice e-mail and sets `notice_expiration=1`.

public notifyUpcomingExpirations([int $limit = 50 ]) : array{notified: int, failed: int, skipped: int, markerFailed: int}

When sending fails, the flag stays unset and the next cron run retries. markerFailed = the e-mail was queued but the noticeExpiration marker was not written (the next run sends it again).

An advert whose notice does not go out (e-mail failure, missing address, marker not written) gets a back-off (CronFailureManager) and the selection skips it meanwhile; otherwise 50 such adverts at the head would starve all the others of their notice.

Parameters
$limit : int = 50
Return values
array{notified: int, failed: int, skipped: int, markerFailed: int}

onUserBanState()

Owner ban (decision 19. 10.): the records of a banned user disappear from the website. The generated `active` contains `owner_banned = 0`, so lists, ES, sitemap, watchdogs and counts follow on their own; the column is DB-only (not in the entity), an admin save of the record cannot reset it.

public onUserBanState(int $userId, bool $banned) : void

Idempotent: only records whose flag differs are written, reindexed and recounted.

Parameters
$userId : int
$banned : bool

ownershipMatches()

Tells whether the record owner matches the user (pure, stateless check).

public static ownershipMatches(int|null $recordUserId, int $userId) : bool

Like WatchdogManager::ownershipMatches(). null (password profile / guest author) and 0 never match.

Parameters
$recordUserId : int|null
$userId : int
Return values
bool

redeemManageKey()

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
Result

registerEvent()

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
Result

rewardPublishedContentIds()

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>

sanitizedCategoryRelPaid()

Pure (stateless) sanitization derivation, extracted so it is testable without a DB.

public static sanitizedCategoryRelPaid(int $categoryId, array<int, bool> $existingPaidByCategoryId, array<int, bool> $forFreeByCategoryId) : bool
Parameters
$categoryId : int
$existingPaidByCategoryId : array<int, bool>

(a) DB paid for already existing rels

$forFreeByCategoryId : array<int, bool>

(b) Category::getForFree() for new rels

Return values
bool

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
Result

setParamsConfig()

public setParamsConfig(Config $paramsConfig) : static
Parameters
$paramsConfig : Config
Return values
static

updateAllRelations()

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
Result

updateColumn()

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
Result

updateColumns()

Sends the moderation notice also for grid toggles (approved, banned).

public updateColumns(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result

Those go through updateColumns() without save hooks, so the moderation e-mail is handled here too.

Parameters
$entityId : int|array<string|int, mixed>
$values : array<string, mixed>
Return values
Result

updateColumnsWithoutNotices()

`updateColumns()` without moderation e-mails, for system steps that are not a decision about the advert (account anonymization deactivates the adverts of a deleted account; its owner must not get any more e-mails).

public updateColumnsWithoutNotices(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result
Parameters
$entityId : int|array<string|int, mixed>
$values : array<string, mixed>
Return values
Result

updateRelations()

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
Result

advertCategoryTable()

protected advertCategoryTable() : string
Return values
string

afterDelete()

Runs inside the transaction, after the advert rows are deleted (the cascade already removed `*_advert_file_rels` and the replies). Deletes the reply conversations, the rating threads and the attachment rows; a failure rolls the whole delete back.

protected afterDelete(array<int, object> $entities, Result $result[, array<string, mixed> $meta = [] ]) : Result|null

IMPORTANT: the threads go here, not in beforeDelete(). The adverts of the batch are loaded (managed) before the first row goes and a Comcat advert references its rating thread; a thread removed and flushed by its manager first would make the flush of the next advert delete fail ("a new entity was found through the relationship Advert#ratingThread"). Today only the read UoW::clear() in the thread manager's find() hides that.

Parameters
$entities : array<int, object>
$result : Result
$meta : array<string, mixed> = []
Return values
Result|null

afterRegisterEvent()

Records the daily statistics after a view counter on the profile is incremented.

protected afterRegisterEvent(array<string|int, int> $ids, string $property[, Result|null $result = null ]) : Result|null

Daily statistics are optional (setting advert.statistics.enabled): on weak hosting a large number of rows can cause problems, so nothing is written when it is off.

Statistics are secondary: a failure (recordViews() catches its own errors and returns a Result) must not break the view registration itself, so the result is ignored and null is returned.

Parameters
$ids : array<string|int, int>
$property : string
$result : Result|null = null
Return values
Result|null

afterSave()

protected afterSave(bool $isInsert, Advert &$entity[, Result|null $result = null ][, array<string, mixed> &$meta = [] ]) : Result|null
Parameters
$isInsert : bool
$entity : Advert
$result : Result|null = null
$meta : array<string, mixed> = []
Return values
Result|null

assetCleanupRoots()

{@see AssetProcessingTrait::assetCleanupRoots()}: images and attachments of an profile live in two roots (`advertDir`, `advertFileDir`, both `Y/m/d/<id>/`), so a delete clears the empty date folders of both.

protected assetCleanupRoots() : array<int, string>
Return values
array<int, string>

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

abs or rel

Return values
string

assetIdOf()

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|null

assetVariantFilePaths()

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()

protected beforeDelete(array<int, object> $entities[, array<string, mixed> $meta = [] ]) : Result|null
Parameters
$entities : array<int, object>

adverts loaded by DeleteTrait before the delete

$meta : array<string, mixed> = []
Return values
Result|null

beforeSave()

protected beforeSave(bool $isInsert, Advert &$entity[, array<string, mixed> &$meta = [] ]) : Result|null
Parameters
$isInsert : bool
$entity : Advert
$meta : array<string, mixed> = []
Return values
Result|null

beginRejectedUploads()

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>

completedAdvertHardDelete()

Post-commit cleanup, best effort: an exception only goes to the log because a deleted advert is not brought back. Called from the manager's `completedDelete()`.

protected completedAdvertHardDelete(array<int, object> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void
Parameters
$entities : array<int, object>
$result : Result
$meta : array<string, mixed> = []

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 (DeleteTrait holds them)

$result : Result
$meta : array<string, mixed> = []

completedDelete()

Runs after a hard delete commits: attachment and image files, ES index, category counts and cache of cascade-deleted children, all in {@see AdvertHardDeleteTrait::completedAdvertHardDelete()} (best-effort, `DeleteTrait` only logs an exception).

protected completedDelete(array<int, Advert> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void

NOTE: completedDelete() stays a class method only as the entry point; a class method would override the trait without warning.

Parameters
$entities : array<int, Advert>

preloaded by DeleteTrait before the delete

$result : Result
$meta : array<string, mixed> = []

suppressSearchIndex = bulk imports/migrations (a reindex catches up)

completedSave()

Runs side effects after the transaction commits: e-mails and notifications that must not break an already saved profile.

protected completedSave(bool $isInsert, Advert &$entity[, Result|null $result = null ][, array<string, mixed> &$meta = [] ]) : Result|null

The insert and rels are permanently in the DB by then; SaveTrait::save() only logs a failure (it runs outside the transaction).

Parameters
$isInsert : bool
$entity : Advert
$result : Result|null = null
$meta : array<string, mixed> = []
Return values
Result|null

contentRewardAction()

protected contentRewardAction() : string
Return values
string

contentRewardCandidatesSql()

Published = the generated `active` column (approved, confirmed, paid, not banned/deleted/deactivated).

protected contentRewardCandidatesSql() : string
Return values
string

contentRewardConnection()

protected contentRewardConnection() : Connection
Return values
Connection

createAssetRel()

Creates the concrete `*AssetRel` entity linked to the parent and the asset.

protected abstract createAssetRel(object $parent, Asset $asset, int $order, bool $isMain) : object

The consumer sets it by its entity (setAdvert / setMessage / setFirm).

Parameters
$parent : object
$asset : Asset
$order : int
$isMain : bool
Return values
object

createRatingThread()

Creates a new {@see RatingThread} of the given section and writes its id into `$column` of the entity `$entityId` (default `ratingThreadId`, a scalar FK; pass the association name for an association). Returns a failed {@see Result} (the caller may return it from `afterSave` to roll back), or null.

protected createRatingThread(int|null $entityId, ThreadType $type[, string $column = 'ratingThreadId' ]) : Result|null
Parameters
$entityId : int|null
$type : ThreadType
$column : string = 'ratingThreadId'
Return values
Result|null

deleteAsset()

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
template
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|null

finishRejectedUploads()

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()

getAdvertModuleName()

protected getAdvertModuleName() : string
Return values
string

getAdvertSettingModule()

{@see AdvertRegionAddressTrait::isPreciseAddressEnabled()}: settings module of Comcat is 6.

protected getAdvertSettingModule() : int
Return values
int

getAssetDirAbs()

Absolute prefix of the asset directory. Implement via {@see assetDir()} (reads neon `params` through Config), e.g. `return $this->assetDir(5, 'advertDir', 'abs');`.

protected abstract getAssetDirAbs() : string
Return values
string

getAssetDirRel()

Relative prefix of the asset directory. Implement via {@see assetDir()}, e.g. `return $this->assetDir(5, 'advertDir', 'rel');`.

protected abstract getAssetDirRel() : string
Return values
string

getAssetFormatKinds()

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>

getAssetMaxBytes()

The same limit as the front and admin uploader: parameter `max_size`, then setting `maxImageSize`, then 50 MB.

protected getAssetMaxBytes() : int|null
Return values
int|null

getAssetSubDir()

Relative subdirectory within the asset dir (typically Y/m/d/parentId/ or Y/m/parentId/).

protected abstract getAssetSubDir(int $parentId) : string
Parameters
$parentId : int
Return values
string

getAssetUsageKey()

protected getAssetUsageKey() : string|null
Return values
string|null

getAssetVariantType()

Key in assetVariants.neon (parameters.assetVariants), e.g. 'bazaar-adverts', 'discussion-messages', 'catalog-firms'.

protected abstract getAssetVariantType() : string
Return values
string

getDeletedId()

Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).

protected getDeletedId(array<string, mixed> $meta, object $entity) : int|null
Parameters
$meta : array<string, mixed>
$entity : object
Return values
int|null

getMainAssetColumn()

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
string

getService()

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

parameterRuleContextFor()

protected parameterRuleContextFor(array<int, int> $categoryIds) : array{items: array, rules: list, conditions: list}>, targets: list}>, options: array>}
Parameters
$categoryIds : array<int, int>
Return values
array{items: array, rules: list, conditions: list}>, targets: list}>, options: array>}

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|null

readUploadedItem()

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 path it is checked before reading

Return values
string|null

recordRejectedUpload()

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:

  1. the row delete is atomic with clearing or switching the FK; if anything later in afterSave fails, the rollback restores both, while a post-commit variant is irreversible,
  2. 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),
  3. several affected managers already define the post-commit completedSave() themselves (Blog ArticleManager, Discussion MessageManager, Eshop ProductSetManager), so a trait version would silently not run in them. Files are deleted right after the row, where deleteAssetFiles($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 Asset entity); null or 0 means nothing to release

$keepAssetId : int|null = null

id that must not be released (the newly written asset)

Return values
bool

releaseDroppedAssets()

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|null

rewardPublishedContentAfterUpdate()

`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|null

saveAssets()

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|null

upsertRegionAddressValue()

{@see AdvertRegionAddressTrait}: type gate over {@see upsertParameterRelValue()}.

protected upsertRegionAddressValue(object $advert, object $parameter, string $value) : void
Parameters
$advert : object
$parameter : object
$value : string

advertCategoryIds()

Category ids the save writes: `meta[categoryIds]` (front form) and the category relations of the entity.

private advertCategoryIds(Advert $entity, array<string, mixed> $meta) : array<int, int>
Parameters
$entity : Advert
$meta : array<string, mixed>
Return values
array<int, int>

applyCoordinates()

Computes the advert coordinates (`latitude`/`longitude` parameters) from the selected municipality, street and postcode through `GeocodingManager`. Runs only when the form sent the parameters (`parametersTouched`); otherwise an external geocoder would be called even for a quick-action save without a form. When nothing is found, the old values stay and the reason is stored in `$meta` for {@see logGeocodingMiss()} (the id of a new advert is known only after the save; a canceled save logs nothing).

private applyCoordinates(object $advert, array<string, mixed> &$meta) : void

A coordinate parameter with "always regenerate" (always_regenerate) off is computed only while it is empty.

Parameters
$advert : object
$meta : array<string, mixed>

applyInitialConfirmation()

private applyInitialConfirmation(Advert|Advert|Message<mixed, mixed> $entity, int $module, int|null $ownerUserId, array<string, mixed> &$meta) : bool
Parameters
$entity : Advert|Advert|Message<mixed, mixed>
$module : int

5 = Bazaar, 6 = Comcat, 8 = Discussion

$ownerUserId : int|null
$meta : array<string, mixed>
Return values
bool —

true when a key was issued and the record awaits author confirmation

applyRegionAddress()

private applyRegionAddress(object $advert, array<string, mixed> $meta) : void
Parameters
$advert : object
$meta : array<string, mixed>

attachmentAbsolutePath()

Returns the absolute path of an attachment from the relative path stored in the database (`/www/modules/<module>/files/items/Y/m/d/<id>/`): the relative attachment directory prefix is replaced by the absolute one (server params, not `getSettings()`).

private attachmentAbsolutePath(array{id: int, path: string, fileName: string, fileExtension: string} $file) : string|null
Parameters
$file : array{id: int, path: string, fileName: string, fileExtension: string}
Return values
string|null

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|null

bumpDeleteCacheTags()

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>

calculateExpirationDate()

Computes the new expiration date.

private calculateExpirationDate(DateTime|null $currentExpiration, array<int, object> $categories) : DateTime|null

The base is the current expiration when it is valid and in the future; if it is missing or already passed, today is used. The longest category expiration interval (resolveExpirationInterval()) is added to the base.

Returns null when no valid interval is available; the expiration cannot be determined and the caller keeps the original value or returns an error.

Parameters
$currentExpiration : DateTime|null
$categories : array<int, object>
Return values
DateTime|null

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>

captureModerationState()

private captureModerationState(array<int, int> $advertIds) : void
Parameters
$advertIds : array<int, int>

collectParameterRelValues()

Returns the first value of each parameter from the merged `parameterRels` collection (`beforeSave` runs after hydration, so the collection already carries the submitted values).

private collectParameterRelValues(object $advert) : array<int, string>
Parameters
$advert : object
Return values
array<int, string> —

parameterId => value

columnsInvalidateCache()

private columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
$columns : array<int, string>
Return values
bool

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
Result

deleteExistingRatingThreads()

private deleteExistingRatingThreads(array<int, int> $ratingThreadIds) : Result
Parameters
$ratingThreadIds : array<int, int>
Return values
Result

fetchCategoryForFreeMap()

Raw DBAL: reads `Category::getForFree()` (column `for_free`) for the given categories.

private fetchCategoryForFreeMap(array<string|int, int> $categoryIds) : array<int, bool>
Parameters
$categoryIds : array<string|int, int>
Return values
array<int, bool> —

categoryId => for_free

fetchExistingCategoryRelPaidMap()

Raw DBAL: reads `paid` of already existing rel rows (advertId + categoryId), i.e. the pre-save DB state (`beforeSave` runs before flush). Empty on insert (advertId null) and without categories.

private fetchExistingCategoryRelPaidMap(int|null $advertId, array<string|int, int> $categoryIds) : array<int, bool>
Parameters
$advertId : int|null
$categoryIds : array<string|int, int>
Return values
array<int, bool> —

categoryId => DB paid

guardInsertHasCategory()

Rejects an INSERT of a profile without any category.

private guardInsertHasCategory(Advert $entity, array<string, mixed> $meta) : Result|null

The admin form enforces a category (AdvertCategoryFieldsTrait::guardCategoriesSelected()), but that validates only one producer. Api save() is the common path for the admin form, the front form and external clients; without this gate an empty profile could still be created outside the admin form. A category is not decoration: it carries the parameter fields (name, text, region), so a profile without one has neither the required data nor a place to be displayed.

Reads both legitimate paths by which categories arrive on insert, otherwise the gate would break half the application:

  • meta['categoryIds']: insert-only path of the front form and a simple external client (AdvertRelSyncTrait::syncCategoryRels(), runs only in afterSave),
  • the object-graph collection categoryRels: admin form (applyCategoryRels()) and an external client sending data.categoryRels; the hydrator merges it before beforeSave.

Applies to insert only. UPDATE is deliberately not checked: an empty categoryRels is a legitimate admin remove-all on edit, and removing the last category of an existing profile is a different decision than creating an empty record.

Parameters
$entity : Advert
$meta : array<string, mixed>
Return values
Result|null

guardNewFiles()

Gate for new attachments, called from `beforeSave()`, before the advert and files are written.

private guardNewFiles(array<string, mixed> $meta) : Result|null

One forbidden file rejects the whole save (the user would not notice a silently dropped file). The rule is held by AttachmentFilePolicy; the dropzone is only UX, external clients call the API too.

Parameters
$meta : array<string, mixed>
Return values
Result|null

guardParameterValues()

Value and option rules apply to every save; required ones only when the save carries the parameters (`$checkRequired`: insert, or the form marked them as sent), since an update without them does not hold the full set.

private guardParameterValues(object $advert, array<int, int> $categoryIds, bool $checkRequired) : Result|null
Parameters
$advert : object
$categoryIds : array<int, int>
$checkRequired : bool
Return values
Result|null

hardDeleteWatchdogEmailRels()

private hardDeleteWatchdogEmailRels(array<int, int> $advertIds) : Result
Parameters
$advertIds : array<int, int>
Return values
Result

initialConfirmationEmailKind()

Returns the e-mail kind to send the author after insert commit: `'activation'` (with the confirmation link), `'insert'` (insert notice only) or `null` for nothing (confirmation was required but no key was issued: an admin confirmed or the e-mail is missing).

private initialConfirmationEmailKind(array<string, mixed> $meta) : string|null
Parameters
$meta : array<string, mixed>
Return values
string|null

isAssetPathAllowed()

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
bool

isExtensionPaymentRequired()

Whether a non-privileged caller extends a record that lies in a paid category.

private isExtensionPaymentRequired(Advert $advert) : bool
Parameters
$advert : Advert
Return values
bool

isPreciseAddressEnabled()

Switch of the module setting `geocoding.precise_address` (off by default in Bazaar, on in Comcat).

private isPreciseAddressEnabled() : bool

A missing key means off: Google is not paid for unless enabled on purpose.

Return values
bool

issueManageKey()

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|null

loadCategoriesFromMeta()

private loadCategoriesFromMeta(array<string, mixed> $meta) : array<string|int, Category>
Parameters
$meta : array<string, mixed>
Return values
array<string|int, Category>

loadEntitiesForCacheTags()

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>

logGeocodingMiss()

Writes a row to `log/geocoding-miss.log` (channel `geocoding-miss`); the host calls it at the start of `completedSave` (after the commit, the advert id already exists). For manual review: advertiser typo versus a municipality missing from the data.

private logGeocodingMiss(object $advert, array<string, mixed> $meta) : void
Parameters
$advert : object
$meta : array<string, mixed>

notifyWatchdogs()

Prepares the watchdog e-mail of a new profile and hands it to the matching watchdogs.

private notifyWatchdogs(Advert $advert) : void
Parameters
$advert : Advert

prepareAdvertForAdd()

private prepareAdvertForAdd(Advert $advert, array<string, mixed> &$meta) : Result
Parameters
$advert : Advert
$meta : array<string, mixed>
Return values
Result

prepareAdvertForUpdate()

private prepareAdvertForUpdate(Advert $advert, array<string, mixed> &$meta) : Result
Parameters
$advert : Advert
$meta : array<string, mixed>
Return values
Result

processFiles()

Processes the ordered file list (existing and new in one array).

private processFiles(Advert &$entity, array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}> $orderedFiles) : Result|null

Each item is {type: 'existing', fileId, order} or {type: 'new', name, data|path, order} (path = a file in the dropzone temp folder, internal calls only).

Parameters
$entity : Advert
$orderedFiles : array<int, array{type: string, fileId?: int, name?: string, data?: string, path?: string, order?: int}>
Return values
Result|null

readModerationState()

private readModerationState(array<int, int> $advertIds) : array<int, array{approved: bool, banned: bool, deleted: bool, deactivated: bool}>
Parameters
$advertIds : array<int, int>
Return values
array<int, array{approved: bool, banned: bool, deleted: bool, deactivated: bool}>

reattachRelReferences()

Replaces the reference (non-owner) association of every owned rel row with a fresh managed reference (`em->getReference()`: no query, only an identity map entry) by the same id: `parameterRels[].parameter` and `categoryRels[].category`.

private reattachRelReferences(Advert $advert) : void

The owner back-reference (advert) is left alone; it is part of the object graph (cascade). Defends against GetAllTrait::clearDoctrine(), see beforeSave().

Parameters
$advert : Advert

reloadInsertedAdvert()

Loads the just inserted profile with its parameters for the e-mail and the watchdog.

private reloadInsertedAdvert(Advert $entity) : Advert

After commit the rels are in the DB. The in-memory $entity has parameterRels with only parameterId (no key), so get('name') would be empty.

Parameters
$entity : Advert
Return values
Advert

removeEmptyAdvertDirs()

Removes the empty advert directories after the files are deleted (`…/Y/m/d/<advertId>/` and its variant subdirectories): a directory named by the id of a deleted advert and its direct subdirectories, only when empty. Then the date folders above them (`Y/m/d`) that became empty, never the module root ({@see AssetProcessingTrait::removeEmptyAssetDirs()} with the advert roots; without it an emptied `items/2026/10/22/` stayed, admin walkthrough B8).

private removeEmptyAdvertDirs(AdvertHardDeletePlan $plan, array<int, string> $attachmentDirs) : void
Parameters
$plan : AdvertHardDeletePlan
$attachmentDirs : array<int, string>

resolveExpirationInterval()

Returns the longest category expiration interval; falls back to the default setting (setting 6, key 'advert.expiration.days') when there are no categories or all have interval 0.

private resolveExpirationInterval(array<int, object> $categories) : int
Parameters
$categories : array<int, object>
Return values
int

sanitizeExternalCategoryRelsPaid()

Entry point called from `beforeSave`. No-op on the internal path (marker missing) and when the `categoryRels` collection is empty (the external client sent no such key).

private sanitizeExternalCategoryRelsPaid(Advert $advert, array<string, mixed> $meta) : void
Parameters
$advert : Advert
$meta : array<string, mixed>

saveWithHooks()

Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.

private saveWithHooks(TEntity $entity, array<string, mixed> $meta, array<int, string> $skipFields) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed>
$skipFields : array<int, string>
Return values
Result

sendEditationNotice()

Sends the "advert was edited" e-mail to the advertiser after a successful edit.

private sendEditationNotice(Advert $entity, array<string, mixed> $meta, string $emailKey) : void

Loads the advert fresh with its parameters (the in-memory entity from save has rels without keys, so the name would be empty). Nothing is sent without an advert e-mail.

Parameters
$entity : Advert
$meta : array<string, mixed>
$emailKey : string

sendEmail()

private sendEmail(Advert $advert, array<string, mixed> $meta, string $emailKey) : void
Parameters
$advert : Advert
$meta : array<string, mixed>
$emailKey : string

sendExpirationNoticeEmail()

Sends `CC_ADVERT_EXPIRATION_NOTICE` for one advert. The language is taken primarily from the advert's `insertLanguageId`; a cron run has no front `languageContext` like a regular request.

private sendExpirationNoticeEmail(Advert $advert) : Result
Parameters
$advert : Advert
Return values
Result

sendModerationNotices()

private sendModerationNotices(array<string, mixed> $meta) : void
Parameters
$meta : array<string, mixed>

syncCategoryRels()

private syncCategoryRels(Advert $advert, array<string, mixed> $meta) : void
Parameters
$advert : Advert
$meta : array<string, mixed>

upsertParameterRelValue()

Upserts a parameter value into `$advert->getParameterRels()`: finds an existing rel by the managed reference `$parameter`, otherwise adds a new one.

private upsertParameterRelValue(Advert $advert, Parameter $parameter, string $value) : void

IMPORTANT: $parameter must come from the EM, not be a stub new Parameter()->setId(); on flush Doctrine would treat the stub as a new unmanaged entity on a non-cascaded association.

Parameters
$advert : Advert
$parameter : Parameter
$value : string

usesAssetPathTrait()

private static usesAssetPathTrait(string $class) : bool
Parameters
$class : string
Return values
bool

watchesModerationColumns()

Whether `updateColumns()` with these values should capture the state for notices.

private watchesModerationColumns(array<string, mixed> $values) : bool
Parameters
$values : array<string, mixed>
Return values
bool
On this page

Search results