Test Setup Failed
Pull Request — master (#10)
by Bas
02:56 queued 01:19
created

ManagesTransactions::compileTransactionAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 1
nc 1
nop 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
     * @return mixed
25
     *
26
     * @throws \Exception|\Throwable
27
     */
28
    public function transaction(Closure $callback, $options = [], $attempts = 1)
29
    {
30
        $this->beginTransaction();
31
32
        return tap($callback($this), function () use ($options, $attempts) {
33
            $this->commit($options, $attempts);
34
        });
35
    }
36
37
    /**
38
     * Start a new database transaction.
39
     *
40
     * @return void
41
     *
42
     * @throws \Exception
43
     */
44
    public function beginTransaction()
45
    {
46
        $this->transactions++;
47
48
        $this->transactionCommands[$this->transactions] = [];
49
50
        $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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
51
    }
52
53
    /**
54
     * Add a command to the transaction. Parameters must include:
55
     * collections['write'][]: collections that are written to
56
     * collections['read'][]: collections that are read from
57
     * command: the db command to execute.
58
     *
59
     * @param \Illuminate\Support\Fluent $command
60
     */
61
    public function addTransactionCommand(IlluminateFluent $command)
62
    {
63
        $this->transactionCommands[$this->transactions][] = $command;
64
    }
65
66
    /**
67
     * Add a query command to the transaction.
68
     *
69
     * @param $query
70
     * @param $bindings
71
     * @param array|null $collections
72
     * @return IlluminateFluent
73
     */
74
    public function addQueryToTransaction($query, $bindings = [], $collections = null)
75
    {
76
        //If transaction collections aren't provided we will try to extract them from the query.
77
        if (empty($collections)) {
78
            $collections = $this->extractTransactionCollections($query, $bindings, $collections);
79
        }
80
81
//        $query = addslashes($query);
82
        $jsCommand = 'db._query(aql`'.$query.'`';
83
        if (! empty($bindings)) {
84
            $bindings = json_encode($bindings);
85
            $jsCommand .= ', '.$bindings;
86
        }
87
        $jsCommand .= ');';
88
        $command = new IlluminateFluent([
89
            'name' => 'aqlQuery',
90
            'command' => $jsCommand,
91
            'collections' => $collections,
92
        ]);
93
94
        $this->addTransactionCommand($command);
95
96
        return $command;
97
    }
98
99
    /**
100
     * Transaction like a list of read collections to prevent possible read deadlocks.
101
     * Transactions require a list of write collections to prepare write locks.
102
     *
103
     * @param $query
104
     * @param $bindings
105
     * @param $collections
106
     * @return mixed
107
     */
108
    public function extractTransactionCollections($query, $bindings, $collections)
109
    {
110
        //Extract write collections
111
        $collections = $this->extractReadCollections($query, $bindings, $collections);
112
        $collections = $this->extractWriteCollections($query, $bindings, $collections);
113
114
        return $collections;
115
    }
116
117
    /**
118
     * Extract collections that are read from in a query. Not required but can prevent deadlocks.
119
     *
120
     * @param $query
121
     * @param $bindings
122
     * @param $collections
123
     * @return mixed
124
     */
125
    public function extractReadCollections($query, $bindings, $collections)
126
    {
127
        $extractedCollections = [];
128
        //WITH statement at the start of the query
129
        preg_match_all('/^(?:\s+?)WITH(?:\s+?)([\S\s]*?)(?:\s+?)FOR/mis', $query, $rawWithCollections);
130
        foreach ($rawWithCollections[1] as $key => $value) {
131
            $splits = preg_split("/\s*,\s*/", $value);
132
            $extractedCollections = array_merge($extractedCollections, $splits);
133
        }
134
135
        //FOR statements
136
        preg_match_all('/FOR(?:\s+?)(?:\w+)(?:\s+?)(?:IN|INTO)(?:\s+?)(?!OUTBOUND|INBOUND|ANY)(@?@?\w+(?!\.))/mis', $query, $rawForCollections);
137
        $extractedCollections = array_merge($extractedCollections, $rawForCollections[1]);
138
139
        //Document functions which require a document as their first argument
140
        preg_match_all('/(?:DOCUMENT\(|ATTRIBUTES\(|HAS\(|KEEP\(|LENGTH\(|MATCHES\(|PARSE_IDENTIFIER\(|UNSET\(|UNSET_RECURSIVE\(|VALUES\(|OUTBOUND|INBOUND|ANY)(?:\s+?)(?!\{)(?:\"|\'|\`)(@?@?\w+)\/(?:\w+)(?:\"|\'|\`)/mis', $query, $rawDocCollections);
141
        $extractedCollections = array_merge($extractedCollections, $rawDocCollections[1]);
142
143
        $extractedCollections = array_map('trim', $extractedCollections);
144
145
        $extractedCollections = $this->getCollectionByBinding($extractedCollections, $bindings);
146
147
        if (isset($collections['read'])) {
148
            $collections['read'] = array_merge($collections['read'], $extractedCollections);
149
        } else {
150
            $collections['read'] = $extractedCollections;
151
        }
152
153
        $collections['read'] = array_unique($collections['read']);
154
155
        return $collections;
156
    }
157
158
    /**
159
     * Extract collections that are written to in a query.
160
     *
161
     * @param $query
162
     * @param $bindings
163
     * @param $collections
164
     * @return mixed
165
     */
166
    public function extractWriteCollections($query, $bindings, $collections)
167
    {
168
        preg_match_all('/(?:\s+?)(?:INSERT|REPLACE|UPDATE|REMOVE)(?:\s+?)(?:{(?:.*?)}|@?@?\w+?)(?:\s+?)(?:IN|INTO)(?:\s+?)(@?@?\w+)/mis', $query, $extractedCollections);
169
        $extractedCollections = array_map('trim', $extractedCollections[1]);
170
171
        $extractedCollections = $this->getCollectionByBinding($extractedCollections, $bindings);
172
173
        if (isset($collections['write'])) {
174
            $collections['write'] = array_merge($collections['write'], $extractedCollections);
175
        } else {
176
            $collections['write'] = $extractedCollections;
177
        }
178
179
        $collections['read'] = array_unique($collections['read']);
180
181
        return $collections;
182
    }
183
184
    /**
185
     * Get the collection names that are bound in a query.
186
     *
187
     * @param $collections
188
     * @param $bindings
189
     * @return mixed
190
     */
191
    public function getCollectionByBinding($collections, $bindings)
192
    {
193
        foreach ($collections as $key => $collection) {
194
            if (strpos($collection, '@@') === 0 && isset($bindings[$collection])) {
195
                $collections[$key] = $bindings[$collection];
196
            }
197
        }
198
199
        return $collections;
200
    }
201
202
    /**
203
     * Commit the current transaction.
204
     *
205
     * @param array $options
206
     * @param int $attempts
207
     * @return mixed
208
     * @throws Exception
209
     */
210
    public function commit($options = [], $attempts = 1)
211
    {
212
        if (! $this->transactions > 0) {
213
            throw new \Exception('Transaction committed before starting one.');
214
        }
215
        if (! isset($this->transactionCommands[$this->transactions]) || empty($this->transactionCommands[$this->transactions])) {
216
            throw new \Exception('Cannot commit an empty transaction.');
217
        }
218
219
        $options['collections'] = $this->compileTransactionCollections();
220
221
        $options['action'] = $this->compileTransactionAction();
222
223
        $results = $this->executeTransaction($options, $attempts);
224
225
        $this->fireConnectionEvent('committed');
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
226
227
        return $results;
228
    }
229
230
    public function executeTransaction($options, $attempts = 1)
231
    {
232
        $results = null;
233
234
        $this->arangoTransaction = new ArangoTransaction($this->arangoConnection, $options);
0 ignored issues
show
Bug introduced by
The property arangoConnection does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
235
236
        for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
237
            try {
238
                $results = $this->arangoTransaction->execute();
239
240
                $this->transactions--;
241
            } catch (Exception $e) {
242
                $this->fireConnectionEvent('rollingBack');
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
243
244
                $results = $this->handleTransactionException($e, $currentAttempt, $attempts);
245
            }
246
        }
247
248
        return $results;
249
    }
