refactor(customfield): interface-based DI for repositories
Align CustomField-Schicht mit core/Repository/Access-Muster. Vier Repositories (Tenant-Definition, Tenant-Option, User-Value, User-Value-Option) bekommen je ein Interface, werden von static auf instance umgestellt und ueber CustomFieldRepositoryFactory injiziert. TenantCustomFieldService und UserCustomFieldValueService injizieren die Repository-Interfaces statt statisch aufzurufen. CustomFieldServicesFactory reicht Instanzen durch. AddressBookService (Modul) bekommt TenantCustomFieldOptionRepositoryInterface per Container — Interface-Kopplung verbessert Modul-Isolation gegenueber dem vorherigen statischen Aufruf. Kein Verhaltenswechsel, keine SQL-Aenderung, keine Service-Signatur-Aenderung. Alle tenant_id-Parameter erhalten (GR-SEC-009, Security-Review SR-002). Oeffnet den Weg fuer Cluster B1 (TenantCustomFieldService-Tests) und B2 (UserCustomFieldValueService-Tests), die nun mit createMock() moeglich sind. Nebenbei zwei PHPStan-Folge-Findings korrigiert: - Veralteter Ignore-Eintrag fuer findById aus phpstan-baseline.neon entfernt - Redundanter is_array-Check in TenantCustomFieldOptionRepository entfernt (durch typisierte Interface-Signatur abgedeckt) Gates: QG-001 (1945 Tests, 28581 Assertions) / QG-002 / QG-003 / QG-006 pass. Workflow: .agents/runs/CUSTOMFIELD-DI-REFACTOR-001/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace MintyPHP\Service\CustomField;
|
||||
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepository;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
|
||||
use MintyPHP\Repository\Tenant\TenantRepository;
|
||||
|
||||
class TenantCustomFieldService
|
||||
@@ -11,7 +11,9 @@ class TenantCustomFieldService
|
||||
private const ALLOWED_TYPES = ['text', 'textarea', 'select', 'multiselect', 'boolean', 'date'];
|
||||
|
||||
public function __construct(
|
||||
private readonly TenantRepository $tenantRepository
|
||||
private readonly TenantRepository $tenantRepository,
|
||||
private readonly TenantCustomFieldDefinitionRepositoryInterface $definitionRepository,
|
||||
private readonly TenantCustomFieldOptionRepositoryInterface $optionRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -22,14 +24,14 @@ class TenantCustomFieldService
|
||||
return [];
|
||||
}
|
||||
|
||||
$definitions = TenantCustomFieldDefinitionRepository::listByTenantId($tenantId, false);
|
||||
$definitions = $this->definitionRepository->listByTenantId($tenantId, false);
|
||||
if (!$definitions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$definitionIds = array_map(static fn (array $row): int => (int) ($row['id'] ?? 0), $definitions);
|
||||
$definitionIds = array_values(array_filter($definitionIds, static fn (int $id): bool => $id > 0));
|
||||
$options = TenantCustomFieldOptionRepository::listByDefinitionIds($definitionIds, false);
|
||||
$options = $this->optionRepository->listByDefinitionIds($definitionIds, false);
|
||||
$optionsByDefinition = [];
|
||||
foreach ($options as $option) {
|
||||
$definitionId = (int) ($option['definition_id'] ?? 0);
|
||||
@@ -57,18 +59,18 @@ class TenantCustomFieldService
|
||||
|
||||
$form = self::sanitizeForm($input);
|
||||
$form['tenant_id'] = $tenantId;
|
||||
$form['field_key'] = self::resolveFieldKey(
|
||||
$form['field_key'] = $this->resolveFieldKey(
|
||||
$tenantId,
|
||||
(string) ($form['label'] ?? ''),
|
||||
null,
|
||||
(string) ($form['field_key'] ?? '')
|
||||
);
|
||||
$errors = self::validateForm($form, null);
|
||||
$errors = $this->validateForm($form, null);
|
||||
if ($errors) {
|
||||
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
||||
}
|
||||
|
||||
$definitionId = TenantCustomFieldDefinitionRepository::create([
|
||||
$definitionId = $this->definitionRepository->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'field_key' => $form['field_key'],
|
||||
'label' => $form['label'],
|
||||
@@ -76,7 +78,7 @@ class TenantCustomFieldService
|
||||
'is_required' => $form['is_required'],
|
||||
'is_filterable' => $form['is_filterable'],
|
||||
'active' => $form['active'],
|
||||
'sort_order' => $form['sort_order'] ?? self::nextSortOrder($tenantId),
|
||||
'sort_order' => $form['sort_order'] ?? $this->nextSortOrder($tenantId),
|
||||
'created_by' => $currentUserId > 0 ? $currentUserId : null,
|
||||
]);
|
||||
if (!$definitionId) {
|
||||
@@ -84,7 +86,7 @@ class TenantCustomFieldService
|
||||
}
|
||||
|
||||
if (in_array($form['type'], ['select', 'multiselect'], true)) {
|
||||
TenantCustomFieldOptionRepository::replaceForDefinition((int) $definitionId, $form['options']);
|
||||
$this->optionRepository->replaceForDefinition((int) $definitionId, $form['options']);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'id' => (int) $definitionId, 'form' => $form];
|
||||
@@ -97,25 +99,25 @@ class TenantCustomFieldService
|
||||
return ['ok' => false, 'errors' => [t('Custom field not found')]];
|
||||
}
|
||||
|
||||
$existing = TenantCustomFieldDefinitionRepository::findByUuid($uuid);
|
||||
$existing = $this->definitionRepository->findByUuid($uuid);
|
||||
if (!$existing) {
|
||||
return ['ok' => false, 'errors' => [t('Custom field not found')]];
|
||||
}
|
||||
|
||||
$form = self::sanitizeForm($input);
|
||||
$form['tenant_id'] = (int) ($existing['tenant_id'] ?? 0);
|
||||
$form['field_key'] = self::resolveFieldKey(
|
||||
$form['field_key'] = $this->resolveFieldKey(
|
||||
(int) ($existing['tenant_id'] ?? 0),
|
||||
(string) ($form['label'] ?? ''),
|
||||
(int) ($existing['id'] ?? 0),
|
||||
(string) ($existing['field_key'] ?? '')
|
||||
);
|
||||
$errors = self::validateForm($form, (int) ($existing['id'] ?? 0));
|
||||
$errors = $this->validateForm($form, (int) ($existing['id'] ?? 0));
|
||||
if ($errors) {
|
||||
return ['ok' => false, 'errors' => $errors, 'form' => $form];
|
||||
}
|
||||
|
||||
$updated = TenantCustomFieldDefinitionRepository::update((int) $existing['id'], [
|
||||
$updated = $this->definitionRepository->update((int) $existing['id'], [
|
||||
'field_key' => $form['field_key'],
|
||||
'label' => $form['label'],
|
||||
'type' => $form['type'],
|
||||
@@ -131,9 +133,9 @@ class TenantCustomFieldService
|
||||
|
||||
// When type is changed away from select/multiselect, remove orphaned option rows.
|
||||
if (in_array($form['type'], ['select', 'multiselect'], true)) {
|
||||
TenantCustomFieldOptionRepository::replaceForDefinition((int) $existing['id'], $form['options']);
|
||||
$this->optionRepository->replaceForDefinition((int) $existing['id'], $form['options']);
|
||||
} else {
|
||||
TenantCustomFieldOptionRepository::deleteByDefinitionId((int) $existing['id']);
|
||||
$this->optionRepository->deleteByDefinitionId((int) $existing['id']);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'form' => $form];
|
||||
@@ -145,11 +147,11 @@ class TenantCustomFieldService
|
||||
if ($uuid === '') {
|
||||
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
||||
}
|
||||
$existing = TenantCustomFieldDefinitionRepository::findByUuid($uuid);
|
||||
$existing = $this->definitionRepository->findByUuid($uuid);
|
||||
if (!$existing || !isset($existing['id'])) {
|
||||
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
|
||||
}
|
||||
$deleted = TenantCustomFieldDefinitionRepository::delete((int) $existing['id']);
|
||||
$deleted = $this->definitionRepository->delete((int) $existing['id']);
|
||||
if (!$deleted) {
|
||||
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
|
||||
}
|
||||
@@ -158,7 +160,7 @@ class TenantCustomFieldService
|
||||
|
||||
public function definitionTenantIdByUuid(string $uuid): int
|
||||
{
|
||||
$definition = TenantCustomFieldDefinitionRepository::findByUuid(trim($uuid));
|
||||
$definition = $this->definitionRepository->findByUuid(trim($uuid));
|
||||
return (int) ($definition['tenant_id'] ?? 0);
|
||||
}
|
||||
|
||||
@@ -184,7 +186,7 @@ class TenantCustomFieldService
|
||||
];
|
||||
}
|
||||
|
||||
private static function validateForm(array &$form, ?int $excludeId): array
|
||||
private function validateForm(array &$form, ?int $excludeId): array
|
||||
{
|
||||
$errors = [];
|
||||
$tenantId = (int) ($form['tenant_id'] ?? 0);
|
||||
@@ -204,7 +206,7 @@ class TenantCustomFieldService
|
||||
$errors[] = t('Field type is invalid');
|
||||
}
|
||||
if ($fieldKey !== '') {
|
||||
$existingByKey = TenantCustomFieldDefinitionRepository::findByTenantIdAndKey($tenantId, $fieldKey);
|
||||
$existingByKey = $this->definitionRepository->findByTenantIdAndKey($tenantId, $fieldKey);
|
||||
if ($existingByKey && (int) ($existingByKey['id'] ?? 0) !== (int) ($excludeId ?? 0)) {
|
||||
$errors[] = t('Field key already exists');
|
||||
}
|
||||
@@ -285,10 +287,10 @@ class TenantCustomFieldService
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function nextSortOrder(int $tenantId): int
|
||||
private function nextSortOrder(int $tenantId): int
|
||||
{
|
||||
$max = 0;
|
||||
foreach (TenantCustomFieldDefinitionRepository::listByTenantId($tenantId, false) as $definition) {
|
||||
foreach ($this->definitionRepository->listByTenantId($tenantId, false) as $definition) {
|
||||
$sortOrder = (int) ($definition['sort_order'] ?? 0);
|
||||
if ($sortOrder > $max) {
|
||||
$max = $sortOrder;
|
||||
@@ -300,7 +302,7 @@ class TenantCustomFieldService
|
||||
return $max + 10;
|
||||
}
|
||||
|
||||
private static function resolveFieldKey(int $tenantId, string $label, ?int $excludeId, string $preferred): string
|
||||
private function resolveFieldKey(int $tenantId, string $label, ?int $excludeId, string $preferred): string
|
||||
{
|
||||
$excludeId = (int) ($excludeId ?? 0);
|
||||
$source = trim($preferred) !== '' ? trim($preferred) : trim($label);
|
||||
@@ -317,7 +319,7 @@ class TenantCustomFieldService
|
||||
$candidate = $base;
|
||||
$counter = 2;
|
||||
while (true) {
|
||||
$existing = TenantCustomFieldDefinitionRepository::findByTenantIdAndKey($tenantId, $candidate);
|
||||
$existing = $this->definitionRepository->findByTenantIdAndKey($tenantId, $candidate);
|
||||
if (!$existing || (int) ($existing['id'] ?? 0) === $excludeId) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user