$credentials */ public function __construct( public readonly string $id, public readonly string $type, public readonly string $label, public readonly array $credentials = [], public readonly ?string $accountIdentity = null, public readonly ?string $checkedAt = null, public readonly ?string $checkOutcome = null, ) { } /** * @param array $stored */ public static function fromArray(string $id, array $stored): self { return new self( $id, (string) ($stored['type'] ?? ''), (string) ($stored['label'] ?? ''), is_array($stored['credentials'] ?? null) ? $stored['credentials'] : [], is_string($stored['account_identity'] ?? null) ? $stored['account_identity'] : null, is_string($stored['checked_at'] ?? null) ? $stored['checked_at'] : null, is_string($stored['check_outcome'] ?? null) ? $stored['check_outcome'] : null, ); } /** * @return array{type: string, label: string, credentials: array} */ public function toArray(): array { return ['type' => $this->type, 'label' => $this->label, 'credentials' => $this->credentials, 'account_identity' => $this->accountIdentity, 'checked_at' => $this->checkedAt, 'check_outcome' => $this->checkOutcome]; } /** * The Connection as REST may return it: only configured flags, with keys * kept so the admin can render the fields it has. * * A field is reported as filled or empty and never as its value. That is * the whole of the protection described in the class docblock, and it is * here rather than in the controller so a second caller cannot forget it. * * @return array{id: string, type: string, label: string, credentials: array>, account_identity: ?string, checked_at: ?string, check_outcome: ?string} */ public function masked(): array { $configured = []; foreach ($this->credentials as $field => $value) { // A list of headers reports its names, which the merchant needs to edit it; never its values. $configured[(string) $field] = is_array($value) ? array_values(array_filter(array_map(static fn ($pair) => is_array($pair) ? ($pair['name'] ?? null) : null, $value), 'is_string')) : $value !== null && $value !== ''; } // The identity is what a merchant recognises — a webhook's host, never its URL — and the admin picks a logo by it. return ['id' => $this->id, 'type' => $this->type, 'label' => $this->label, 'credentials' => $configured, 'account_identity' => $this->accountIdentity, 'checked_at' => $this->checkedAt, 'check_outcome' => $this->checkOutcome]; } }