• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mpyw / laravel-database-advisory-lock / 27455202536

13 Jun 2026 03:27AM UTC coverage: 99.401% (-0.6%) from 100.0%
27455202536

Pull #16

github

mpyw
Fix QueryException tail boundary: connection details land in Laravel 12, not 13

The Host/Port/Database segments were added to QueryException messages in
Laravel 12 (verified: L11 omits them, L12/L13 include them), so the helper
must switch to the detailed format at `>= 12.x-dev`, not `>= 13.x-dev`.
Verified phpstan + the Postgres recovery tests on Laravel 11, 12 and 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull Request #16: [Claude] Support the dedicated `mariadb` driver (closes #15)

2 of 3 new or added lines in 2 files covered. (66.67%)

166 of 167 relevant lines covered (99.4%)

133.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.45
/src/TransactionEventHub.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Mpyw\LaravelDatabaseAdvisoryLock;
6

7
use Illuminate\Contracts\Events\Dispatcher;
8
use Illuminate\Database\Events\TransactionCommitted;
9
use Illuminate\Database\Events\TransactionRolledBack;
10
use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\TransactionTerminationListener;
11
use Throwable;
12
use WeakMap;
13

14
use function spl_object_hash;
15

16
/**
17
 * class TransactionEventHub
18
 *
19
 * Associate an event dispatcher on a connection with a listener of session-level locks.
20
 * WeakMap prevents memory leaks.
21
 */
22
final class TransactionEventHub
23
{
24
    /**
25
     * @var WeakMap<Dispatcher, array<string, TransactionTerminationListener>>
26
     */
27
    private WeakMap $dispatchersAndListeners;
28

29
    /**
30
     * @var null|callable(): self
31
     */
32
    private static $resolver;
33

34
    /**
35
     * Set a singleton instance resolver.
36
     *
37
     * @param null|callable(): self $resolver
38
     */
39
    public static function setResolver(?callable $resolver): void
40
    {
41
        self::$resolver = $resolver;
528✔
42
    }
43

44
    /**
45
     * Create or retrieve a singleton instance through resolver.
46
     */
47
    public static function resolve(): ?self
48
    {
49
        return self::$resolver ? (self::$resolver)() : null;
154✔
50
    }
51

52
    public function __construct()
53
    {
54
        $this->dispatchersAndListeners = new WeakMap();
528✔
55
    }
56

57
    /**
58
     * Register self::onTransactionTerminated() as a listener once per connection.
59
     */
60
    public function initializeWithDispatcher(?Dispatcher $dispatcher): void
61
    {
62
        // A connection is not guaranteed to have an event dispatcher, and the
63
        // return type of Connection::getEventDispatcher() differs across Laravel
64
        // versions (nullable on some, non-nullable on others), so accept null here.
65
        if ($dispatcher === null) {
154✔
NEW
66
            return;
×
67
        }
68

69
        if (!isset($this->dispatchersAndListeners[$dispatcher])) {
154✔
70
            $dispatcher->listen(
154✔
71
                [TransactionCommitted::class, TransactionRolledBack::class],
154✔
72
                [self::class, 'onTransactionTerminated'],
154✔
73
            );
154✔
74
        }
75

76
        $this->dispatchersAndListeners[$dispatcher] ??= [];
154✔
77
    }
78

79
    /**
80
     * Register underlying user listener per connection.
81
     * Listeners registered here are invoked only once.
82
     */
83
    public function registerOnceListener(TransactionTerminationListener $listener): void
84
    {
85
        foreach ($this->dispatchersAndListeners as $dispatcher => $_) {
22✔
86
            $this->dispatchersAndListeners[$dispatcher][spl_object_hash($listener)] = $listener;
22✔
87
        }
88
    }
89

90
    /**
91
     * Fire on events.
92
     */
93
    public function onTransactionTerminated(TransactionCommitted|TransactionRolledBack $event): void
94
    {
95
        /** @var array<string, array<string, TransactionTerminationListener>> $savedListenerGroups */
96
        $savedListenerGroups = [];
55✔
97

98
        // First, save all listeners.
99
        foreach ($this->dispatchersAndListeners as $dispatcher => $listeners) {
55✔
100
            foreach ($listeners as $listener) {
55✔
101
                $savedListenerGroups[spl_object_hash($dispatcher)][spl_object_hash($listener)] = $listener;
22✔
102
            }
103
        }
104

105
        // Next, remove listeners in advance.
106
        foreach ($this->dispatchersAndListeners as $dispatcher => $_) {
55✔
107
            $this->dispatchersAndListeners[$dispatcher] = [];
55✔
108
        }
109

110
        // Finally, run the saved listeners.
111
        // It does not matter if new listeners are registered again during the execution.
112
        foreach ($savedListenerGroups as $savedListeners) {
55✔
113
            foreach ($savedListeners as $listener) {
22✔
114
                try {
115
                    $listener->onTransactionTerminated($event);
22✔
116
                    // @codeCoverageIgnoreStart
117
                } catch (Throwable) {
118
                }
119
                // @codeCoverageIgnoreEnd
120
            }
121
        }
122
    }
123
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc