119 lines
5.5 KiB
PHP
119 lines
5.5 KiB
PHP
<?php
|
|||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace App\Domain\Client;
|
||
|
|
|
||
|
|
require_once __DIR__ . '/ContactUpdateCommand.php';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Storage-agnostic command for safe contact edits, primary promotion and deletion.
|
||
|
|
* Persistence adapters can apply the returned replacement IDs transactionally.
|
||
|
|
*/
|
||
|
|
final class ContactEditCommand
|
||
|
|
{
|
||
|
|
public function __construct(private readonly ?ContactUpdateCommand $contacts = null)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string,mixed> */
|
||
|
|
public function validateEdit(int $id, array $input, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
$result = ($this->contacts ?? new ContactUpdateCommand())->validateForEdit($id, $input, $existingContacts);
|
||
|
|
if ($id < 1) {
|
||
|
|
$result['errors']['id'] = 'Contact ID must be a positive integer.';
|
||
|
|
$result['valid'] = false;
|
||
|
|
}
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Alias matching the create/update command vocabulary. */
|
||
|
|
public function validateForEdit(int $id, array $input, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
return $this->validateEdit($id, $input, $existingContacts);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string,mixed> */
|
||
|
|
public function validate(array $input, array $existingContacts = [], ?int $currentId = null): array
|
||
|
|
{
|
||
|
|
return $currentId === null
|
||
|
|
? ($this->contacts ?? new ContactUpdateCommand())->validateForCreate($input, $existingContacts)
|
||
|
|
: $this->validateEdit($currentId, $input, $existingContacts);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate logical deletion. A primary is promoted to the lowest remaining
|
||
|
|
* contact for the same client; deleting the sole contact is not allowed.
|
||
|
|
* @return array<string,mixed>
|
||
|
|
*/
|
||
|
|
public function validateDelete(int $id, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
$errors = [];
|
||
|
|
$target = null;
|
||
|
|
foreach ($existingContacts as $contact) {
|
||
|
|
if (is_array($contact) && $this->id($contact['id'] ?? null) === $id) {
|
||
|
|
$target = $contact;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ($id < 1) $errors['id'] = 'Contact ID must be a positive integer.';
|
||
|
|
if ($target === null) {
|
||
|
|
$errors['id'] = 'Contact was not found.';
|
||
|
|
return ['valid' => false, 'id' => $id, 'client_id' => null, 'replacement_primary_contact_id' => null, 'errors' => $errors];
|
||
|
|
}
|
||
|
|
$clientId = $this->id($target['client_id'] ?? null);
|
||
|
|
if ($clientId === null) {
|
||
|
|
$errors['client_id'] = 'Contact client ID must be a positive integer.';
|
||
|
|
return ['valid' => false, 'id' => $id, 'client_id' => null, 'replacement_primary_contact_id' => null, 'errors' => $errors];
|
||
|
|
}
|
||
|
|
$sameClient = array_values(array_filter($existingContacts, fn ($row): bool => is_array($row) && $this->id($row['client_id'] ?? null) === $clientId && $this->id($row['id'] ?? null) !== $id));
|
||
|
|
$replacement = null;
|
||
|
|
if (count($sameClient) === 0) {
|
||
|
|
$errors['delete'] = 'The only contact cannot be deleted; add another contact first.';
|
||
|
|
} elseif ($this->boolean($target['is_primary'] ?? false)) {
|
||
|
|
usort($sameClient, fn (array $a, array $b): int => ($this->id($a['id'] ?? null) ?? PHP_INT_MAX) <=> ($this->id($b['id'] ?? null) ?? PHP_INT_MAX));
|
||
|
|
$replacement = isset($sameClient[0]) ? $this->id($sameClient[0]['id'] ?? null) : null;
|
||
|
|
if ($replacement === null) $errors['delete'] = 'The only contact cannot be deleted; add another contact first.';
|
||
|
|
}
|
||
|
|
return ['valid' => $errors === [], 'id' => $id, 'client_id' => $clientId, 'replacement_primary_contact_id' => $replacement, 'errors' => $errors];
|
||
|
|
}
|
||
|
|
|
||
|
|
public function validateForDelete(int $id, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
return $this->validateDelete($id, $existingContacts);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string,mixed> */
|
||
|
|
public function delete(int $id, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
return $this->validateDelete($id, $existingContacts);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string,mixed> */
|
||
|
|
public function validatePrimary(int $id, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
foreach ($existingContacts as $row) {
|
||
|
|
if (is_array($row) && $this->id($row['id'] ?? null) === $id) {
|
||
|
|
$clientId = $this->id($row['client_id'] ?? null);
|
||
|
|
$demote = [];
|
||
|
|
foreach ($existingContacts as $other) if (is_array($other) && $this->id($other['client_id'] ?? null) === $clientId && $this->id($other['id'] ?? null) !== $id && $this->boolean($other['is_primary'] ?? false)) $demote[] = $this->id($other['id'] ?? null);
|
||
|
|
return ['valid' => true, 'id' => $id, 'client_id' => $clientId, 'replace_primary_contact_ids' => array_values(array_filter($demote)), 'errors' => []];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ['valid' => false, 'id' => $id, 'client_id' => null, 'replace_primary_contact_ids' => [], 'errors' => ['id' => 'Contact was not found.']];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string,mixed> */
|
||
|
|
public function setPrimary(int $id, array $existingContacts = []): array
|
||
|
|
{
|
||
|
|
return $this->validatePrimary($id, $existingContacts);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function id(mixed $value): ?int
|
||
|
|
{
|
||
|
|
if (is_int($value) && $value > 0) return $value;
|
||
|
|
if (is_string($value) && preg_match('/^[1-9]\d*$/', trim($value)) === 1) return filter_var(trim($value), FILTER_VALIDATE_INT) ?: null;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
private function boolean(mixed $value): bool { return $value === true || $value === 1 || (is_string($value) && in_array(strtolower(trim($value)), ['1','true','yes','on'], true)); }
|
||
|
|
}
|