Passed
Push — 2.x ( 5e81a5...d097d1 )
by Aleksei
14:17
created

Runner::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Transaction;
6
7
use Cycle\Database\Driver\DriverInterface;
8
use Cycle\ORM\Command\CommandInterface;
9
use Cycle\ORM\Command\CompleteMethodInterface;
10
use Cycle\ORM\Command\RollbackMethodInterface;
11
use Cycle\ORM\Command\StoreCommandInterface;
12
use Cycle\ORM\Exception\RunnerException;
13
use Traversable;
14
15
/**
16
 * @internal
17
 */
18
final class Runner implements RunnerInterface
19
{
20
    private const MODE_IGNORE_TRANSACTION = 0;
21
    private const MODE_CONTINUE_TRANSACTION = 1;
22
    private const MODE_OPEN_TRANSACTION = 2;
23
24
    /** @var DriverInterface[] */
25
    private array $drivers = [];
26
27
    /** @var CommandInterface[] */
28
    private array $executed = [];
29
30
    private int $countExecuted = 0;
31
32 2996
    private function __construct(
33
        private int $mode
34
    ) {
35
    }
36
37 2668
    public function run(CommandInterface $command): void
38
    {
39 2668
        if ($command instanceof Traversable) {
40 168
            foreach ($command as $cmd) {
41 168
                $this->run($cmd);
42
            }
43 168
            return;
44
        }
45
        // found the same link from multiple branches
46 2668
        if ($command->isExecuted()) {
47
            $this->countExecuted++;
48
            return;
49
        }
50 2668
        if ($command instanceof StoreCommandInterface && !$command->hasData()) {
51 64
            return;
52
        }
53
54 2620
        if ($command->getDatabase() !== null) {
55 2532
            $driver = $command->getDatabase()->getDriver();
56
57 2532
            if (!\in_array($driver, $this->drivers, true)) {
58 2532
                $this->prepareTransaction($driver);
59
60 2516
                $this->drivers[] = $driver;
61
            }
62
        }
63
64 2604
        $command->execute();
65 2588
        $this->countExecuted++;
66
67 2588
        if ($command instanceof CompleteMethodInterface || $command instanceof RollbackMethodInterface) {
68
            $this->executed[] = $command;
69
        }
70
    }
71
72
    public function count(): int
73
    {
74
        return $this->countExecuted;
75
    }
76
77 2828
    public function complete(): void
78
    {
79 2828
        if ($this->mode === self::MODE_OPEN_TRANSACTION) {
80
            // Commit all of the open and normalized database transactions
81 2796
            foreach (array_reverse($this->drivers) as $driver) {
82
                /** @var DriverInterface $driver */
83 2380
                $driver->commitTransaction();
84
            }
85
        }
86
87
        // Other type of transaction to close
88 2828
        foreach ($this->executed as $command) {
89
            if ($command instanceof CompleteMethodInterface) {
90
                $command->complete();
91
            }
92
        }
93 2828
        $this->drivers = $this->executed = [];
94
    }
95
96 192
    public function rollback(): void
97
    {
98 192
        if ($this->mode === self::MODE_OPEN_TRANSACTION) {
99
            // Close all open and normalized database transactions
100 144
            foreach (array_reverse($this->drivers) as $driver) {
101
                /** @var DriverInterface $driver */
102 88
                $driver->rollbackTransaction();
103
            }
104
        }
105
106
        // Close all of external types of transactions (revert changes)
107 192
        foreach (array_reverse($this->executed) as $command) {
108
            if ($command instanceof RollbackMethodInterface) {
109
                $command->rollBack();
110
            }
111
        }
112
113 192
        $this->drivers = $this->executed = [];
114
    }
115
116
    /**
117
     * Create Runner in the 'inner transaction' mode.
118
     * In this case the Runner will open new transaction for each used driver connection
119
     * and will close they on finish.
120
     */
121 2916
    public static function innerTransaction(): self
122
    {
123 2916
        return new self(self::MODE_OPEN_TRANSACTION);
124
    }
125
126
    /**
127
     * Create Runner in the 'outer transaction' mode.
128
     * In this case the Runner won't begin transactions, you should do it previously manually.
129
     * This mode also means the Runner WON'T commit or rollback opened transactions on success or fail.
130
     * But commands that implement CompleteMethodInterface or RollbackMethodInterface will be called.
131
     *
132
     * @param bool $strict Check transaction statuses before commands running.
133
     *        When strict mode is {@see true} and a transaction of any used driver didn't be opened
134
     *        the Runner will throw an Exception and stop Unit of Work.
135
     *        When strict mode is {@see false} the Runner won't begin/commit/rollback transactions
136
     *        and will ignore any transaction statuses.
137
     */
138 88
    public static function outerTransaction(bool $strict = true): self
139
    {
140 88
        return new self($strict ? self::MODE_CONTINUE_TRANSACTION : self::MODE_IGNORE_TRANSACTION);
141
    }
142
143 2532
    private function prepareTransaction(DriverInterface $driver): void
144
    {
145 2532
        if ($this->mode === self::MODE_IGNORE_TRANSACTION) {
146 24
            return;
147
        }
148
149 2508
        if ($this->mode === self::MODE_CONTINUE_TRANSACTION) {
150 64
            if ($driver->getTransactionLevel() === 0) {
151 40
                throw new RunnerException(sprintf(
152
                    'The `%s` driver connection has no opened transaction.',
153 40
                    $driver->getType()
154
                ));
155
            }
156 40
            return;
157
        }
158
159 2452
        $driver->beginTransaction();
160
    }
161
}
162