NestableUnitOfWork   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 88.52%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 31
c 7
b 1
f 0
lcom 1
cbo 5
dl 0
loc 238
ccs 108
cts 122
cp 0.8852
rs 9.8

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B commit() 0 29 4
A performCleanup() 0 7 2
B start() 0 35 4
C rollback() 0 34 7
A isStarted() 0 4 1
doStart() 0 1 ?
doCommit() 0 1 ?
doRollback() 0 1 ?
A performInnerCommit() 0 20 3
A assertStarted() 0 6 2
A stop() 0 5 1
A clear() 0 4 1
A commitInnerUnitOfWork() 0 8 3
A registerInnerUnitOfWork() 0 4 1
saveAggregates() 0 1 ?
notifyListenersPrepareCommit() 0 1 ?
notifyListenersCleanup() 0 1 ?
A setLogger() 0 4 1
1
<?php
2
3
/*
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * The software is based on the Axon Framework project which is
17
 * licensed under the Apache 2.0 license. For more information on the Axon Framework
18
 * see <http://www.axonframework.org/>.
19
 * 
20
 * This software consists of voluntary contributions made by many individuals
21
 * and is licensed under the MIT license. For more information, see
22
 * <http://www.governor-framework.org/>.
23
 */
24
25
namespace Governor\Framework\UnitOfWork;
26
27
use Psr\Log\LoggerInterface;
28
use Psr\Log\LoggerAwareInterface;
29
use Governor\Framework\Common\Logging\NullLogger;
30
31
/**
32
 * Description of DummyUnitOfWork
33
 *
34
 * @author    "David Kalosi" <[email protected]>
35
 * @license   <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a>
36
 */
