CoolCMS API

AdvertHardDeletePlan
in package

FinalYes

Plan for the hard delete of an advert (Bazaar and Comcat): the advert and all dependent data and files are deleted for good. Soft delete is a different operation (the `deleted` toggle) and does not come here.

Pure logic without DB; it decides three things that can be tested in isolation (tests/Unit/Api/AdvertHardDeletePlanTest.php):

  1. What runs and in which order. The DB cascade (on delete cascade) removes most children (rels, replies, reports, statistics, followers, payment items, watchdog e-mails and their texts). An explicit step is needed only for what the cascade does not cover:
    • before the advert row: the watchdog queue (*_watchdog_email_rel, no action in Bazaar, which made the grid delete fail on the FK);
    • after the row (still in the transaction): private reply conversations (a reply keeps the thread with set null, so the thread would be orphaned with its messages), the rating thread (rating_thread_id has no FK, it would be orphaned with its ratings) and the cms_system_files attachment rows; only once the cascade removed the advert link can it be told reliably whether someone else still holds them. The threads go after the advert row on purpose: an advert still managed by Doctrine references its rating thread, and a thread removed and flushed before it would make the next flush fail ("a new entity was found through the relationship");
    • after commit (best-effort): files on disk, images (releaseAsset guards a shared asset itself), the ES index and recounting adverts in categories.
  2. Accounting data is never deleted. Issued invoices (cms_mod_invoicer_*) and payments (cms_system_payments + the cms_system_payment_items snapshot) have no foreign key to the advert; the only link is the module item cms_mod_*_payment_items (tariff activation for the advert), which is not an accounting document and is deleted with the advert. If a cascade from the advert ever reached an accounting table, the plan refuses (uncoveredForeignKeys()); it must never happen silently.
  3. The schema must not surprise. uncoveredForeignKeys() walks the FK graph from information_schema and returns every relation the deletion could not handle, so a new table with no action on the advert ends in a clear refusal before the first write, not in a DB exception.

Table of Contents

Constants

