AbstractResponse
abstract class AbstractResponse implements ResponseInterface (View source)
Shared Response foundation
Brand-neutral base for every registrar Response. It owns the machinery that is identical across brands — the constructor skeleton (template method), command sanitisation, column/record bookkeeping, record iteration and the assembly of the {\CNIC\Paginator} — and leaves the parts that genuinely differ to the concrete subclasses:
- wire hooks: {\CNIC\translate()} / {\CNIC\populate()} (protected),
- factories: {\CNIC\newRecord()} and {\CNIC\newResponseParser()} (protected),
- the brand's own
addColumn()(protected), which has to build its correctly-typed Column before handing it to {\CNIC\registerColumn()}, - the status/code accessors declared on {\CNIC\ResponseInterface} (getCode/getDescription/isError/isSuccess) — each reads a different wire shape,
- the pagination primitives, likewise declared on {\CNIC\ResponseInterface} (getFirstRecordIndex, getLastRecordIndex, getRecordsTotalCount, getRecordsLimitation) — the four methods that read a brand's own pagination metadata off its hash (metadata is not column data since RSRMID-2965 — see {\CNIC\AbstractResponse::$metaKeys}) — which this base deliberately does NOT implement — not even as single-page defaults — so a brand that forgets pagination fails at declaration time instead of silently answering "one page, no next page". The seam is drawn at the wire: a brand answers only what its own metadata says, and every derivation from those four answers is shared and written once — on {\CNIC\Paginator} since RSRMID-2965, which this base only assembles (see {\CNIC\getPagination()}). RSRMID-2943 first collected those derivations here, for the same reason they now sit one step further out: none of them reads a column of its own.
None of the members in those last two groups is declared abstract here: they are interface methods this base simply never implements, so every concrete brand must supply them. Do not add base defaults for the four pagination primitives — see docs/agents/architecture.md for why the seam is drawn there, and tests/ResponsePaginationSeamTest.php, which refuses it.
CNR\Response and IBS\Response both extend this as siblings — mirroring the AbstractClient / AbstractSocketConfig / AbstractResponseTemplateManager / AbstractResponseTranslator pattern — so neither brand is-a the other. The CNR-only capabilities (telemetry, transient/pending status, list-hash) live on CNR\Response via {\CNIC\ExtendedResponseInterface} and are deliberately NOT part of this base, so brands like IBS/Moniker never inherit methods they cannot support.
Properties
| protected array<string, string> | $command | The API Command used within this request |
|
| protected string[] | $sensitiveFields | Command parameter keys that carry sensitive data for this brand (account password, domain authorization code, ...). Their values are masked before the command is stored so they can never be read back (e.g. by custom loggers). Matching is case-insensitive (see sanitizeCommand()), so only the names matter, not their casing. Brand-specific by design: each brand declares the keys it uses, sourced from a single per-brand constant (e.g. |
|
| protected string | $raw | plain API response |
|
| protected array<string, mixed> | $hash | hash representation of plain API response. |
|
| protected non-empty-string | $metaKeys | Regex for the response-level metadata keys this brand's wire format mixes in among the data keys — pagination counters and, on brands that carry them, the transaction-level status fields. |
|
| protected string[] | $columnKeys | Column names available in this response |
|
| protected ColumnInterface[] | $columns | Container of Column Instances |
|
| protected array<string, int> | $columnIndex | Map of column name to its index in the column/columnKeys lists. |
|
| protected RecordInterface[] | $records | Record List (List of rows) |
|
| protected array<string, mixed> | $context | Context data for the response |
|
| protected string | $requestUrl | API request url |
Methods
Constructor
Translate the raw API response into its canonical form.
Parse the translated response into the hash and build the column/record lists from it. Brand-specific because each brand's parser returns a different hash shape (CNR nests columns under PROPERTY, IBS is a flat key => value map).
Instantiate the response parser for this brand.
Instantiate the record type for this brand.
Mask the brand's sensitive command keys (see $sensitiveFields) so their values can never be read back from the response (e.g. by custom loggers).
Assemble the record (row) list from the columns already added via addColumn(). Shared by all brands: each subclass populates the columns with its own Column type beforehand, while the row assembly is identical.
Get context data for the response
Get Request URL
Get Plain API response
Get API response as Hash
Register an already-constructed column into the list bookkeeping.
Add a record to the record list.
Get column by column name
Get Data by Column Name and Index
Get Column Names
Get List of Columns
Get Command used in this request
Get Command used in this request in plain text format
Get the paginator for this response's list window.
Get Record at given index
Get all Records
Get count of rows in this response
Iterate the record list, keyed by record index.
Get a string value from the hash by key, returning a default if not found or not a string
Get an array value from the hash by key, returning an empty array if not found or not an array. The twin of {getHashString()} for the nested blocks a brand's populate() reads (e.g. CNR's PROPERTY).
Is this wire key response metadata rather than data?
Details
__construct(string $raw, array $cmd = [], array $placeholders = [], array $context = [], ResponseParserInterface|null $parser = null, string|null $error = null, ResponseTemplateManagerInterface|null $templates = null)
Constructor
Assembles the response completely: every column and record exists by the time this returns, and nothing afterwards can add one (RSRMID-2939) — see the sealing note on {\CNIC\ResponseInterface}.
The parser is a constructor local, not a property, and reaches
{\CNIC\populate()} as an argument. So does the translated raw response and
the sanitized command. That is deliberate: while populate() read them off
$this, the order of the assignments above it was load-bearing and
enforced by nothing but a comment — moving the $this->command assignment
below the populate() call silently switched the IBS parser to its other
wire branch, because that parser reads the command to choose JSON vs plain
text. Passing them in makes the dependency a signature, so there is no
order left to get wrong. Do not reintroduce a $parser property: nothing
after construction has any use for it.
abstract protected string
translate(string $raw, array $cmd, array $placeholders, string|null $error = null, ResponseTemplateManagerInterface|null $templates = null)
Translate the raw API response into its canonical form.
Brand-specific by the ResponseTranslator each subclass imports; $cmd is already sanitized.
abstract protected void
populate(string $raw, ResponseParserInterface $parser, array $cmd)
Parse the translated response into the hash and build the column/record lists from it. Brand-specific because each brand's parser returns a different hash shape (CNR nests columns under PROPERTY, IBS is a flat key => value map).
Everything it needs arrives as an argument rather than being read off a
half-initialised $this — see the constructor for why. Parse through the
given $parser: instantiating one inline behaves identically and silently
closes the injection seam, which is why the guard is structural
(tests/ResponseParserSeamTest.php).
Called exactly once, from the constructor. It is the only place columns and records are built, so it must finish the job: nothing afterwards can add to either list.
abstract protected ResponseParserInterface
newResponseParser()
Instantiate the response parser for this brand.
Factory hook mirroring {\CNIC\newRecord()} and {\CNIC\AbstractClient::newTransport()}: it supplies the default, and the constructor's $parser argument overrides it — so a substitute parser needs neither reflection nor a subclass. Whichever wins is handed to {\CNIC\populate()}, which must parse through it; instantiating a parser inline there behaves identically and silently closes the seam, which is why the guard is structural (tests/ResponseParserSeamTest.php).
abstract protected RecordInterface
newRecord(array $row)
Instantiate the record type for this brand.
Factory hook for addRecord(). Records share one shape across brands (array<string,mixed>), so every brand currently returns the same shared CNIC\Record — the hook stays abstract nonetheless, because it is the seam a brand needing genuinely different row behaviour would implement, and hard-coding the shared Record here would close it. (Unlike columns, whose value types diverge and so cannot use a param-typed factory at all — see registerColumn().)
protected array
sanitizeCommand(array $cmd)
Mask the brand's sensitive command keys (see $sensitiveFields) so their values can never be read back from the response (e.g. by custom loggers).
Delegates the actual matching/masking to {\CNIC\CommandRedactor::redact()}, which is shared with {\CNIC\AbstractSocketConfig::maskSensitiveCommand()}. Matching is case-insensitive to stay robust against casing differences between what a brand documents and what it actually sends.
protected void
assembleRecords()
Assemble the record (row) list from the columns already added via addColumn(). Shared by all brands: each subclass populates the columns with its own Column type beforehand, while the row assembly is identical.
Replaces the record list rather than appending to it, so calling it twice yields the same rows instead of doubling them (RSRMID-2939). No caller does — each brand's populate() calls it once, at the end — but "assembles the records" is what the name promises, and an append-only version made that promise conditional on a call count nothing enforced.
array
getContext()
Get context data for the response
string
getRequestURL()
Get Request URL
string
getPlain()
Get Plain API response
array
getHash()
Get API response as Hash
protected AbstractResponse
registerColumn(ColumnInterface $col)
Register an already-constructed column into the list bookkeeping.
The bookkeeping ($columns/$columnKeys/$columnIndex) is identical for every brand, and both brands build the same shared CNIC\Column: CNR responses are plaintext (always strings) and IBS/Moniker responses are JSON (arbitrary values, nested arrays and objects included), a difference expressed as a native return type on ColumnInterface::getStringByIndex() rather than a per-brand Column subclass. Each brand's addColumn() still builds its Column locally and hands the finished instance here, so this shared helper never has to construct one itself — see IBS\Response::addColumn()/CNR\Response::addColumn().
A repeated column name is refused rather than half-registered
(RSRMID-2939). The three lists are one data structure: $columns/$columnKeys
are positional while $columnIndex maps a name to one position, so a second
column under an existing name used to append to the first two while the
??= kept the index pointing at the first — leaving getColumns() holding a
column getColumn() could never return, and getColumnKeys() listing a name
twice.
Neither shipped brand can reach the throw, and neither can a substitute parser: both brands derive their column names from array_keys() of the parsed hash, and two distinct PHP array keys cannot stringify to the same name. It guards the invariant against a future brand whose populate() builds its columns some other way — there the collision is a programming error and says so instead of silently desynchronising the three lists.
protected AbstractResponse
addRecord(array $row)
Add a record to the record list.
Protected since RSRMID-2939: a record added after construction changed getRecordsCount() and, through it, the pagination getters IBS derives from it (getRecordsTotalCount/getRecordsLimitation/getLastRecordIndex/ getNumberOfPages) — so a caller could silently repaginate a finished response. Only {\CNIC\assembleRecords()} calls this.
ColumnInterface|null
getColumn(string $columnName)
Get column by column name
mixed
getColumnIndex(string $columnName, int $recordIndex)
Get Data by Column Name and Index
array
getColumnKeys()
Get Column Names
Data columns only. There is nothing left to filter here since
RSRMID-2965: a brand's populate() never registers a metadata key as a
column, so the list this returns is already free of them and the former
getColumnKeys(bool $filterPaginationKeys) — with its preg_grep over
every call — has no work to do. Do not re-add the flag: it existed only
because metadata was mixed into the column pool, and a boolean parameter
on a public interface is the cost that modelling error was charging every
consumer.
array
getColumns()
Get List of Columns
array
getCommand()
Get Command used in this request
string
getCommandPlain()
Get Command used in this request in plain text format
Paginator
getPagination()
Get the paginator for this response's list window.
The one place the four brand primitives meet the shared arithmetic. Every derivation from them — page numbers, the page count, the has-next/ has-previous predicates — lives on {\CNIC\Paginator} since RSRMID-2965, because none of it reads a column, holds state or needs a wire payload: keeping it here meant an offset grid could only be exercised by hand-authoring an API response that carried four integers.
A fresh Paginator per call, over numbers that can no longer change (a response is sealed once constructed), so two callers cannot observe each other and there is no cache to invalidate.
{\CNIC\getRecordsCount()} supplies the fifth member deliberately: it counts the rows this response holds and is {\CNIC\getRecord()}'s bounds authority, so it is the reading that cannot be made to lie by a wire that miscounts.
RecordInterface|null
getRecord(int $recordIndex)
Get Record at given index
array
getRecords()
Get all Records
int
getRecordsCount()
Get count of rows in this response
Traversable
getIterator()
Iterate the record list, keyed by record index.
A fresh ArrayIterator per call, over a list that can no longer change: two
foreach loops over one response therefore see identical rows, in either
order, without a rewind step between them, and neither is observable to the
other. That is the property the removed record cursor could not offer —
see {\CNIC\ResponseInterface} for the full account.
protected string
getHashString(string $key, string $default = "")
Get a string value from the hash by key, returning a default if not found or not a string
protected array
getHashArray(string $key)
Get an array value from the hash by key, returning an empty array if not found or not an array. The twin of {getHashString()} for the nested blocks a brand's populate() reads (e.g. CNR's PROPERTY).
protected bool
isMetaKey(string $key)
Is this wire key response metadata rather than data?
The one place {$metaKeys} is matched, called from each brand's populate() before it registers a column. Shared so that "which keys are metadata" is answered identically for every brand while what those keys are stays brand-specific — see {$metaKeys} for why the two sets must not be merged.