250
251
    /**
252
     * Handle an exception encountered when running a transacted statement.
253
     *
254
     * @param Exception $e
255
     * @param int $currentAttempt
256
     * @param int $attempts
257
     * @return mixed
258
     */
259
    protected function handleTransactionException($e, $currentAttempt, $attempts)
260
    {
261
        $retry = false;
262
        // If the failure was due to a lost connection we can just try again.
263
        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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
264
            $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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
265
            $retry = true;
266
        }
267
268
        // Retry if the failure was caused by a deadlock or ArangoDB suggests we try so.
269
        // We can check if we have exceeded the maximum attempt count for this and if
270
        // we haven't we will return and try this transaction again.
271
        if ($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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
272
            $currentAttempt < $attempts) {
273
            $retry = true;
274
        }
275
276
        if ($retry) {
277
            return $this->arangoTransaction->execute();
278
        }
279
280
        throw $e;
281
    }
282
283
    /**
284
     * compile an array of unique collections that are used to read from and/or write to.
285
     *
286
     * @return array
287
     */
288
    public function compileTransactionCollections()
289
    {
290
        $result['write'] = [];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
291
        $result['read'] = [];
292
293
        $commands = $this->transactionCommands[$this->transactions];
294
295
        foreach ($commands as $command) {
296
            if (isset($command->collections['write'])) {
297
                $write = $command->collections['write'];
298
                if (is_string($write)) {
299
                    $write = (array) $write;
300
                }
301
                $result['write'] = array_merge($result['write'], $write);
302
            }
303
            if (isset($command->collections['read'])) {
304
                $read = $command->collections['read'];
305
                if (is_string($read)) {
306
                    $read = (array) $read;
307
                }
308
                $result['read'] = array_merge($result['write'], $read);
309
            }
310
        }
311
312
        $result['read'] = array_merge($result['read'], $result['write']);
313
314
        $result['write'] = array_filter(array_unique($result['write']));
315
        if (empty($result['write'])) {
316
            unset($result['write']);
317
        }
318
319
        $result['read'] = array_filter(array_unique($result['read']));
320
        if (empty($result['read'])) {
321
            unset($result['read']);
322
        }
323
324
        $result = array_filter($result);
325
326
        return $result;
327
    }