ACCOUNTING_TABLE_PREFIXES  : array<string|int, mixed> = ['cms_mod_invoicer_', 'cms_system_payments', 'c...
Accounting tables: must not be deleted even by a cascade from the advert. Prefixes (`LIKE 'x%'`).
STEP_ASSETS  : string = 'assets'
STEP_ATTACHMENT_FILES  : string = 'attachmentFiles'
STEP_ATTACHMENT_ROWS  : string = 'attachmentRows'
STEP_CATEGORY_COUNTS  : string = 'categoryCounts'
STEP_MESSAGE_THREADS  : string = 'messageThreads'
STEP_RATING_THREADS  : string = 'ratingThreads'
STEP_SEARCH_INDEX  : string = 'searchIndex'
STEP_WATCHDOG_EMAIL_RELS  : string = 'watchdogEmailRels'

Properties

$advertIds  : array<string|int, mixed>
$assetIds  : array<int, int>
$files  : array<int, array{id: int, path: string, fileName: string, fileExtension: string}>
$messageThreadIds  : array<int, int>
$ratingThreadIds  : array<int, int>

Methods

advertIds()  : array<int, int>
assetIds()  : array<int, int>
explicitlyClearedColumns()  : array<int, string>
Columns whose rows the plan deletes explicitly before the advert row, so their `no action` does not stop the deletion. Derived from the advert table prefix (`cms_mod_<module>_adverts`).
files()  : array<int, array{id: int, path: string, fileName: string, fileExtension: string}>
fromDependents()  : self
isAccountingTable()  : bool
messageThreadIds()  : array<int, int>
ratingThreadIds()  : array<int, int>
stepsAfterAdvertRow()  : array<int, string>
Steps inside the transaction after the advert row is deleted. The threads must come after the row: an advert still managed in the unit of work references its rating thread, so a thread removed first would break the flush of the advert delete. A step without data is skipped.
stepsAfterCommit()  : array<int, string>
Steps after commit (best-effort; a deleted advert does not come back). Files go after commit: a rollback knows nothing about the disk, and deleting a file earlier would leave a dangling DB reference on failure.
stepsBeforeAdvertRow()  : array<int, string>
Steps inside the transaction before the advert row is deleted. The order is a contract: the watchdog queue must disappear before the cascade touches `*_watchdog_emails` (Bazaar `no action`). A step without data is skipped.
uncoveredForeignKeys()  : array<int, string>
Returns the relations the hard delete of an advert could not handle. An empty array means the schema is covered.
__construct()  : mixed
ids()  : array<int, int>
positiveInt()  : int|null
stringOf()  : string

Constants

ACCOUNTING_TABLE_PREFIXES

Accounting tables: must not be deleted even by a cascade from the advert. Prefixes (`LIKE 'x%'`).

public array<string|int, mixed> ACCOUNTING_TABLE_PREFIXES = ['cms_mod_invoicer_', 'cms_system_payments', 'cms_system_payment_items', 'cms_system_payment_transactions']

Properties

$files read-only

private array<int, array{id: int, path: string, fileName: string, fileExtension: string}> $files

Methods

explicitlyClearedColumns()

Columns whose rows the plan deletes explicitly before the advert row, so their `no action` does not stop the deletion. Derived from the advert table prefix (`cms_mod_<module>_adverts`).

public static explicitlyClearedColumns(string $advertTable) : array<int, string>
Parameters
$advertTable : string
Return values
array<int, string> —

table.column in lowercase

files()

public files() : array<int, array{id: int, path: string, fileName: string, fileExtension: string}>
Return values
array<int, array{id: int, path: string, fileName: string, fileExtension: string}>

fromDependents()

public static fromDependents(array<int|string, mixed> $advertIds, array{messageThreadIds?: array, ratingThreadIds?: array, assetIds?: array, files?: array} $dependents) : self
Parameters
$advertIds : array<int|string, mixed>
$dependents : array{messageThreadIds?: array, ratingThreadIds?: array, assetIds?: array, files?: array}

raw data from the repository (DBAL returns numbers as strings)

Return values
self

isAccountingTable()

public static isAccountingTable(string $table) : bool
Parameters
$table : string
Return values
bool

messageThreadIds()

public messageThreadIds() : array<int, int>
Return values
array<int, int>

ratingThreadIds()

public ratingThreadIds() : array<int, int>
Return values
array<int, int>

stepsAfterAdvertRow()

Steps inside the transaction after the advert row is deleted. The threads must come after the row: an advert still managed in the unit of work references its rating thread, so a thread removed first would break the flush of the advert delete. A step without data is skipped.

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

stepsAfterCommit()

Steps after commit (best-effort; a deleted advert does not come back). Files go after commit: a rollback knows nothing about the disk, and deleting a file earlier would leave a dangling DB reference on failure.

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

stepsBeforeAdvertRow()

Steps inside the transaction before the advert row is deleted. The order is a contract: the watchdog queue must disappear before the cascade touches `*_watchdog_emails` (Bazaar `no action`). A step without data is skipped.

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

uncoveredForeignKeys()

Returns the relations the hard delete of an advert could not handle. An empty array means the schema is covered.

public static uncoveredForeignKeys(string $advertTable, iterable<string|int, array{table: string, column: string, referencedTable: string, deleteRule: string}> $foreignKeys) : array<int, string>

Walks the FK graph from the advert table through cascades (what a cascade deletes must have its own children covered too):

  • cascade: the child is deleted, the walk continues into its children; a cascade into an accounting table is an error,
  • set null: the child survives with an empty link, fine,
  • no action / restrict: the DB would stop the deletion; fine only when the plan deletes those rows explicitly beforehand (explicitlyClearedColumns()).
Parameters
$advertTable : string

e.g. cms_mod_bazaar_adverts

$foreignKeys : iterable<string|int, array{table: string, column: string, referencedTable: string, deleteRule: string}>
Return values
array<int, string> —

description of uncovered relations (table.column → parent (RULE))

__construct()

private __construct(array<int, int> $advertIds, array<string|int, mixed> $messageThreadIds, array<string|int, mixed> $ratingThreadIds, array<string|int, mixed> $assetIds, array<string|int, mixed> $files) : mixed
Parameters
$advertIds : array<int, int>
$messageThreadIds : array<string|int, mixed>
$ratingThreadIds : array<string|int, mixed>
$assetIds : array<string|int, mixed>
$files : array<string|int, mixed>

ids()

private static ids(array<int|string, mixed> $values) : array<int, int>
Parameters
$values : array<int|string, mixed>
Return values
array<int, int>

positiveInt()

private static positiveInt(mixed $value) : int|null
Parameters
$value : mixed
Return values
int|null

stringOf()

private static stringOf(mixed $value) : string
Parameters
$value : mixed
Return values
string
On this page

Search results