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:
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\CustomField;
|
||||
|
||||
interface TenantCustomFieldDefinitionRepositoryInterface
|
||||
{
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public function listByTenantId(int $tenantId, bool $onlyActive = true): array;
|
||||
|
||||
/**
|
||||
* @param int[] $tenantIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listByTenantIds(array $tenantIds, bool $onlyActive = true): array;
|
||||
|
||||
/**
|
||||
* @param int[] $tenantIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\CustomField;
|
||||
|
||||
interface TenantCustomFieldOptionRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param int[] $definitionIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listByDefinitionIds(array $definitionIds, bool $onlyActive = true): array;
|
||||
|
||||
/** @param array<int, array<string, mixed>> $options */
|
||||
public function replaceForDefinition(int $definitionId, array $options): bool;
|
||||
|
||||
public function deleteByDefinitionId(int $definitionId): bool;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\CustomField;
|
||||
|
||||
interface UserCustomFieldValueOptionRepositoryInterface
|
||||
{
|
||||
/** @param int[] $optionIds */
|
||||
public function replaceForValueId(int $valueId, array $optionIds): bool;
|
||||
|
||||
/**
|
||||
* @param int[] $valueIds
|
||||
* @return array<int, int[]>
|
||||
*/
|
||||
public function listOptionIdsByValueIds(array $valueIds): array;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository\CustomField;
|
||||
|
||||
interface UserCustomFieldValueRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param int[] $definitionIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listByUserAndDefinitionIds(int $userId, array $definitionIds): array;
|
||||
|
||||
/** @param array<string, mixed> $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;
|
||||
}
|
||||
40
core/Service/CustomField/CustomFieldRepositoryFactory.php
Normal file
40
core/Service/CustomField/CustomFieldRepositoryFactory.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\CustomField;
|
||||
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepository;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepository;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueRepository;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueRepositoryInterface;
|
||||
|
||||
class CustomFieldRepositoryFactory
|
||||
{
|
||||
private ?TenantCustomFieldDefinitionRepository $tenantCustomFieldDefinitionRepository = null;
|
||||
private ?TenantCustomFieldOptionRepository $tenantCustomFieldOptionRepository = null;
|
||||
private ?UserCustomFieldValueRepository $userCustomFieldValueRepository = null;
|
||||
private ?UserCustomFieldValueOptionRepository $userCustomFieldValueOptionRepository = null;
|
||||
|
||||
public function createTenantCustomFieldDefinitionRepository(): TenantCustomFieldDefinitionRepositoryInterface
|
||||
{
|
||||
return $this->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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user