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

PartHandler   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 142
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 18
eloc 55
dl 0
loc 142
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A insertChunk() 0 29 5
A flush() 0 21 5
A hasPending() 0 3 1
A reset() 0 5 1
A setAddToPartRepair() 0 3 1
A __construct() 0 4 1
A addPart() 0 16 2
A getFailedNumbers() 0 3 1
A getInsertedNumbers() 0 3 1
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 part record creation during header storage.
12
 */
13
final class PartHandler
14
{
15
    /** @var array Pending parts to insert */
16
    private array $parts = [];
17
18
    /** @var array Part numbers successfully inserted */
19
    private array $insertedPartNumbers = [];
20
21
    /** @var array Part numbers that failed to insert */
22
    private array $failedPartNumbers = [];
23
24
    private int $chunkSize;
25
26
    private bool $addToPartRepair;
27
28
    public function __construct(int $chunkSize = 5000, bool $addToPartRepair = true)
29
    {
30
        $this->chunkSize = max(100, $chunkSize);
31
        $this->addToPartRepair = $addToPartRepair;
32
    }
33
34
    /**
35
     * Reset state for a new batch.
36
     */
37
    public function reset(): void
38
    {
39
        $this->parts = [];
40
        $this->insertedPartNumbers = [];
41
        $this->failedPartNumbers = [];
42
    }
43
44
    /**
45
     * Set whether to add failed parts to repair queue.
46
     */
47
    public function setAddToPartRepair(bool $value): void
48
    {
49
        $this->addToPartRepair = $value;
50
    }
51
52
    /**
53
     * Add a part to the pending insert queue.
54
     *
55
     * @return bool True if chunk was flushed successfully (or not needed), false on flush failure
56
     */
57
    public function addPart(int $binaryId, array $header): bool
58
    {
59
        $this->parts[] = [
60
            'binaries_id' => $binaryId,
61
            'number' => $header['Number'],
62
            'messageid' => $header['Message-ID'],
63
            'partnumber' => $header['matches'][2],
64
            'size' => $header['Bytes'],
65
        ];
66
67
        // Auto-flush when chunk size reached
68
        if (\count($this->parts) >= $this->chunkSize) {
69
            return $this->flush();
70
        }
71
72
        return true;
73
    }
74
75
    /**
76
     * Flush pending parts to database.
77
     */
78
    public function flush(): bool
79
    {
80
        if (empty($this->parts)) {
81
            return true;
82
        }
83
84
        $success = $this->insertChunk($this->parts);
85
86
        if ($success) {
87
            foreach ($this->parts as $part) {
88
                $this->insertedPartNumbers[] = $part['number'];
89
            }
90
        } else {
91
            foreach ($this->parts as $part) {
92
                $this->failedPartNumbers[] = $part['number'];
93
            }
94
        }
95
96
        $this->parts = [];
97
98
        return $success;
99
    }
100
101
    private function insertChunk(array $parts): bool
102
    {
103
        $placeholders = [];
104
        $bindings = [];
105
        $driver = DB::getDriverName();
106
107
        foreach ($parts as $row) {
108
            $placeholders[] = '(?,?,?,?,?)';
109
            $bindings[] = $row['binaries_id'];
110
            $bindings[] = $row['number'];
111
            $bindings[] = $row['messageid'];
112
            $bindings[] = $row['partnumber'];
113
            $bindings[] = $row['size'];
114
        }
115
116
        $sql = $driver === 'sqlite'
117
            ? 'INSERT OR IGNORE INTO parts (binaries_id, number, messageid, partnumber, size) VALUES '.implode(',', $placeholders)
118
            : 'INSERT IGNORE INTO parts (binaries_id, number, messageid, partnumber, size) VALUES '.implode(',', $placeholders);
119
120
        try {
121
            DB::statement($sql, $bindings);
122
123
            return true;
124
        } catch (\Throwable $e) {
125
            if (config('app.debug') === true) {
126
                Log::error('Parts chunk insert failed: '.$e->getMessage());
127
            }
128
129
            return false;
130
        }
131
    }
132
133
    /**
134
     * Get numbers of successfully inserted parts.
135
     */
136
    public function getInsertedNumbers(): array
137
    {
138
        return $this->insertedPartNumbers;
139
    }
140
141
    /**
142
     * Get numbers of failed part inserts.
143
     */
144
    public function getFailedNumbers(): array
145
    {
146
        return $this->failedPartNumbers;
147
    }
148
149
    /**
150
     * Check if there are pending parts waiting to be flushed.
151
     */
152
    public function hasPending(): bool
153
    {
154
        return ! empty($this->parts);
155
    }
156
}
157
158