From 6b05bbfe9c2f4815f492bcd1738486199d683660 Mon Sep 17 00:00:00 2001 From: fs Date: Wed, 22 Apr 2026 19:32:14 +0200 Subject: [PATCH] refactor(customfield): interface-based DI for repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Registrars/RepositoryFactoryRegistrar.php | 2 + .../Registrars/ServiceFactoryRegistrar.php | 4 +- .../TenantCustomFieldDefinitionRepository.php | 20 +++---- ...stomFieldDefinitionRepositoryInterface.php | 33 ++++++++++++ .../TenantCustomFieldOptionRepository.php | 13 ++--- ...ntCustomFieldOptionRepositoryInterface.php | 17 ++++++ .../UserCustomFieldValueOptionRepository.php | 6 +-- ...tomFieldValueOptionRepositoryInterface.php | 15 ++++++ .../UserCustomFieldValueRepository.php | 10 ++-- ...serCustomFieldValueRepositoryInterface.php | 21 ++++++++ .../CustomFieldRepositoryFactory.php | 40 ++++++++++++++ .../CustomFieldServicesFactory.php | 17 ++++-- .../CustomField/TenantCustomFieldService.php | 52 ++++++++++--------- .../UserCustomFieldValueService.php | 38 ++++++++------ .../AddressBookContainerRegistrar.php | 4 +- .../Service/AddressBookService.php | 7 +-- .../Service/AddressBookServiceTest.php | 5 +- phpstan-baseline.neon | 6 --- 18 files changed, 227 insertions(+), 83 deletions(-) create mode 100644 core/Repository/CustomField/TenantCustomFieldDefinitionRepositoryInterface.php create mode 100644 core/Repository/CustomField/TenantCustomFieldOptionRepositoryInterface.php create mode 100644 core/Repository/CustomField/UserCustomFieldValueOptionRepositoryInterface.php create mode 100644 core/Repository/CustomField/UserCustomFieldValueRepositoryInterface.php create mode 100644 core/Service/CustomField/CustomFieldRepositoryFactory.php diff --git a/core/App/Container/Registrars/RepositoryFactoryRegistrar.php b/core/App/Container/Registrars/RepositoryFactoryRegistrar.php index 4d3d2c1..df97d5f 100644 --- a/core/App/Container/Registrars/RepositoryFactoryRegistrar.php +++ b/core/App/Container/Registrars/RepositoryFactoryRegistrar.php @@ -9,6 +9,7 @@ use MintyPHP\Repository\Stats\AdminStatsRepository; use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Service\Access\AccessRepositoryFactory; use MintyPHP\Service\Auth\AuthRepositoryFactory; +use MintyPHP\Service\CustomField\CustomFieldRepositoryFactory; use MintyPHP\Service\Directory\DirectoryRepositoryFactory; use MintyPHP\Service\Import\ImportRepositoryFactory; use MintyPHP\Service\Mail\MailRepositoryFactory; @@ -32,6 +33,7 @@ final class RepositoryFactoryRegistrar implements ContainerRegistrar $container->set(AccessRepositoryFactory::class, static fn (): AccessRepositoryFactory => new AccessRepositoryFactory()); $container->set(UserRepositoryFactory::class, static fn (): UserRepositoryFactory => new UserRepositoryFactory()); $container->set(AuthRepositoryFactory::class, static fn (): AuthRepositoryFactory => new AuthRepositoryFactory()); + $container->set(CustomFieldRepositoryFactory::class, static fn (): CustomFieldRepositoryFactory => new CustomFieldRepositoryFactory()); // These repositories have no factory because they're standalone — no shared config or gateway needed. $container->set(AdminStatsRepository::class, static fn (): AdminStatsRepository => new AdminStatsRepository()); $container->set(SearchQueryRepository::class, static fn (): SearchQueryRepository => new SearchQueryRepository()); diff --git a/core/App/Container/Registrars/ServiceFactoryRegistrar.php b/core/App/Container/Registrars/ServiceFactoryRegistrar.php index ed376ab..f1a5506 100644 --- a/core/App/Container/Registrars/ServiceFactoryRegistrar.php +++ b/core/App/Container/Registrars/ServiceFactoryRegistrar.php @@ -25,6 +25,7 @@ use MintyPHP\Service\Auth\AuthGatewayFactory; use MintyPHP\Service\Auth\AuthRepositoryFactory; use MintyPHP\Service\Auth\AuthServicesFactory; use MintyPHP\Service\Branding\BrandingServicesFactory; +use MintyPHP\Service\CustomField\CustomFieldRepositoryFactory; use MintyPHP\Service\CustomField\CustomFieldServicesFactory; use MintyPHP\Service\Directory\DirectoryGatewayFactory; use MintyPHP\Service\Directory\DirectoryRepositoryFactory; @@ -67,7 +68,8 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar )); $container->set(CustomFieldServicesFactory::class, static fn (AppContainer $c): CustomFieldServicesFactory => new CustomFieldServicesFactory( $c->get(TenantRepository::class), - $c->get(TenantScopeService::class) + $c->get(TenantScopeService::class), + $c->get(CustomFieldRepositoryFactory::class) )); $container->set(TenantServicesFactory::class, static fn (AppContainer $c): TenantServicesFactory => new TenantServicesFactory( $c->get(PermissionService::class), diff --git a/core/Repository/CustomField/TenantCustomFieldDefinitionRepository.php b/core/Repository/CustomField/TenantCustomFieldDefinitionRepository.php index f709e10..1c34173 100644 --- a/core/Repository/CustomField/TenantCustomFieldDefinitionRepository.php +++ b/core/Repository/CustomField/TenantCustomFieldDefinitionRepository.php @@ -6,7 +6,7 @@ use MintyPHP\DB; use MintyPHP\Repository\Support\RepoQuery; /** Reads and writes tenant-scoped custom field definitions with type and pagination filtering. */ -class TenantCustomFieldDefinitionRepository +class TenantCustomFieldDefinitionRepository implements TenantCustomFieldDefinitionRepositoryInterface { private static function unwrap(?array $row): ?array { @@ -31,7 +31,7 @@ class TenantCustomFieldDefinitionRepository return $list; } - public static function listByTenantId(int $tenantId, bool $onlyActive = true): array + public function listByTenantId(int $tenantId, bool $onlyActive = true): array { if ($tenantId <= 0) { return []; @@ -46,7 +46,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrapList(DB::select($query, ...$params)); } - public static function listByTenantIds(array $tenantIds, bool $onlyActive = true): array + public function listByTenantIds(array $tenantIds, bool $onlyActive = true): array { $tenantIds = array_values(array_unique(array_map('intval', $tenantIds))); $tenantIds = array_values(array_filter($tenantIds, static fn ($id) => $id > 0)); @@ -63,7 +63,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrapList(call_user_func_array(['MintyPHP\\DB', 'select'], array_merge([$query], $params))); } - public static function listFilterableByTenantIds(array $tenantIds): array + public function listFilterableByTenantIds(array $tenantIds): array { $tenantIds = array_values(array_unique(array_map('intval', $tenantIds))); $tenantIds = array_values(array_filter($tenantIds, static fn ($id) => $id > 0)); @@ -78,7 +78,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrapList(call_user_func_array(['MintyPHP\\DB', 'select'], [$query, $tenantIds])); } - public static function findByUuid(string $uuid): ?array + public function findByUuid(string $uuid): ?array { $row = DB::selectOne( 'select id, uuid, tenant_id, field_key, label, type, is_required, is_filterable, active, sort_order, created_by, modified_by, created, modified ' . @@ -88,7 +88,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrap($row); } - public static function findById(int $id): ?array + public function findById(int $id): ?array { $row = DB::selectOne( 'select id, uuid, tenant_id, field_key, label, type, is_required, is_filterable, active, sort_order, created_by, modified_by, created, modified ' . @@ -98,7 +98,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrap($row); } - public static function findByTenantIdAndKey(int $tenantId, string $fieldKey): ?array + public function findByTenantIdAndKey(int $tenantId, string $fieldKey): ?array { if ($tenantId <= 0 || $fieldKey === '') { return null; @@ -112,7 +112,7 @@ class TenantCustomFieldDefinitionRepository return self::unwrap($row); } - public static function create(array $data): int|false + public function create(array $data): int|false { return DB::insert( 'insert into tenant_custom_field_definitions ' . @@ -131,7 +131,7 @@ class TenantCustomFieldDefinitionRepository ); } - public static function update(int $id, array $data): bool + public function update(int $id, array $data): bool { if ($id <= 0) { return false; @@ -164,7 +164,7 @@ class TenantCustomFieldDefinitionRepository return $result !== false; } - public static function delete(int $id): bool + public function delete(int $id): bool { if ($id <= 0) { return false; diff --git a/core/Repository/CustomField/TenantCustomFieldDefinitionRepositoryInterface.php b/core/Repository/CustomField/TenantCustomFieldDefinitionRepositoryInterface.php new file mode 100644 index 0000000..f77edc3 --- /dev/null +++ b/core/Repository/CustomField/TenantCustomFieldDefinitionRepositoryInterface.php @@ -0,0 +1,33 @@ +> */ + public function listByTenantId(int $tenantId, bool $onlyActive = true): array; + + /** + * @param int[] $tenantIds + * @return array> + */ + public function listByTenantIds(array $tenantIds, bool $onlyActive = true): array; + + /** + * @param int[] $tenantIds + * @return array> + */ + public function listFilterableByTenantIds(array $tenantIds): array; + + public function findByUuid(string $uuid): ?array; + + public function findById(int $id): ?array; + + public function findByTenantIdAndKey(int $tenantId, string $fieldKey): ?array; + + public function create(array $data): int|false; + + public function update(int $id, array $data): bool; + + public function delete(int $id): bool; +} diff --git a/core/Repository/CustomField/TenantCustomFieldOptionRepository.php b/core/Repository/CustomField/TenantCustomFieldOptionRepository.php index a7be895..25b9e3c 100644 --- a/core/Repository/CustomField/TenantCustomFieldOptionRepository.php +++ b/core/Repository/CustomField/TenantCustomFieldOptionRepository.php @@ -5,7 +5,7 @@ namespace MintyPHP\Repository\CustomField; use MintyPHP\DB; /** Retrieves selectable options for tenant-scoped custom field definitions. */ -class TenantCustomFieldOptionRepository +class TenantCustomFieldOptionRepository implements TenantCustomFieldOptionRepositoryInterface { private static function unwrapList($rows): array { @@ -22,7 +22,7 @@ class TenantCustomFieldOptionRepository return $list; } - public static function listByDefinitionIds(array $definitionIds, bool $onlyActive = true): array + public function listByDefinitionIds(array $definitionIds, bool $onlyActive = true): array { $definitionIds = array_values(array_unique(array_map('intval', $definitionIds))); $definitionIds = array_values(array_filter($definitionIds, static fn ($id) => $id > 0)); @@ -39,12 +39,12 @@ class TenantCustomFieldOptionRepository return self::unwrapList(call_user_func_array(['MintyPHP\\DB', 'select'], array_merge([$query], $params))); } - public static function replaceForDefinition(int $definitionId, array $options): bool + public function replaceForDefinition(int $definitionId, array $options): bool { if ($definitionId <= 0) { return false; } - $existing = self::listByDefinitionIds([$definitionId], false); + $existing = $this->listByDefinitionIds([$definitionId], false); $existingByKey = []; foreach ($existing as $row) { $key = (string) ($row['option_key'] ?? ''); @@ -55,9 +55,6 @@ class TenantCustomFieldOptionRepository $keepIds = []; foreach ($options as $option) { - if (!is_array($option)) { - continue; - } $optionKey = trim((string) ($option['option_key'] ?? '')); $label = trim((string) ($option['label'] ?? '')); if ($optionKey === '' || $label === '') { @@ -117,7 +114,7 @@ class TenantCustomFieldOptionRepository return true; } - public static function deleteByDefinitionId(int $definitionId): bool + public function deleteByDefinitionId(int $definitionId): bool { if ($definitionId <= 0) { return false; diff --git a/core/Repository/CustomField/TenantCustomFieldOptionRepositoryInterface.php b/core/Repository/CustomField/TenantCustomFieldOptionRepositoryInterface.php new file mode 100644 index 0000000..29ccbb7 --- /dev/null +++ b/core/Repository/CustomField/TenantCustomFieldOptionRepositoryInterface.php @@ -0,0 +1,17 @@ +> + */ + public function listByDefinitionIds(array $definitionIds, bool $onlyActive = true): array; + + /** @param array> $options */ + public function replaceForDefinition(int $definitionId, array $options): bool; + + public function deleteByDefinitionId(int $definitionId): bool; +} diff --git a/core/Repository/CustomField/UserCustomFieldValueOptionRepository.php b/core/Repository/CustomField/UserCustomFieldValueOptionRepository.php index b52c576..861d77f 100644 --- a/core/Repository/CustomField/UserCustomFieldValueOptionRepository.php +++ b/core/Repository/CustomField/UserCustomFieldValueOptionRepository.php @@ -5,9 +5,9 @@ namespace MintyPHP\Repository\CustomField; use MintyPHP\DB; /** Manages selected option links for user custom field values (atomic replace). */ -class UserCustomFieldValueOptionRepository +class UserCustomFieldValueOptionRepository implements UserCustomFieldValueOptionRepositoryInterface { - public static function replaceForValueId(int $valueId, array $optionIds): bool + public function replaceForValueId(int $valueId, array $optionIds): bool { if ($valueId <= 0) { return false; @@ -37,7 +37,7 @@ class UserCustomFieldValueOptionRepository return true; } - public static function listOptionIdsByValueIds(array $valueIds): array + public function listOptionIdsByValueIds(array $valueIds): array { $valueIds = array_values(array_unique(array_map('intval', $valueIds))); $valueIds = array_values(array_filter($valueIds, static fn ($id) => $id > 0)); diff --git a/core/Repository/CustomField/UserCustomFieldValueOptionRepositoryInterface.php b/core/Repository/CustomField/UserCustomFieldValueOptionRepositoryInterface.php new file mode 100644 index 0000000..6145c2f --- /dev/null +++ b/core/Repository/CustomField/UserCustomFieldValueOptionRepositoryInterface.php @@ -0,0 +1,15 @@ + + */ + public function listOptionIdsByValueIds(array $valueIds): array; +} diff --git a/core/Repository/CustomField/UserCustomFieldValueRepository.php b/core/Repository/CustomField/UserCustomFieldValueRepository.php index b0d5958..1dcf029 100644 --- a/core/Repository/CustomField/UserCustomFieldValueRepository.php +++ b/core/Repository/CustomField/UserCustomFieldValueRepository.php @@ -5,7 +5,7 @@ namespace MintyPHP\Repository\CustomField; use MintyPHP\DB; /** Reads and writes custom field values attached to individual users. */ -class UserCustomFieldValueRepository +class UserCustomFieldValueRepository implements UserCustomFieldValueRepositoryInterface { private static function unwrapList($rows): array { @@ -22,7 +22,7 @@ class UserCustomFieldValueRepository return $list; } - public static function listByUserAndDefinitionIds(int $userId, array $definitionIds): array + public function listByUserAndDefinitionIds(int $userId, array $definitionIds): array { if ($userId <= 0) { return []; @@ -41,7 +41,7 @@ class UserCustomFieldValueRepository return self::unwrapList($rows); } - public static function upsertScalarValue(int $userId, int $definitionId, array $typedValue): int|false + public function upsertScalarValue(int $userId, int $definitionId, array $typedValue): int|false { if ($userId <= 0 || $definitionId <= 0) { return false; @@ -80,7 +80,7 @@ class UserCustomFieldValueRepository ); } - public static function deleteByUserAndDefinitionIds(int $userId, array $definitionIds): bool + public function deleteByUserAndDefinitionIds(int $userId, array $definitionIds): bool { if ($userId <= 0) { return false; @@ -98,7 +98,7 @@ class UserCustomFieldValueRepository return $result !== false; } - public static function deleteByUserOutsideTenantIds(int $userId, array $tenantIds): bool + public function deleteByUserOutsideTenantIds(int $userId, array $tenantIds): bool { if ($userId <= 0) { return false; diff --git a/core/Repository/CustomField/UserCustomFieldValueRepositoryInterface.php b/core/Repository/CustomField/UserCustomFieldValueRepositoryInterface.php new file mode 100644 index 0000000..9be0829 --- /dev/null +++ b/core/Repository/CustomField/UserCustomFieldValueRepositoryInterface.php @@ -0,0 +1,21 @@ +> + */ + public function listByUserAndDefinitionIds(int $userId, array $definitionIds): array; + + /** @param array $typedValue */ + public function upsertScalarValue(int $userId, int $definitionId, array $typedValue): int|false; + + /** @param int[] $definitionIds */ + public function deleteByUserAndDefinitionIds(int $userId, array $definitionIds): bool; + + /** @param int[] $tenantIds */ + public function deleteByUserOutsideTenantIds(int $userId, array $tenantIds): bool; +} diff --git a/core/Service/CustomField/CustomFieldRepositoryFactory.php b/core/Service/CustomField/CustomFieldRepositoryFactory.php new file mode 100644 index 0000000..61696cb --- /dev/null +++ b/core/Service/CustomField/CustomFieldRepositoryFactory.php @@ -0,0 +1,40 @@ +tenantCustomFieldDefinitionRepository ??= new TenantCustomFieldDefinitionRepository(); + } + + public function createTenantCustomFieldOptionRepository(): TenantCustomFieldOptionRepositoryInterface + { + return $this->tenantCustomFieldOptionRepository ??= new TenantCustomFieldOptionRepository(); + } + + public function createUserCustomFieldValueRepository(): UserCustomFieldValueRepositoryInterface + { + return $this->userCustomFieldValueRepository ??= new UserCustomFieldValueRepository(); + } + + public function createUserCustomFieldValueOptionRepository(): UserCustomFieldValueOptionRepositoryInterface + { + return $this->userCustomFieldValueOptionRepository ??= new UserCustomFieldValueOptionRepository(); + } +} diff --git a/core/Service/CustomField/CustomFieldServicesFactory.php b/core/Service/CustomField/CustomFieldServicesFactory.php index 85a872c..4c3cabe 100644 --- a/core/Service/CustomField/CustomFieldServicesFactory.php +++ b/core/Service/CustomField/CustomFieldServicesFactory.php @@ -12,17 +12,28 @@ class CustomFieldServicesFactory public function __construct( private readonly TenantRepository $tenantRepository, - private readonly TenantScopeService $userScopeGateway + private readonly TenantScopeService $userScopeGateway, + private readonly CustomFieldRepositoryFactory $customFieldRepositoryFactory ) { } public function createTenantCustomFieldService(): TenantCustomFieldService { - return $this->tenantCustomFieldService ??= new TenantCustomFieldService($this->tenantRepository); + return $this->tenantCustomFieldService ??= new TenantCustomFieldService( + $this->tenantRepository, + $this->customFieldRepositoryFactory->createTenantCustomFieldDefinitionRepository(), + $this->customFieldRepositoryFactory->createTenantCustomFieldOptionRepository(), + ); } public function createUserCustomFieldValueService(): UserCustomFieldValueService { - return $this->userCustomFieldValueService ??= new UserCustomFieldValueService($this->userScopeGateway); + return $this->userCustomFieldValueService ??= new UserCustomFieldValueService( + $this->userScopeGateway, + $this->customFieldRepositoryFactory->createTenantCustomFieldDefinitionRepository(), + $this->customFieldRepositoryFactory->createTenantCustomFieldOptionRepository(), + $this->customFieldRepositoryFactory->createUserCustomFieldValueRepository(), + $this->customFieldRepositoryFactory->createUserCustomFieldValueOptionRepository(), + ); } } diff --git a/core/Service/CustomField/TenantCustomFieldService.php b/core/Service/CustomField/TenantCustomFieldService.php index 0cbf0b7..1e231c4 100644 --- a/core/Service/CustomField/TenantCustomFieldService.php +++ b/core/Service/CustomField/TenantCustomFieldService.php @@ -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; } diff --git a/core/Service/CustomField/UserCustomFieldValueService.php b/core/Service/CustomField/UserCustomFieldValueService.php index 226bbf4..0d1e0e5 100644 --- a/core/Service/CustomField/UserCustomFieldValueService.php +++ b/core/Service/CustomField/UserCustomFieldValueService.php @@ -2,17 +2,21 @@ namespace MintyPHP\Service\CustomField; -use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepository; -use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository; -use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepository; -use MintyPHP\Repository\CustomField\UserCustomFieldValueRepository; +use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface; +use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface; +use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepositoryInterface; +use MintyPHP\Repository\CustomField\UserCustomFieldValueRepositoryInterface; use MintyPHP\Repository\Support\RepoQuery; use MintyPHP\Service\Tenant\TenantScopeService; class UserCustomFieldValueService { public function __construct( - private readonly TenantScopeService $userScopeGateway + private readonly TenantScopeService $userScopeGateway, + private readonly TenantCustomFieldDefinitionRepositoryInterface $definitionRepository, + private readonly TenantCustomFieldOptionRepositoryInterface $optionRepository, + private readonly UserCustomFieldValueRepositoryInterface $valueRepository, + private readonly UserCustomFieldValueOptionRepositoryInterface $valueOptionRepository ) { } @@ -39,7 +43,7 @@ class UserCustomFieldValueService return []; } - $definitions = TenantCustomFieldDefinitionRepository::listByTenantIds($tenantIds, !$includeInactive); + $definitions = $this->definitionRepository->listByTenantIds($tenantIds, !$includeInactive); if (!$definitions) { return []; } @@ -48,7 +52,7 @@ class UserCustomFieldValueService static fn (array $definition): int => (int) ($definition['id'] ?? 0), $definitions ), static fn (int $id): bool => $id > 0)); - $options = TenantCustomFieldOptionRepository::listByDefinitionIds($definitionIds, !$includeInactive); + $options = $this->optionRepository->listByDefinitionIds($definitionIds, !$includeInactive); $optionsByDefinition = []; foreach ($options as $option) { $definitionId = (int) ($option['definition_id'] ?? 0); @@ -85,7 +89,7 @@ class UserCustomFieldValueService return []; } - $rows = UserCustomFieldValueRepository::listByUserAndDefinitionIds($userId, $definitionIds); + $rows = $this->valueRepository->listByUserAndDefinitionIds($userId, $definitionIds); if (!$rows) { return []; } @@ -94,7 +98,7 @@ class UserCustomFieldValueService static fn (array $row): int => (int) ($row['id'] ?? 0), $rows ), static fn (int $id): bool => $id > 0)); - $optionIdsByValue = UserCustomFieldValueOptionRepository::listOptionIdsByValueIds($valueIds); + $optionIdsByValue = $this->valueOptionRepository->listOptionIdsByValueIds($valueIds); $map = []; foreach ($rows as $row) { @@ -250,7 +254,7 @@ class UserCustomFieldValueService $tenantIds = self::normalizeTenantIds($tenantIds); // Always clean values for tenants no longer assigned — happens even when !$canEdit, // because orphaned custom field data should not persist after tenant reassignment. - if (!UserCustomFieldValueRepository::deleteByUserOutsideTenantIds($userId, $tenantIds)) { + if (!$this->valueRepository->deleteByUserOutsideTenantIds($userId, $tenantIds)) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } if (!$canEdit) { @@ -270,14 +274,14 @@ class UserCustomFieldValueService } foreach ($prepared['prepared'] as $definitionId => $valueData) { if (!empty($valueData['empty'])) { - if (!UserCustomFieldValueRepository::deleteByUserAndDefinitionIds($userId, [(int) $definitionId])) { + if (!$this->valueRepository->deleteByUserAndDefinitionIds($userId, [(int) $definitionId])) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } continue; } if (!empty($valueData['option_ids'])) { - $valueId = UserCustomFieldValueRepository::upsertScalarValue($userId, (int) $definitionId, [ + $valueId = $this->valueRepository->upsertScalarValue($userId, (int) $definitionId, [ 'value_text' => null, 'value_bool' => null, 'value_date' => null, @@ -286,13 +290,13 @@ class UserCustomFieldValueService if (!$valueId) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } - if (!UserCustomFieldValueOptionRepository::replaceForValueId((int) $valueId, $valueData['option_ids'])) { + if (!$this->valueOptionRepository->replaceForValueId((int) $valueId, $valueData['option_ids'])) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } continue; } - $valueId = UserCustomFieldValueRepository::upsertScalarValue($userId, (int) $definitionId, [ + $valueId = $this->valueRepository->upsertScalarValue($userId, (int) $definitionId, [ 'value_text' => $valueData['value_text'], 'value_bool' => $valueData['value_bool'], 'value_date' => $valueData['value_date'], @@ -301,7 +305,7 @@ class UserCustomFieldValueService if (!$valueId) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } - if (!UserCustomFieldValueOptionRepository::replaceForValueId((int) $valueId, [])) { + if (!$this->valueOptionRepository->replaceForValueId((int) $valueId, [])) { return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]]; } } @@ -419,7 +423,7 @@ class UserCustomFieldValueService public function extractCustomFieldFilterSpec(array $query, int $currentUserId): array { $tenantIds = $this->userScopeGateway()->getUserTenantIds($currentUserId); - $definitions = TenantCustomFieldDefinitionRepository::listFilterableByTenantIds($tenantIds); + $definitions = $this->definitionRepository->listFilterableByTenantIds($tenantIds); if (!$definitions) { return ['definitions' => [], 'filters' => [], 'query' => []]; } @@ -436,7 +440,7 @@ class UserCustomFieldValueService $definitionIds[] = $definitionId; } $definitionIds = array_values(array_unique($definitionIds)); - $options = TenantCustomFieldOptionRepository::listByDefinitionIds($definitionIds, true); + $options = $this->optionRepository->listByDefinitionIds($definitionIds, true); $optionMapByDefinition = []; foreach ($options as $option) { $definitionId = (int) ($option['definition_id'] ?? 0); diff --git a/modules/addressbook/lib/Module/AddressBook/AddressBookContainerRegistrar.php b/modules/addressbook/lib/Module/AddressBook/AddressBookContainerRegistrar.php index 846e71d..bcf9888 100644 --- a/modules/addressbook/lib/Module/AddressBook/AddressBookContainerRegistrar.php +++ b/modules/addressbook/lib/Module/AddressBook/AddressBookContainerRegistrar.php @@ -7,6 +7,7 @@ use MintyPHP\App\Container\ContainerRegistrar; use MintyPHP\Module\AddressBook\Service\AddressBookService; use MintyPHP\Service\Access\PermissionService; use MintyPHP\Service\Access\RoleService; +use MintyPHP\Service\CustomField\CustomFieldRepositoryFactory; use MintyPHP\Service\CustomField\UserCustomFieldValueService; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Tenant\TenantScopeService; @@ -31,7 +32,8 @@ final class AddressBookContainerRegistrar implements ContainerRegistrar $c->get(TenantScopeService::class), $c->get(UserCustomFieldValueService::class), $c->get(UserAvatarService::class), - $c->get(UserProfileViewService::class) + $c->get(UserProfileViewService::class), + $c->get(CustomFieldRepositoryFactory::class)->createTenantCustomFieldOptionRepository() )); } } diff --git a/modules/addressbook/lib/Module/AddressBook/Service/AddressBookService.php b/modules/addressbook/lib/Module/AddressBook/Service/AddressBookService.php index 28931b4..ff26ba2 100644 --- a/modules/addressbook/lib/Module/AddressBook/Service/AddressBookService.php +++ b/modules/addressbook/lib/Module/AddressBook/Service/AddressBookService.php @@ -2,7 +2,7 @@ namespace MintyPHP\Module\AddressBook\Service; -use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository; +use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface; use MintyPHP\Service\Access\RoleService; use MintyPHP\Service\CustomField\UserCustomFieldValueService; use MintyPHP\Service\Org\DepartmentService; @@ -22,7 +22,8 @@ class AddressBookService private readonly TenantScopeService $scopeGateway, private readonly UserCustomFieldValueService $customFieldValueService, private readonly UserAvatarService $avatarService, - private readonly UserProfileViewService $userProfileViewService + private readonly UserProfileViewService $userProfileViewService, + private readonly TenantCustomFieldOptionRepositoryInterface $tenantCustomFieldOptionRepository ) { } @@ -76,7 +77,7 @@ class AddressBookService } $customFieldDefinitionIds = array_values(array_unique($customFieldDefinitionIds)); - $customFieldOptions = TenantCustomFieldOptionRepository::listByDefinitionIds( + $customFieldOptions = $this->tenantCustomFieldOptionRepository->listByDefinitionIds( $customFieldDefinitionIds, true ); diff --git a/modules/addressbook/tests/Module/AddressBook/Service/AddressBookServiceTest.php b/modules/addressbook/tests/Module/AddressBook/Service/AddressBookServiceTest.php index a1d3d99..0e05f2a 100644 --- a/modules/addressbook/tests/Module/AddressBook/Service/AddressBookServiceTest.php +++ b/modules/addressbook/tests/Module/AddressBook/Service/AddressBookServiceTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\AddressBook\Service; use MintyPHP\Module\AddressBook\Service\AddressBookService; +use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface; use MintyPHP\Service\Access\RoleService; use MintyPHP\Service\CustomField\UserCustomFieldValueService; use MintyPHP\Service\Org\DepartmentService; @@ -25,6 +26,7 @@ class AddressBookServiceTest extends TestCase $customFieldGateway = $this->createMock(UserCustomFieldValueService::class); $avatarService = $this->createMock(UserAvatarService::class); $userProfileViewService = $this->createMock(UserProfileViewService::class); + $tenantCustomFieldOptionRepository = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class); $scopeGateway ->expects($this->once()) @@ -73,7 +75,8 @@ class AddressBookServiceTest extends TestCase $scopeGateway, $customFieldGateway, $avatarService, - $userProfileViewService + $userProfileViewService, + $tenantCustomFieldOptionRepository ); $context = $service->buildIndexContext(7, []); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 441198c..2b830bc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -222,12 +222,6 @@ parameters: count: 1 path: core/Http/RouteRegistrar.php - - - message: '#^Public method "MintyPHP\\Repository\\CustomField\\TenantCustomFieldDefinitionRepository\:\:findById\(\)" is never used$#' - identifier: public.method.unused - count: 1 - path: core/Repository/CustomField/TenantCustomFieldDefinitionRepository.php - - message: '#^Public method "MintyPHP\\Service\\Access\\AccessServicesFactory\:\:createAuthorizationService\(\)" is never used$#' identifier: public.method.unused