Response
class Response extends AbstractResponse implements ResponseInterface (View source)
IBS Response
Extends the shared AbstractResponse and supplies only what differs for the IBS platform: the JSON-shaped response parsing (the translate() and populate() hooks), the code/description accessors and the flat (single-page) pagination model. The constructor, column/record bookkeeping, record-cursor navigation and derived pagination are inherited from AbstractResponse.
IBS does NOT provide the CNR-only telemetry/transient-status/list-hash capabilities, so those methods are simply absent here rather than present and throwing. They live on CNR\Response via CNIC\ExtendedResponseInterface; consumers narrow to that interface before using them.
Constants
| private COUNT_KEYS |
The count keys IBS emits alongside a list, as a regex alternation. Kept apart from the rest of {$metaKeys} because on this brand the count keys are also a lookup pattern, not just an exclusion list: the same fact arrives under four different names depending on the endpoint — Domain/List carries "domaincount", Url-/EmailForward/List "total_rules", DnsRecord/List "total_records", Nameserver/List "total_hosts" — so {\CNIC\IBS\getRecordsTotalCount()} has to scan the hash for whichever one is present. Do not "simplify" it into a plain array of names, and do not fold it into $metaKeys: matching that would find "transactid" first. The alternation is anchored by both regexes below so it matches these keys exactly and never as a substring. In particular the loose ".*count" form is avoided on purpose: Domain/Count returns one top-level key per TLD the reseller holds, and ".discount" is a real gTLD, so a key literally named "discount" can occur and must NOT be treated as metadata. "totaldomains" (Domain/Count's grand total) is intentionally not matched either — Domain/Count is a portfolio-structure query, not a list, and its total is meaningful aggregate data. |
| private COUNT_KEY_PATTERN |
Anchored form of {COUNT_KEYS}, for the count-key scan. |
Properties
| protected array<string, string> | $command | The API Command used within this request |
from AbstractResponse |
| protected string[] | $sensitiveFields | IBS carries sensitive data under lower-/camel-case command keys. Declared once in {SensitiveFields::KEYS}, shared with {\CNIC\IBS\SocketConfig}. |
|
| protected string | $raw | plain API response |
from AbstractResponse |
| protected array<string, mixed> | $hash | hash representation of plain API response. |
from AbstractResponse |
| protected non-empty-string | $metaKeys | Regex for IBS's response metadata keys — the count keys above plus the transaction-level fields every IBS response carries at the root (transactid, status, message, code). |
|
| protected string[] | $columnKeys | Column names available in this response |
from AbstractResponse |
| protected ColumnInterface[] | $columns | Container of Column Instances |
from AbstractResponse |
| protected array<string, int> | $columnIndex | Map of column name to its index in the column/columnKeys lists. |
from AbstractResponse |
| protected RecordInterface[] | $records | Record List (List of rows) |
from AbstractResponse |
| protected array<string, mixed> | $context | Context data for the response |
from AbstractResponse |
| protected string | $requestUrl | API request url |
from AbstractResponse |
Methods
Constructor
Translate the raw API response using the IBS translator.
Parse the translated response with the IBS parser and build the columns from it. The IBS parser needs the sanitized command — it reads it to choose between the JSON and plain-text wire shapes, which is why the command arrives as an argument rather than off $this (see AbstractResponse::__construct()). IBS responses are flat key => value maps; each data entry becomes a column, list values kept as-is and anything else wrapped into a single-cell list so the shared record assembly can iterate them.
Instantiate the response parser for this brand (Moniker inherits it).
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.
Register an already-constructed column into the list bookkeeping.
Get Data by Column Name and Index
Get Command used in this request in plain text format
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?
Get API response code.
Get API response description
Check if current API response represents an error case.
Check if current API response represents a success case.
Get Index of first row in this response — 0 for a list, null for a
response that is not one.
Get last record index of the current list query, or null when this
response carries no count key, or carries one that counts nothing.
Get total count of records available for the list query, or null when
this response carries no count key (it is not a list).
Get limit(ation) setting of the current list query — the count of
requested rows — or null when this response carries no count key.
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.
protected string
translate(string $raw, array $cmd, array $placeholders, string|null $error = null, ResponseTemplateManagerInterface|null $templates = null)
Translate the raw API response using the IBS translator.
protected void
populate(string $raw, ResponseParserInterface $parser, array $cmd)
Parse the translated response with the IBS parser and build the columns from it. The IBS parser needs the sanitized command — it reads it to choose between the JSON and plain-text wire shapes, which is why the command arrives as an argument rather than off $this (see AbstractResponse::__construct()). IBS responses are flat key => value maps; each data entry becomes a column, list values kept as-is and anything else wrapped into a single-cell list so the shared record assembly can iterate them.
The metadata entries are skipped (RSRMID-2965) — see {$metaKeys} for which ones and why. That is the whole fix for both of this brand's row defects: uniform row shape, because "status" is no longer a one-cell column landing on row 0 of an n-row list, and 0 records instead of 1 for an empty list, because nothing is left to size a row from.
protected ResponseParserInterface
newResponseParser()
Instantiate the response parser for this brand (Moniker inherits it).
protected RecordInterface
newRecord(array $row)
Instantiate the record type for this brand.
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.
int
getCode()
Get API response code.
IBS returns a numeric "code" on some responses even though it is not part of the public API documentation, e.g. for these requests:
- /Domain/Info?domain=noexistingdomain.com&...
- /unknown/path?... Two shapes occur: a top-level "code", and — since the switch to ResponseFormat=JSON — a per-product code nested under product[0].code (earlier "product_0_code", RTLDEV-16781). When present the code is returned as-is; otherwise it is derived from the status: 200 for a success, 500 for an error (see isError()).
string
getDescription()
Get API response description
bool
isError()
Check if current API response represents an error case.
FAILURE is the only IBS status that signals an error. Every other status means the command itself succeeded — "SUCCESS" for ordinary commands, and for Domain/Check specifically "AVAILABLE"/"UNAVAILABLE", which report the domain's registrability rather than a failure.
Reading the root status only is correct for every observed response
(RSRMID-2972). Both Domain/Create failure shapes recorded in RTLDEV-16781
carry a top-level status=FAILURE: the pre-flight one puts code and
message beside it, the provisioning one puts them under product[0]
instead. What the API omits on a provisioning failure is the code and the
message — never the status. That is why {\CNIC\IBS\getCode()} and
{\CNIC\IBS\getDescription()} need a product[0] fallback and this method does
not.
A payload carrying a status only under product[0] would survive the
translator — {\CNIC\IBS\ResponseTranslator::hasMissingRequiredFields()} is
deliberately lenient — and would then be reported here as a success while
{\CNIC\IBS\getCode()} read the nested failure code, so the two would disagree.
No such response is known to exist, so no branch is added for it: pinning
a shape the API does not emit would be dead code. Reconsider only against
a real capture.
The earlier claim here — that missing or empty statuses are "normalised to FAILURE upstream by ResponseTranslator's fallback templates" — was wrong for a nested-only status and is what made the root-only read look unsafe.
bool
isSuccess()
Check if current API response represents a success case.
The complement of isError(): any non-FAILURE status (SUCCESS, AVAILABLE, UNAVAILABLE, ...) is a success. See isError() for why FAILURE is the sole error signal.
protected Response
addColumn(string $columnName, array $data)
Add a column to the column list
IBS responses are JSON, so column values are arbitrary (nested arrays and objects included); the shared CNIC\Column is used as-is, exactly like CNR\Response::addColumn(). Both brands build their column locally and hand the finished instance to the shared registerColumn() bookkeeping — see registerColumn().
Protected since RSRMID-2939, and called only from populate(): records are assembled from the columns once, at the end of construction, so a column added afterwards was absent from every record — present in getColumns() and getColumnKeys(), invisible to getRecord()/getRecords() and to iteration.
int|null
getFirstRecordIndex()
Get Index of first row in this response — 0 for a list, null for a
response that is not one.
IBS's single page always starts at offset 0, so the only question is
whether this response describes a list at all; the presence of a count key
is what answers it. The former unconditional 0 was a stand-in
(RSRMID-2965) that made every Domain/Info or Domain/Check look like the
first page of a list.
int|null
getLastRecordIndex()
Get last record index of the current list query, or null when this
response carries no count key, or carries one that counts nothing.
count - 1, from the wire count rather than from the record list
(RSRMID-2965). An empty list answers null, not -1: with the metadata no
longer forming a phantom row there is no row for an index to point at, and
-1 was never a usable answer anyway — it was the artefact that made the
old code abandon the count key altogether.
int|null
getRecordsTotalCount()
Get total count of records available for the list query, or null when
this response carries no count key (it is not a list).
No getRecordsCount() (RSRMID-2965): the wire count is the brand's own
answer to "how many are there", and the record count is a property of the
rows this object holds — {\CNIC\AbstractResponse::getRecord()}'s bounds
authority, which must stay grounded in the array it indexes. They agree on
every honest IBS list, and conflating them is what let a phantom row
masquerade as a total.
int|null
getRecordsLimitation()
Get limit(ation) setting of the current list query — the count of
requested rows — or null when this response carries no count key.
IBS has no limit/offset concept: one request returns the whole result set, so the window size is the total and both read the same count key. This is not a stand-in for an absent LIMIT — it is what a single-page brand's limit means — and it keeps the shared derivation answering "1 page, no next page" from arithmetic rather than from a brand special case.