Failed Conditions
Push — master ( 11c8ec...b620eb )
by Bas
05:48 queued 10s
created

ManagesTransactions::transactionLevel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace LaravelFreelancerNL\Aranguent\Concerns;
4
5
use ArangoDBClient\Transaction as ArangoTransaction;
6
use Closure;
7
use Exception;
8
use Illuminate\Support\Fluent as IlluminateFluent;
9
10
trait ManagesTransactions
11
{
12
    protected $transactions = 0;
13
14
    protected $transactionCommands = [];
15
16
    protected $arangoTransaction;
17
18
    /**
19
     * Execute a Closure within a transaction.
20
     *
21
     * @param \Closure $callback
22
     * @param array    $options
23
     * @param int      $attempts
24
     *
25
     * @throws \Exception|\Throwable
26
     *
27
     * @return mixed
28
     */
29
    public function transaction(Closure $callback, $options = [], $attempts = 1)
30
    {
31
        $this->beginTransaction();
32
33
        return tap($callback($this), function () use ($options, $attempts) {
34
            $this->commit($options, $attempts);
35
        });
36
    }
37
38
    /**
39
     * Start a new database transaction.
40
     *
41
     * @throws \Exception
42
     *
43
     * @return void
44
     */
45 4
    public function beginTransaction()
46
    {
47 4
        $this->transactions++;
48
49 4
        $this->transactionCommands[$this->transactions] = [];
50
51 4
        $this->fireConnectionEvent('beganTransaction');
0 ignored issues
show
Bug introduced by
It seems like fireConnectionEvent() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

51
        $this->/** @scrutinizer ignore-call */ 
52
               fireConnectionEvent('beganTransaction');
Loading history...
52 4
    }
53
54
    /**
55
     * Add a command to the transaction. Parameters must include:
56
     * collections['write'][]: collections that are written to
57
     * collections['read'][]: collections that are read from
58
     * command: the db command to execute.
59
     *
60
     * @param \Illuminate\Support\Fluent $command
61
     */
62 4
    public function addTransactionCommand(IlluminateFluent $command)
63
    {
64 4
        $this->transactionCommands[$this->transactions][] = $command;
65 4
    }
66
67
    /**
68
     * Add a query command to the transaction.
69
     *
70
     * @param $query
71
     * @param $bindings
72
     * @param array|null $collections
73
     *
74
     * @return IlluminateFluent
75
     */
76 1
    public function addQueryToTransaction($query, $bindings = [], $collections = [])
77
    {
78 1
        [$query, $bindings, $collections] = $this->handleQueryBuilder($query, $bindings, $collections);
0 ignored issues
show
Bug introduced by
It seems like handleQueryBuilder() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

78
        /** @scrutinizer ignore-call */ 
79
        [$query, $bindings, $collections] = $this->handleQueryBuilder($query, $bindings, $collections);
Loading history...
79
80 1
         $jsCommand = 'db._query(aql`' . $query . '`';
81 1
        if (!empty($bindings)) {
82
            $bindings = json_encode($bindings);
83
            $jsCommand .= ', ' . $bindings;
84
        }
85 1
        $jsCommand .= ');';
86 1
        $command = new IlluminateFluent([
87 1
            'name'        => 'aqlQuery',
88 1
            'command'     => $jsCommand,
89 1
            'collections' => $collections,
90
        ]);
91
92 1
        $this->addTransactionCommand($command);
93
94 1
        return $command;
95
    }
96
97
    /**
98
     * Commit the current transaction.
99
     *
100
     * @param array $options
101
     * @param int   $attempts
102
     *
103
     * @throws Exception
104
     *
105
     * @return mixed
106
     */
107 2
    public function commit($options = [], $attempts = 1)
108
    {
109 2
        if (!$this->transactions > 0) {
110 1
            throw new Exception('Transaction committed before starting one.');
111
        }
112
        if (
113 1
            !isset($this->transactionCommands[$this->transactions])
114 1
            || empty($this->transactionCommands[$this->transactions])
115
        ) {
116
            throw new Exception('Cannot commit an empty transaction.');
117
        }
118
119 1
        $options['collections'] = $this->compileTransactionCollections();
120
121 1
        $options['action'] = $this->compileTransactionAction();
122
123 1
        $results = $this->executeTransaction($options, $attempts);
124
125 1
        $this->fireConnectionEvent('committed');
126
127 1
        return $results;
128
    }
129
130 1
    public function executeTransaction($options, $attempts = 1)
131
    {
132 1
        $results = null;
133
134 1
        $this->arangoTransaction = new ArangoTransaction($this->arangoConnection, $options);
135 1
        for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
136
            try {
137 1
                $results = $this->arangoTransaction->execute();
138
139 1
                $this->transactions--;
140
            } catch (Exception $e) {
141
                $this->fireConnectionEvent('rollingBack');
142
143
                $results = $this->handleTransactionException($e, $currentAttempt, $attempts);
144
            }
145
        }
146
147 1
        return $results;
148
    }
