44
55namespace PhpList \Core \Domain \Subscription \Service \Manager ;
66
7- use Doctrine \DBAL \Connection ;
8- use Doctrine \DBAL \Exception ;
9- use Doctrine \DBAL \ParameterType ;
107use PhpList \Core \Domain \Subscription \Model \Dto \DynamicListAttrDto ;
118use PhpList \Core \Domain \Subscription \Repository \DynamicListAttrRepository ;
129use RuntimeException ;
13- use Throwable ;
1410
1511class DynamicListAttrManager
1612{
17- private string $ prefix ;
18-
19- public function __construct (
20- private readonly DynamicListAttrRepository $ dynamicListAttrRepository ,
21- private readonly Connection $ connection ,
22- string $ dbPrefix = 'phplist_ ' ,
23- string $ dynamicListTablePrefix = 'listattr_ ' ,
24- ) {
25- $ this ->prefix = $ dbPrefix . $ dynamicListTablePrefix ;
13+ public function __construct (private readonly DynamicListAttrRepository $ dynamicListAttrRepository ,)
14+ {
2615 }
2716
2817 /**
2918 * Seed options into the just-created options table.
3019 *
3120 * @param string $listTable logical table (without global prefix)
3221 * @param DynamicListAttrDto[] $rawOptions
33- * @throws Exception|Throwable
3422 */
3523 public function insertOptions (string $ listTable , array $ rawOptions , array &$ usedOrders = []): array
3624 {
@@ -39,8 +27,6 @@ public function insertOptions(string $listTable, array $rawOptions, array &$used
3927 return $ result ;
4028 }
4129
42- $ fullTable = $ this ->prefix . $ listTable ;
43-
4430 $ options = [];
4531 $ index = 0 ;
4632 foreach ($ rawOptions as $ opt ) {
@@ -72,72 +58,93 @@ public function insertOptions(string $listTable, array $rawOptions, array &$used
7258 return $ result ;
7359 }
7460
75- $ this ->connection ->beginTransaction ();
76- try {
77- return $ this ->createFromDtos ($ fullTable , $ unique );
78- } catch (Throwable $ e ) {
79- $ this ->connection ->rollBack ();
80- return [];
81- }
61+ return $ this ->dynamicListAttrRepository ->transactional (function () use ($ listTable , $ unique ) {
62+ return $ this ->dynamicListAttrRepository ->insertMany ($ listTable , $ unique );
63+ });
8264 }
8365
8466 public function syncOptions (string $ getTableName , array $ options ): array
8567 {
86- $ fullTable = $ this ->prefix . $ getTableName ;
8768 $ result = [];
8869 $ usedOrders = $ this ->getSetListOrders ($ options );
8970 [$ incomingById , $ incomingNew ] = $ this ->splitOptionsSet ($ options , $ usedOrders );
9071
91- $ this ->connection ->beginTransaction ();
92- try {
93- [$ currentById , $ currentByName ] = $ this ->splitCurrentSet ($ fullTable );
94-
95- // 1) Updates for items with id
96- $ result = array_merge ($ result , $ this ->updateRowsById ($ incomingById , $ currentById , $ fullTable ));
72+ return $ this ->dynamicListAttrRepository ->transactional (function () use (
73+ $ getTableName ,
74+ $ incomingById ,
75+ $ incomingNew ,
76+ $ usedOrders ,
77+ &$ result
78+ ) {
79+ [$ currentById , $ currentByName ] = $ this ->splitCurrentSet ($ getTableName );
80+
81+ // Track all lowercase names that should remain after sync (to avoid accidental pruning)
82+ $ keptByLowerName = [];
83+
84+ // 1) Updates for items with id and inserts for id-missing-but-present ones
85+ $ result = array_merge (
86+ $ result ,
87+ $ this ->updateRowsById (
88+ incomingById: $ incomingById ,
89+ currentById: $ currentById ,
90+ listTable: $ getTableName
91+ )
92+ );
93+ foreach ($ incomingById as $ dto ) {
94+ // Keep target names (case-insensitive)
95+ $ keptByLowerName [mb_strtolower ($ dto ->name )] = true ;
96+ }
9797
98- foreach ($ incomingNew as $ dto ) {
99- if (isset ($ currentByName [$ dto ->name ])) {
100- $ this ->connection ->update (
101- $ fullTable ,
102- ['name ' => $ dto ->name , 'listorder ' => $ dto ->listOrder ],
103- ['id ' => $ dto ->id ],
98+ // Handle incoming items without id but matching an existing row by case-insensitive name
99+ foreach ($ incomingNew as $ key => $ dto ) {
100+ // $key is already lowercase (set in splitOptionsSet)
101+ if (isset ($ currentByName [$ key ])) {
102+ $ existing = $ currentByName [$ key ];
103+ $ this ->dynamicListAttrRepository ->updateById (
104+ listTable: $ getTableName ,
105+ id: (int )$ existing ->id ,
106+ updates: ['name ' => $ dto ->name , 'listorder ' => $ dto ->listOrder ]
104107 );
105- $ result [] = $ dto ;
106- unset($ incomingNew [$ dto ->name ]);
108+ $ result [] = new DynamicListAttrDto (id: $ existing ->id , name: $ dto ->name , listOrder: $ dto ->listOrder );
109+ // Mark as kept and remove from incomingNew so it won't be re-inserted
110+ $ keptByLowerName [$ key ] = true ;
111+ unset($ incomingNew [$ key ]);
107112 }
108113 }
109114
110- // 2) Inserts for new items (no id)
111- $ result = array_merge ($ result , $ this ->insertOptions ($ getTableName , $ incomingNew , $ usedOrders ));
115+ // 2) Inserts for truly new items (no id and no existing match)
116+ // Mark remaining new keys as kept, then insert them
117+ foreach (array_keys ($ incomingNew ) as $ lowerKey ) {
118+ $ keptByLowerName [$ lowerKey ] = true ;
119+ }
120+ $ result = array_merge (
121+ $ result ,
122+ $ this ->insertOptions (
123+ listTable: $ getTableName ,
124+ rawOptions: $ incomingNew ,
125+ usedOrders: $ usedOrders
126+ )
127+ );
112128
113- // 3) Prune: rows not present in input
114- $ missing = array_diff_key ($ currentByName , $ incomingNew );
115- foreach ($ missing as $ row ) {
116- // This row is not in input → consider removal
117- if (!$ this ->optionHasReferences ($ getTableName , $ row ->id )) {
118- $ this ->connection ->delete ($ fullTable , ['id ' => $ row ->id ], ['integer ' ]);
119- } else {
120- $ result [] = $ row ;
129+ // 3) Prune: rows not present in the intended final set (case-insensitive)
130+ foreach ($ currentByName as $ lowerKey => $ row ) {
131+ if (!isset ($ keptByLowerName [$ lowerKey ])) {
132+ // This row is not in desired input → consider removal
133+ if (!$ this ->optionHasReferences ($ getTableName , (int )$ row ->id )) {
134+ $ this ->dynamicListAttrRepository ->deleteById ($ getTableName , (int )$ row ->id );
135+ } else {
136+ $ result [] = $ row ;
137+ }
121138 }
122139 }
123140
124- $ this ->connection ->commit ();
125- } catch (Throwable $ e ) {
126- $ this ->connection ->rollBack ();
127- throw $ e ;
128- }
129-
130- return $ result ;
141+ return $ result ;
142+ });
131143 }
132144
133145 private function optionHasReferences (string $ listTable , int $ id ): bool
134146 {
135- $ fullTable = $ this ->prefix . $ listTable ;
136- $ stmt = $ this ->connection ->executeQuery (
137- 'SELECT COUNT(*) FROM ' . $ fullTable . ' WHERE id = :id ' ,
138- ['id ' => $ id ]
139- );
140- return (bool )$ stmt ->fetchOne ();
147+ return $ this ->dynamicListAttrRepository ->existsById ($ listTable , $ id );
141148 }
142149
143150 private function getSetListOrders (array $ options ): array
@@ -185,12 +192,12 @@ private function splitOptionsSet(array $options, array &$usedOrders): array
185192 return [$ incomingById , $ incomingNew ];
186193 }
187194
188- private function splitCurrentSet (string $ fullTable ): array
195+ private function splitCurrentSet (string $ listTable ): array
189196 {
190197 $ currentById = [];
191198 $ currentByName = [];
192199
193- $ rows = $ this ->dynamicListAttrRepository ->getAll ($ fullTable );
200+ $ rows = $ this ->dynamicListAttrRepository ->getAll ($ listTable );
194201 foreach ($ rows as $ listAttrDto ) {
195202 $ currentById [$ listAttrDto ->id ] = $ listAttrDto ;
196203 $ currentByName [mb_strtolower ($ listAttrDto ->name )] = $ listAttrDto ;
@@ -199,29 +206,23 @@ private function splitCurrentSet(string $fullTable): array
199206 return [$ currentById , $ currentByName ];
200207 }
201208
202- private function updateRowsById (array $ incomingById , array $ currentById , string $ fullTable ): array
209+ private function updateRowsById (array $ incomingById , array $ currentById , string $ listTable ): array
203210 {
204211 $ result = [];
205212
206- $ insertSql = 'INSERT INTO ' . $ fullTable . ' (name, listorder) VALUES (:name, :listOrder) ' ;
207- $ insertStmt = $ this ->connection ->prepare ($ insertSql );
208-
209213 foreach ($ incomingById as $ dto ) {
210214 if (!isset ($ currentById [$ dto ->id ])) {
211- $ insertStmt ->bindValue ('name ' , $ dto ->name , ParameterType::STRING );
212- $ insertStmt ->bindValue ('listorder ' , $ dto ->listOrder , ParameterType::INTEGER );
213- $ insertStmt ->executeStatement ();
214-
215- $ result [] = new DynamicListAttrDto (
216- id: (int ) $ this ->connection ->lastInsertId (),
217- name: $ dto ->name ,
218- listOrder: $ dto ->listOrder
215+ // Unexpected: incoming has id but the current table does not — insert a new row
216+ $ inserted = $ this ->dynamicListAttrRepository ->insertOne (
217+ listTable: $ listTable ,
218+ dto: new DynamicListAttrDto (id: null , name: $ dto ->name , listOrder: $ dto ->listOrder )
219219 );
220+ $ result [] = $ inserted ;
220221 } else {
221222 $ cur = $ currentById [$ dto ->id ];
222223 $ updates = [];
223224 if ($ cur ->name !== $ dto ->name ) {
224- $ nameExists = $ this ->dynamicListAttrRepository ->existsByName ($ fullTable , $ dto ->name );
225+ $ nameExists = $ this ->dynamicListAttrRepository ->existsByName ($ listTable , $ dto ->name );
225226 if ($ nameExists ) {
226227 throw new RuntimeException ('Option name ' . $ dto ->name . ' already exists. ' );
227228 }
@@ -232,36 +233,12 @@ private function updateRowsById(array $incomingById, array $currentById, string
232233 }
233234
234235 if ($ updates ) {
235- $ this ->connection -> update ( $ fullTable , $ updates , [ ' id ' => $ dto ->id ] );
236+ $ this ->dynamicListAttrRepository -> updateById ( $ listTable , ( int ) $ dto ->id , $ updates );
236237 }
237238 $ result [] = $ dto ;
238239 }
239240 }
240241
241242 return $ result ;
242243 }
243-
244- private function createFromDtos (string $ fullTable , array $ unique ): array
245- {
246- $ sql = 'INSERT INTO ' . $ fullTable . ' (name, listorder) VALUES (:name, :listOrder) ' ;
247- $ stmt = $ this ->connection ->prepare ($ sql );
248-
249- $ result = [];
250- foreach ($ unique as $ opt ) {
251- $ stmt ->bindValue ('name ' , $ opt ->name , ParameterType::STRING );
252- $ stmt ->bindValue ('listOrder ' , $ opt ->listOrder , ParameterType::INTEGER );
253- $ stmt ->executeStatement ();
254-
255- $ inserted = new DynamicListAttrDto (
256- id: (int ) $ this ->connection ->lastInsertId (),
257- name: $ opt ->name ,
258- listOrder: $ opt ->listOrder
259- );
260-
261- $ result [] = $ inserted ;
262- }
263- $ this ->connection ->commit ();
264-
265- return $ result ;
266- }
267244}
0 commit comments