Transaction   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 137
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 4
dl 0
loc 137
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A store() 0 4 1
A delete() 0 4 1
A addCommand() 0 4 1
A getCommands() 0 11 3
F run() 0 70 16
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM;
9
10
use Spiral\Database\Entities\Driver;
11
use Spiral\ORM\Exceptions\RecordException;
12
13
/**
14
 * Singular ORM transaction with ability to automatically open transaction for all involved
15
 * drivers.
16
 *
17
 * Drivers will be automatically fetched from commands. Potentially Transaction can be improved
18
 * to optimize commands inside it (batch insert, batch delete and etc).
19
 *
20
 * Technically Transaction can work as classic UnitOfWork with ability to watch entities, but it's
21
 * recommended to create additional abstraction at top with proper business rules.
22
 */
23
final class Transaction implements TransactionInterface
24
{
25
    /**
26
     * @var CommandInterface[]
27
     */
28
    private $commands = [];
29
30
    /**
31
     * Store entity information (update or insert).
32
     *
33
     * @param RecordInterface $record
34
     * @param bool            $queueRelations
35
     *
36
     * @throws RecordException
37
     */
38
    public function store(RecordInterface $record, bool $queueRelations = true)
39
    {
40
        $this->addCommand($record->queueStore($queueRelations));
41
    }
42
43
    /**
44
     * Delete entity from database.
45
     *
46
     * @param RecordInterface $record
47
     *
48
     * @throws RecordException
49
     */
50
    public function delete(RecordInterface $record)
51
    {
52
        $this->addCommand($record->queueDelete());
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function addCommand(CommandInterface $command)
59
    {
60
        $this->commands[] = $command;
61
    }
62
63
    /**
64
     * Will return flattened list of commands.
65
     *
66
     * @return \Generator
67
     */
68
    public function getCommands()
69
    {
70
        foreach ($this->commands as $command) {
71
            if ($command instanceof \Traversable) {
72
                //Nested commands
73
                yield from $command;
74
            }
75
76
            yield $command;
77
        }
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     *
83
     * @param bool $forceTransaction When set to false transaction would not be started when only
84
     *                               single command is presented inside (all commands are flatten
85
     *                               before appearing inside this method).
86
     * @param bool $clean            When set to true - transaction will be cleaned after
87
     *                               execution, default behaviour.
88
     */
89
    public function run(bool $forceTransaction = false, bool $clean = true)
90
    {
91
        /**
92
         * @var Driver[]           $drivers
93
         * @var CommandInterface[] $commands
94
         */
95
        $drivers = $commands = [];
96
97
        foreach ($this->getCommands() as $command) {
98
            if ($command instanceof SQLCommandInterface) {
99
                $driver = $command->getDriver();
100
                if (!empty($driver) && !in_array($driver, $drivers)) {
101
                    $drivers[] = $driver;
102
                }
103
            }
104
105
            $commands[] = $command;
106
        }
107
108
        if (empty($commands)) {
109
            return;
110
        }
111
112
        //Commands we executed and drivers with started transactions
113
        $executedCommands = $wrappedDrivers = [];
114
115
        try {
116
            if ($forceTransaction || count($commands) > 1) {
117
                //Starting transactions
118
                foreach ($drivers as $driver) {
119
                    $driver->beginTransaction();
120
                    $wrappedDrivers[] = $driver;
121
                }
122
            }
123
124
            //Run commands
125
            foreach ($commands as $command) {
126
                $command->execute();
127
                $executedCommands[] = $command;
128
            }
129
        } catch (\Throwable $e) {
130
            foreach (array_reverse($wrappedDrivers) as $driver) {
131
                /** @var Driver $driver */
132
                $driver->rollbackTransaction();
133
            }
134
135
            foreach (array_reverse($executedCommands) as $command) {
136
                /** @var CommandInterface $command */
137
                $command->rollBack();
138
            }
139
140
            $this->commands = [];
141
            throw $e;
142
        }
143
144
        foreach (array_reverse($wrappedDrivers) as $driver) {
145
            /** @var Driver $driver */
146
            $driver->commitTransaction();
147
        }
148
149
        foreach ($executedCommands as $command) {
150
            //This is the point when record will get related PK and FKs filled
151
            $command->complete();
152
        }
153
154
        //Clean transaction
155
        if ($clean) {
156
            $this->commands = [];
157
        }
158
    }
159
}
160