149
150
    /**
151
     * Handle an exception encountered when running a transacted statement.
152
     *
153
     * @param $e
154
     * @param $currentAttempt
155
     * @param $attempts
156
     *
157
     * @return mixed
158
     */
159
    protected function handleTransactionException($e, $currentAttempt, $attempts)
160
    {
161
        $retry = false;
162
        // If the failure was due to a lost connection we can just try again.
163
        if ($this->causedByLostConnection($e)) {
0 ignored issues
show
Bug introduced by
It seems like causedByLostConnection() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
        if ($this->/** @scrutinizer ignore-call */ causedByLostConnection($e)) {
Loading history...
164
            $this->reconnect();
0 ignored issues
show
Bug introduced by
It seems like reconnect() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
            $this->/** @scrutinizer ignore-call */ 
165
                   reconnect();
Loading history...
165
            $retry = true;
166
        }
167
168
        // Retry if the failure was caused by a deadlock or ArangoDB suggests we try so.
169
        // We can check if we have exceeded the maximum attempt count for this and if
170
        // we haven't we will return and try this transaction again.
171
        if (
172
            $this->causedByDeadlock($e) &&
0 ignored issues
show
Bug introduced by
It seems like causedByDeadlock() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
            $this->/** @scrutinizer ignore-call */ 
173
                   causedByDeadlock($e) &&
Loading history...
173
            $currentAttempt < $attempts
174
        ) {
175
            $retry = true;
176
        }
177
178
        if ($retry) {
179
            return $this->arangoTransaction->execute();
180
        }
181
182
        throw $e;
183
    }
184
185
    /**
186
     * compile an array of unique collections that are used to read from and/or write to.
187
     *
188
     * @return array
189
     */
190 1
    public function compileTransactionCollections()
191
    {
192 1
        $result['write'] = [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.
Loading history...
193 1
        $result['read'] = [];
194
195 1
        $commands = $this->transactionCommands[$this->transactions];
196
197 1
        foreach ($commands as $command) {
198 1
            if (isset($command->collections['write'])) {
199 1
                $write = $command->collections['write'];
200 1
                if (is_string($write)) {
201 1
                    $write = (array) $write;
202
                }
203 1
                $result['write'] = array_merge($result['write'], $write);
204
            }
205 1
            if (isset($command->collections['read'])) {
206 1
                $read = $command->collections['read'];
207 1
                if (is_string($read)) {
208 1
                    $read = (array) $read;
209
                }
210 1
                $result['read'] = array_merge($result['write'], $read);
211
            }
212
        }
213
214 1
        $result['read'] = array_merge($result['read'], $result['write']);
215
216 1
        $result['write'] = array_filter(array_unique($result['write']));
217 1
        if (empty($result['write'])) {
218
            unset($result['write']);
219
        }
220
221 1
        $result['read'] = array_filter(array_unique($result['read']));
222 1
        if (empty($result['read'])) {
223
            unset($result['read']);
224
        }
225
226 1
        $result = array_filter($result);
227
228 1
        return $result;
229
    }
230
231 2
    public function compileTransactionAction()
232
    {
233 2
        $commands = collect($this->transactionCommands[$this->transactions]);
234
235 2
        $action = "function () { var db = require('@arangodb').db; ";
236 2
        $action .= $commands->implode('command', ' ');
237 2
        $action .= ' }';
238
239 2
        return $action;
240
    }
241
242
    /**
243
     * Handle an exception from a rollback.
244
     *
245
     * @param \Exception $e
246
     *
247
     * @throws \Exception
248
     */
249
    protected function handleRollBackException($e)
250
    {
251
        if ($this->causedByLostConnection($e)) {
252
            $this->transactions = 0;
253
        }
254
255
        throw $e;
256
    }
257
258
    /**
259
     * Get the number of active transactions.
260
     *
261
     * @return int
262
     */
263 121
    public function transactionLevel()
264
    {
265 121
        return $this->transactions;
266
    }
267
268 2
    public function getTransactionCommands()
269
    {
270 2
        return $this->transactionCommands;
271
    }
272
273
    //Override unused trait transaction functions with dummy methods
274
275
//    /**
276
//     * Dummy.
277
//     *
278
//     * @param $e
279
//     */
280
//    public function handleBeginTransactionException($e)
281
//    {
282
//        //
283
//    }
284
285
//    /**
286
//     * Dummy override: Rollback the active database transaction.
287
//     *
288
//     * @param int|null $toLevel
289
//     *
290
//     * @throws \Exception
291
//     *
292
//     * @return void
293
//     */
294
//    public function rollBack($toLevel = null)
295
//    {
296
//        //
297
//    }
298
//
299
//    /**
300
//     * Dummy override: ArangoDB rolls back the entire transaction on a failure.
301
//     *
302
//     * @param int $toLevel
303
//     *
304
//     * @return void
305
//     */
306
//    protected function performRollBack($toLevel)
307
//    {
308
//        //
309
//    }
310
//
311
//    /**
312
//     * Create a save point within the database.
313
//     * Not supported by ArangoDB(?).
314
//     *
315
//     * @return void
316
//     */
317
//    protected function createSavepoint()
318
//    {
319
//        //
320
//    }
321
}
322