Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.45% covered (warning)
85.45%
47 / 55
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UniversalFieldCleanupService
87.04% covered (warning)
87.04%
47 / 54
80.00% covered (warning)
80.00%
4 / 5
15.49
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cleanupFieldReferences
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 cleanupFilterReferences
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 cleanupRelationMmReferences
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
3.53
 decodeJsonArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Persistence\UniversalPersistenceManager;
12use PDO;
13
14/**
15 * Handles cascading cleanup of field references from filters and many-to-many configurations.
16 */
17final readonly class UniversalFieldCleanupService
18{
19    public function __construct(
20        private UniversalPersistenceManager $persistence
21    ) {
22    }
23
24    /**
25     * Cleans up references to a deleted field from filter and relation records.
26     *
27     * @param string $fieldKey Field identifier.
28     * @param int    $moduleId Module ID.
29     */
30    public function cleanupFieldReferences(string $fieldKey, int $moduleId): void
31    {
32        $this->cleanupFilterReferences($fieldKey, $moduleId);
33        $this->cleanupRelationMmReferences($fieldKey, $moduleId);
34    }
35
36    /**
37     * Cleans up deleted field references in filter records.
38     *
39     * @param string $fieldKey Field identifier.
40     * @param int    $moduleId Module ID.
41     */
42    private function cleanupFilterReferences(string $fieldKey, int $moduleId): void
43    {
44        $pdo = $this->persistence->getPdo();
45        $stmt = $pdo->prepare(
46            'SELECT `id`, `visible_fields`, `conditions` FROM `a_core_filter_records` WHERE `module_id` = :mod_id'
47        );
48        $stmt->execute([':mod_id' => $moduleId]);
49        /** @var array<int, array{id: int, visible_fields: string|null, conditions: string|null}> $filters */
50        $filters = $stmt->fetchAll(PDO::FETCH_ASSOC);
51
52        $updFilter = $pdo->prepare(
53            'UPDATE `a_core_filter_records` SET `visible_fields` = :vis, `conditions` = :cond WHERE `id` = :id'
54        );
55        foreach ($filters as $flt) {
56            $vis = $this->decodeJsonArray($flt['visible_fields']);
57            $conds = $this->decodeJsonArray($flt['conditions']);
58            $changed = false;
59
60            if (in_array($fieldKey, $vis, true)) {
61                $vis = array_values(array_filter($vis, static fn($k): bool => $k !== $fieldKey));
62                $changed = true;
63            }
64
65            $filteredConds = array_values(array_filter(
66                $conds,
67                static fn($c): bool => is_array($c) && ($c['field'] ?? '') !== $fieldKey
68            ));
69            if (count($filteredConds) !== count($conds)) {
70                $conds = $filteredConds;
71                $changed = true;
72            }
73
74            if ($changed) {
75                $updFilter->execute([
76                    ':vis'  => (string) json_encode($vis),
77                    ':cond' => (string) json_encode($conds),
78                    ':id'   => $flt['id'],
79                ]);
80            }
81        }
82    }
83
84    /**
85     * Cleans up deleted field references in many-to-many relation records.
86     *
87     * @param string $fieldKey Field identifier.
88     * @param int    $moduleId Module ID.
89     */
90    private function cleanupRelationMmReferences(string $fieldKey, int $moduleId): void
91    {
92        $pdo = $this->persistence->getPdo();
93        $stmtMm = $pdo->prepare(
94            'SELECT `id`, `visible_fields` FROM `a_core_relation_mm_records` ' .
95            'WHERE JSON_CONTAINS(`target_module_ids`, :mod_id)'
96        );
97        $stmtMm->execute([':mod_id' => (string) json_encode($moduleId)]);
98        /** @var array<int, array{id: int, visible_fields: string|null}> $mmRels */
99        $mmRels = $stmtMm->fetchAll(PDO::FETCH_ASSOC);
100
101        $updMm = $pdo->prepare(
102            'UPDATE `a_core_relation_mm_records` SET `visible_fields` = :vis WHERE `id` = :id'
103        );
104        foreach ($mmRels as $rel) {
105            $vis = $this->decodeJsonArray($rel['visible_fields']);
106            if (in_array($fieldKey, $vis, true)) {
107                $vis = array_values(array_filter($vis, static fn($k): bool => $k !== $fieldKey));
108                $updMm->execute([
109                    ':vis' => (string) json_encode($vis),
110                    ':id'  => $rel['id'],
111                ]);
112            }
113        }
114    }
115
116    /**
117     * Decodes JSON string into an associative array safely.
118     *
119     * @param string|null $raw Raw JSON string.
120     * @return array<mixed>
121     */
122    private function decodeJsonArray(?string $raw): array
123    {
124        if (!is_string($raw) || $raw === '') {
125            return [];
126        }
127        $decoded = json_decode($raw, true);
128        return is_array($decoded) ? $decoded : [];
129    }
130}