Passed
Push — master ( ae3f18...b8bba4 )
by Darko
10:59
created

HeaderStorageTransaction::cleanupCollections()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 41
rs 9.472
c 0
b 0
f 0
cc 4
nc 6
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Services\Binaries;
6
7
use Illuminate\Support\Facades\DB;
8
use Illuminate\Support\Facades\Log;
9
10
/**
11
 * Handles header storage transactions and rollback cleanup.
12
 */
13
final class HeaderStorageTransaction
14
{
15
    private CollectionHandler $collectionHandler;
16
17
    private BinaryHandler $binaryHandler;
18
19
    private PartHandler $partHandler;
20
21
    private string $batchNoise;
22
23
    private bool $hadErrors = false;
24
25
    public function __construct(
26
        CollectionHandler $collectionHandler,
27
        BinaryHandler $binaryHandler,
28
        PartHandler $partHandler
29
    ) {
30
        $this->collectionHandler = $collectionHandler;
31
        $this->binaryHandler = $binaryHandler;
32
        $this->partHandler = $partHandler;
33
        $this->batchNoise = bin2hex(random_bytes(8));
34
    }
35
36
    /**
37
     * Get the batch noise marker for this transaction.
38
     */
39
    public function getBatchNoise(): string
40
    {
41
        return $this->batchNoise;
42
    }
43
44
    /**
45
     * Start a new database transaction.
46
     */
47
    public function begin(): void
48
    {
49
        DB::beginTransaction();
50
        $this->hadErrors = false;
51
    }
52
53
    /**
54
     * Mark that an error occurred.
55
     */
56
    public function markError(): void
57
    {
58
        $this->hadErrors = true;
59
    }
60
61
    /**
62
     * Check if errors occurred.
63
     */
64
    public function hasErrors(): bool
65
    {
66
        return $this->hadErrors;
67
    }
68
69
    /**
70
     * Commit the transaction if no errors, rollback otherwise.
71
     */
72
    public function finish(): bool
73
    {
74
        if ($this->hadErrors) {
75
            $this->rollbackAndCleanup();
76
77
            return false;
78
        }
79
80
        try {
81
            DB::commit();
82
83
            return true;
84
        } catch (\Throwable $e) {
85
            $this->rollbackAndCleanup();
86
87
            if (config('app.debug') === true) {
88
                Log::error('HeaderStorageTransaction commit failed: '.$e->getMessage());
89
            }
90
91
            return false;
92
        }
93
    }
94
95
    /**
96
     * Perform rollback and cleanup any orphaned data.
97
     */
98
    private function rollbackAndCleanup(): void
99
    {
100
        try {
101
            DB::rollBack();
102
        } catch (\Throwable $e) {
103
            // Already rolled back
104
        }
105
106
        $this->cleanup();
107
    }
108
109
    /**
110
     * Cleanup rows that may have been inserted before rollback.
111
     */
112
    private function cleanup(): void
113
    {
114
        try {
115
            $this->cleanupParts();
116
            $this->cleanupBinaries();
117
            $this->cleanupCollections();
118
119
            // Final guard for sqlite
120
            if (DB::getDriverName() === 'sqlite') {
121
                DB::statement('DELETE FROM parts');
122
                DB::statement('DELETE FROM binaries');
123
                DB::statement('DELETE FROM collections');
124
            }
125
        } catch (\Throwable $e) {
126
            if (config('app.debug') === true) {
127
                Log::warning('Post-rollback cleanup failed: '.$e->getMessage());
128
            }
129
        }
130
    }
131
132
    private function cleanupParts(): void
133
    {
134
        $numbers = $this->partHandler->getInsertedNumbers();
135
        if (! empty($numbers)) {
136
            $placeholders = implode(',', array_fill(0, \count($numbers), '?'));
137
            DB::statement("DELETE FROM parts WHERE number IN ({$placeholders})", $numbers);
138
        }
139
    }
140
141
    private function cleanupBinaries(): void
142
    {
143
        $ids = $this->binaryHandler->getInsertedIds();
144
        if (! empty($ids)) {
145
            $placeholders = implode(',', array_fill(0, \count($ids), '?'));
146
            DB::statement("DELETE FROM binaries WHERE id IN ({$placeholders})", $ids);
147
        }
148
    }
149
150
    private function cleanupCollections(): void
151
    {
152
        $insertedIds = $this->collectionHandler->getInsertedIds();
153
        $allIds = $this->collectionHandler->getAllIds();
154
        $hashes = $this->collectionHandler->getBatchHashes();
155
156
        $ids = ! empty($insertedIds) ? $insertedIds : $allIds;
157
158
        if (! empty($ids)) {
159
            $placeholders = implode(',', array_fill(0, \count($ids), '?'));
160
161
            // Remove parts and binaries referencing these collections, then collections
162
            DB::statement(
163
                "DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN ({$placeholders}))",
164
                $ids
165
            );
166
            DB::statement("DELETE FROM binaries WHERE collections_id IN ({$placeholders})", $ids);
167
            DB::statement("DELETE FROM collections WHERE id IN ({$placeholders})", $ids);
168
        } elseif (! empty($hashes)) {
169
            $placeholders = implode(',', array_fill(0, \count($hashes), '?'));
170
171
            DB::statement(
172
                "DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders})))",
173
                $hashes
174
            );
175
            DB::statement(
176
                "DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders}))",
177
                $hashes
178
            );
179
            DB::statement("DELETE FROM collections WHERE collectionhash IN ({$placeholders})", $hashes);
180
        } else {
181
            // Fallback by noise marker
182
            DB::statement(
183
                'DELETE FROM parts WHERE binaries_id IN (SELECT b.id FROM binaries b WHERE b.collections_id IN (SELECT c.id FROM collections c WHERE c.noise = ?))',
184
                [$this->batchNoise]
185
            );
186
            DB::statement(
187
                'DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE noise = ?)',
188
                [$this->batchNoise]
189
            );
190
            DB::statement('DELETE FROM collections WHERE noise = ?', [$this->batchNoise]);
191
        }
192
    }
193
}
194
195