37
abstract class NestableUnitOfWork implements UnitOfWorkInterface, LoggerAwareInterface
38
{
39
40
    /**
41
     * @var UnitOfWorkInterface
42
     */
43
    private $outerUnitOfWork;
44
45
    /**
46
     * @var NestableUnitOfWork[]
47
     */
48
    private $innerUnitsOfWork = [];
49
50
    /**
51
     * @var bool
52
     */
53
    private $isStarted;
54
55
    /**
56
     * @var LoggerInterface
57
     */
58
    protected $logger;
59
60 46
    function __construct()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
61
    {
62 46
        $this->logger = new NullLogger();
63 46
    }
64
65 28
    public function commit()
66
    {
67 28
        $this->logger->debug("Committing Unit Of Work");
68 28
        $this->assertStarted();
69
        try {
70 28
            $this->notifyListenersPrepareCommit();
71 27
            $this->saveAggregates();
72 26
            if (null === $this->outerUnitOfWork) {
73 24
                $this->logger->debug("This Unit Of Work is not nested. Finalizing commit...");
74 24
                $this->doCommit();
75 22
                $this->stop();
76 22
                $this->performCleanup();
77 22
            } else {
78 5
                $this->logger->debug("This Unit Of Work is nested. Commit will be finalized by outer Unit Of Work.");
79
            }
80 28
        } catch (\RuntimeException $ex) {
81 3
            $this->logger->debug("An error occurred while committing this UnitOfWork. Performing rollback...");
82 3
            $this->doRollback($ex);
83 3
            $this->stop();
84 3
            if (null === $this->outerUnitOfWork) {
85 3
                $this->performCleanup();
86 3
            }
87
88 3
            throw $ex;
89 23
        } finally {
90 28
            $this->logger->debug("Clearing resources of this Unit Of Work.");
91 28
            $this->clear();
92
        }
93 23
    }
94
95 44
    private function performCleanup()
96
    {
97 44
        foreach ($this->innerUnitsOfWork as $uow) {
98 7
            $uow->performCleanup();
99 44
        }
100 44
        $this->notifyListenersCleanup();
101 44
    }
102
103 46
    public function start()
104
    {
105 46
        $this->logger->debug("Starting Unit Of Work.");
106 46
        if ($this->isStarted) {
107
            throw new \RuntimeException("UnitOfWork is already started");
108
        }
109
110 46
        $this->doStart();
111 46
        if (CurrentUnitOfWork::isStarted()) {
112
            // we're nesting.
113 8
            $this->outerUnitOfWork = CurrentUnitOfWork::get();
114
115 8
            if ($this->outerUnitOfWork instanceof NestableUnitOfWork) {
116 7
                $this->outerUnitOfWork->registerInnerUnitOfWork($this);
117 7
            } else {
118 1
                $listener = new CommitOnOuterCommitTask(
119
                    function (UnitOfWorkInterface $uow) {
0 ignored issues
show
Unused Code introduced by
The parameter $uow 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...
120
                        $this->performInnerCommit();
121 1
                    },
122
                    function (UnitOfWorkInterface $uow) {
0 ignored issues
show
Unused Code introduced by
The parameter $uow 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...
123
                        $this->performCleanup();
124 1
                    },
125
                    function (UnitOfWorkInterface $uow, \Exception $ex = null) {
126
                        CurrentUnitOfWork::set($this);
127
                        $this->rollback($ex);
128
                    }
129 1
                );
130
131 1
                $this->outerUnitOfWork->registerListener($listener);
132
            }
133 8
        }
134 46
        $this->logger->debug("Registering Unit Of Work as CurrentUnitOfWork");
135 46
        CurrentUnitOfWork::set($this);
136 46
        $this->isStarted = true;
137 46
    }
138
139 25
    public function rollback(\Exception $ex = null)
140
    {
141 25
        if (null !== $ex) {
142 2
            $this->logger->debug(
143 2
                "Rollback requested for Unit Of Work due to exception. {exception} ",
144 2
                array('exception' => $ex->getMessage())
145 2
            );
146 2
        } else {
147 23
            $this->logger->debug("Rollback requested for Unit Of Work for unknown reason.");
148
        }
149
150
        try {
151 25
            if ($this->isStarted()) {
152 24
                foreach ($this->innerUnitsOfWork as $inner) {
153 2
                    CurrentUnitOfWork::set($inner);
154 2
                    $inner->rollback($ex);
155 24
                }
156 24
                $this->doRollback($ex);
157 24
            }
158 25
        } catch (\Exception $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
159
160
        }
161
162 25
        if (null === $this->outerUnitOfWork) {
163 23
            $this->performCleanup();
164 23
        }
165
166 25
        $this->clear();
167 25
        $this->stop();
168
169 25
        if ($ex) {
170 2
            throw $ex;
171
        }
172 23
    }
173
174 32
    public function isStarted()
175
    {
176 32
        return $this->isStarted;
177
    }
178
179
    /**
180
     * Performs logic required when starting this UnitOfWork instance.
181
     */
182
    protected abstract function doStart();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
183
184
    /**
185
     * Executes the logic required to commit this unit of work.
186
     */
187
    protected abstract function doCommit();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
188
189
    /**
190
     * Executes the logic required to rollback this unit of work.
191
     *
192
     * @param \Exception|null $ex
193
     */
194
    protected abstract function doRollback(\Exception $ex = null);
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
195
196 4
    private function performInnerCommit()
197
    {
198 4
        $exception = null;
199 4
        $this->logger->debug("Finalizing commit of inner Unit Of Work...");
200 4
        CurrentUnitOfWork::set($this);
201
202
        try {
203 4
            $this->doCommit();
204 4
        } catch (\RuntimeException $ex) {
205
            $this->doRollback($ex);
206
            $exception = $ex;
207
        }
208
209 4
        $this->clear();
210 4
        $this->stop();
211
212 4
        if (null !== $exception) {
213
            throw $exception;
214
        }
215 4
    }
216
217 28
    private function assertStarted()
218
    {
219 28
        if (!$this->isStarted) {
220
            throw new \RuntimeException("UnitOfWork is not started");
221
        }
222 28
    }
223
224 45
    private function stop()
225
    {
226 45
        $this->logger->debug("Stopping Unit Of Work");
227 45
        $this->isStarted = false;
228 45
    }
229
230 46
    private function clear()
231
    {
232 46
        CurrentUnitOfWork::clear($this);
233 46
    }
234
235
    /**
236
     * Commit all registered inner units of work. This should be invoked after events have been dispatched and before
237
     * any listeners are notified of the commit.
238
     */
239 23
    protected function commitInnerUnitOfWork()
240
    {
241 23
        foreach ($this->innerUnitsOfWork as $unitOfWork) {
242 5
            if ($unitOfWork->isStarted()) {
243 4
                $unitOfWork->performInnerCommit();
244 4
            }
245 23
        }
246 23
    }
247
248
    /**
249
     * @param NestableUnitOfWork $unitOfWork
250
     */
251 7
    private function registerInnerUnitOfWork(NestableUnitOfWork $unitOfWork)
252
    {
253 7
        $this->innerUnitsOfWork[] = $unitOfWork;
254 7
    }
255
256
    /**
257
     * Saves all registered aggregates by calling their respective callbacks.
258
     */
259
    protected abstract function saveAggregates();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
260
261
    protected abstract function notifyListenersPrepareCommit();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
262
263
    protected abstract function notifyListenersCleanup();
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
264
265
    /**
266
     * @param LoggerInterface $logger
267
     * @return null
268
     */
269
    public function setLogger(LoggerInterface $logger)
270
    {
271
        $this->logger = $logger;
272
    }
273
274
}
275
276
class CommitOnOuterCommitTask extends UnitOfWorkListenerAdapter
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
277
{
278
    /**
279
     * @var \Closure
280
     */
281
    private $commitClosure;
282
283
    /**
284
     * @var \Closure
285
     */
286
    private $cleanupClosure;
287
288
    /**
289
     * @var \Closure
290
     */
291
    private $rollbackClosure;
292
293
    /**
294
     * @param callable $commitClosure
295
     * @param callable $cleanupClosure
296
     * @param callable $rollbackClosure
297
     */
298 1
    function __construct(
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
299
        \Closure $commitClosure,
300
        \Closure $cleanupClosure,
301
        \Closure $rollbackClosure
302
    ) {
303 1
        $this->commitClosure = $commitClosure;
304 1
        $this->cleanupClosure = $cleanupClosure;
305 1
        $this->rollbackClosure = $rollbackClosure;
306 1
    }
307
308
    /**
309
     * @param UnitOfWorkInterface $unitOfWork
310
     */
311
    public function afterCommit(UnitOfWorkInterface $unitOfWork)
312
    {
313
        $cb = $this->commitClosure;
314
        $cb($unitOfWork);
315
    }
316
317
    /**
318
     * @param UnitOfWorkInterface $unitOfWork
319
     */
320
    public function onCleanup(UnitOfWorkInterface $unitOfWork)
321
    {
322
        $cb = $this->cleanupClosure;
323
        $cb($unitOfWork);
324
    }
325
326
    /**
327
     * @param UnitOfWorkInterface $unitOfWork
328
     * @param \Exception $failureCause
329
     */
330
    public function onRollback(
331
        UnitOfWorkInterface $unitOfWork,
332
        \Exception $failureCause = null
333
    ) {
334
        $cb = $this->rollbackClosure;
335
        $cb($unitOfWork, $failureCause);
336
    }
337
338
}
339