328
329
    public function compileTransactionAction()
330
    {
331
        $commands = collect($this->transactionCommands[$this->transactions]);
332
333
        $action = "function () { var db = require('@arangodb').db; ";
334
        $action .= $commands->implode('command', ' ');
335
        $action .= ' }';
336
337
        return $action;
338
    }
339
340
    /**
341
     * Handle an exception from a rollback.
342
     *
343
     * @param \Exception  $e
344
     *
345
     * @throws \Exception
346
     */
347
    protected function handleRollBackException($e)
348
    {
349
        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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
350
            $this->transactions = 0;
351
        }
352
353
        throw $e;
354
    }
355
356
    /**
357
     * Get the number of active transactions.
358
     *
359
     * @return int
360
     */
361
    public function transactionLevel()
362
    {
363
        return $this->transactions;
364
    }
365
366
    public function getTransactionCommands()
367
    {
368
        return $this->transactionCommands;
369
    }
370
371
    //Override unused trait transaction functions with dummy methods
372
373
    /**
374
     * Dummy.
375
     *
376
     * @param $e
377
     */
378
    public function handleBeginTransactionException($e)
0 ignored issues
show
Unused Code introduced by
The parameter $e is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
379
    {
380
        //
381
    }
382
383
    /**
384
     * Dummy override: Rollback the active database transaction.
385
     *
386
     * @param  int|null  $toLevel
387
     * @return void
388
     *
389
     * @throws \Exception
390
     */
391
    public function rollBack($toLevel = null)
0 ignored issues
show
Unused Code introduced by
The parameter $toLevel is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
392
    {
393
        //
394
    }
395
396
    /**
397
     * Dummy override: ArangoDB rolls back the entire transaction on a failure.
398
     *
399
     * @param  int  $toLevel
400
     * @return void
401
     */
402
    protected function performRollBack($toLevel)
0 ignored issues
show
Unused Code introduced by
The parameter $toLevel is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
403
    {
404
        //
405
    }
406
407
    /**
408
     * Create a save point within the database.
409
     * Not supported by ArangoDB(?).
410
     *
411
     * @return void
412
     */
413
    protected function createSavepoint()
414
    {
415
        //
416
    }
417
}
418