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

dg / dibi / 23992579605

05 Apr 2026 02:31AM UTC coverage: 77.838%. Remained the same
23992579605

push

github

dg
drivers: escape*() methods moved to Engine [WIP]

101 of 113 new or added lines in 8 files covered. (89.38%)

162 existing lines in 9 files now uncovered.

1721 of 2211 relevant lines covered (77.84%)

0.78 hits per line

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

77.87
/src/Dibi/Connection.php
1
<?php declare(strict_types=1);
1✔
2

3
/**
4
 * This file is part of the Dibi, smart database abstraction layer (https://dibi.nette.org)
5
 * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Dibi;
9

10
use JetBrains\PhpStorm\Language;
11
use Traversable;
12
use function array_key_exists, is_array, sprintf;
13
use const PHP_SAPI;
14

15

16
/**
17
 * Dibi connection.
18
 *
19
 * @property-read int $affectedRows
20
 * @property-read int $insertId
21
 */
22
class Connection
23
{
24
        private const Drivers = [
25
                'firebird' => Drivers\Ibase\Connection::class,
26
                'mysqli' => Drivers\MySQLi\Connection::class,
27
                'odbc' => Drivers\ODBC\Connection::class,
28
                'oracle' => Drivers\OCI8\Connection::class,
29
                'pdo' => Drivers\PDO\Connection::class,
30
                'postgre' => Drivers\PgSQL\Connection::class,
31
                'sqlite3' => Drivers\SQLite3\Connection::class,
32
                'sqlite' => Drivers\SQLite3\Connection::class,
33
                'sqlsrv' => Drivers\SQLSrv\Connection::class,
34
        ];
35

36

37
        /** @var array<callable(Event): void>  Occurs after query is executed */
38
        public array $onEvent = [];
39

40
        /** @var array<string, mixed> */
41
        private array $config;
42

43
        /** @var array<string, ?string>  Type constant => format string */
44
        private array $formats;
45
        private ?Drivers\Connection $driver = null;
46
        private Drivers\Engine $engine;
47
        private ?Translator $translator = null;
48

49
        /** @var array<string, callable(object): Expression | null> */
50
        private array $translators = [];
51
        private bool $sortTranslators = false;
52
        private HashMap $substitutes;
53
        private int $transactionDepth = 0;
54

55

56
        /**
57
         * Connection options: (see driver-specific options too)
58
         *   - lazy (bool) => if true, connection will be established only when required
59
         *   - result (array) => result set options
60
         *       - normalize => normalizes result fields (default: true)
61
         *       - formatDateTime => date-time format
62
         *           empty for decoding as Dibi\DateTime (default)
63
         *           "..." formatted according to given format, see https://www.php.net/manual/en/datetime.format.php
64
         *           "native" for leaving value as is
65
         *       - formatTimeInterval => time-interval format
66
         *           empty for decoding as DateInterval (default)
67
         *           "..." formatted according to given format, see https://www.php.net/manual/en/dateinterval.format.php
68
         *           "native" for leaving value as is
69
         *       - formatJson => json format
70
         *           "array" for decoding json as an array (default)
71
         *           "object" for decoding json as \stdClass
72
         *           "native" for leaving value as is
73
         *   - profiler (array)
74
         *       - run (bool) => enable profiler?
75
         *       - file => file to log
76
         *       - errorsOnly (bool) => log only errors
77
         *   - substitutes (array) => map of driver specific substitutes (under development)
78
         *   - onConnect (array) => list of SQL queries to execute (by Connection::query()) after connection is established
79
         * @param  array<string, mixed>  $config
80
         * @throws Exception
81
         */
82
        public function __construct(array $config, ?string $name = null)
1✔
83
        {
84
                Helpers::alias($config, 'username', 'user');
1✔
85
                Helpers::alias($config, 'password', 'pass');
1✔
86
                Helpers::alias($config, 'host', 'hostname');
1✔
87
                Helpers::alias($config, 'result|formatDate', 'resultDate');
1✔
88
                Helpers::alias($config, 'result|formatDateTime', 'resultDateTime');
1✔
89
                $config['driver'] ??= 'mysqli';
1✔
90
                $config['name'] = $name;
1✔
91
                $this->config = $config;
1✔
92

93
                $this->formats = [
1✔
94
                        Type::Date => $this->config['result']['formatDate'],
1✔
95
                        Type::DateTime => $this->config['result']['formatDateTime'],
1✔
96
                        Type::JSON => $this->config['result']['formatJson'] ?? 'array',
1✔
97
                        Type::TimeInterval => $this->config['result']['formatTimeInterval'] ?? null,
1✔
98
                ];
99

100
                // profiler
101
                if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
1✔
UNCOV
102
                        $filter = $config['profiler']['filter'] ?? Event::QUERY;
×
103
                        $errorsOnly = $config['profiler']['errorsOnly'] ?? false;
×
104
                        $this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
×
105
                }
106

107
                $this->substitutes = new HashMap(fn(string $expr) => ":$expr:");
1✔
108
                if (!empty($config['substitutes'])) {
1✔
UNCOV
109
                        foreach ($config['substitutes'] as $key => $value) {
×
110
                                $this->substitutes->$key = $value;
×
111
                        }
112
                }
113

114
                if (isset($config['onConnect']) && !is_array($config['onConnect'])) {
1✔
115
                        throw new \InvalidArgumentException("Configuration option 'onConnect' must be array.");
1✔
116
                }
117

118
                if (empty($config['lazy'])) {
1✔
119
                        $this->connect();
1✔
120
                }
121
        }
1✔
122

123

124
        /**
125
         * Automatically frees the resources allocated for this result set.
126
         */
127
        public function __destruct()
128
        {
129
                if ($this->driver && $this->driver->getResource()) {
1✔
130
                        $this->disconnect();
1✔
131
                }
132
        }
1✔
133

134

135
        /**
136
         * Connects to a database.
137
         */
138
        final public function connect(): void
139
        {
140
                if ($this->config['driver'] instanceof Drivers\Connection) {
1✔
141
                        $this->driver = $this->config['driver'];
1✔
142
                        $this->translator = new Translator($this);
1✔
143
                        return;
1✔
144

145
                } elseif (is_subclass_of($this->config['driver'], Drivers\Connection::class)) {
1✔
UNCOV
146
                        $class = $this->config['driver'];
×
147

148
                } else {
149
                        $class = self::Drivers[strtolower($this->config['driver'])] ?? throw new Exception("Unknown driver '{$this->config['driver']}'.");
1✔
150
                        if (!class_exists($class)) {
1✔
UNCOV
151
                                throw new Exception("Unable to create instance of Dibi driver '$class'.");
×
152
                        }
153
                }
154

155
                $event = $this->onEvent ? new Event($this, Event::CONNECT) : null;
1✔
156
                try {
157
                        $this->driver = new $class($this->config);
1✔
158
                        $this->translator = new Translator($this);
1✔
159

160
                        if ($event) {
1✔
UNCOV
161
                                $this->onEvent($event->done());
×
162
                        }
163

164
                        if (isset($this->config['onConnect'])) {
1✔
165
                                foreach ($this->config['onConnect'] as $sql) {
1✔
166
                                        $this->query($sql);
1✔
167
                                }
168
                        }
169
                } catch (DriverException $e) {
1✔
170
                        if ($event) {
1✔
UNCOV
171
                                $this->onEvent($event->done($e));
×
172
                        }
173

174
                        throw $e;
1✔
175
                }
176
        }
1✔
177

178

179
        /**
180
         * Disconnects from a database.
181
         */
182
        final public function disconnect(): void
183
        {
184
                if ($this->driver) {
1✔
185
                        $this->driver->disconnect();
1✔
186
                        $this->driver = $this->translator = null;
1✔
187
                }
188
        }
1✔
189

190

191
        /**
192
         * Returns true when connection was established.
193
         */
194
        final public function isConnected(): bool
195
        {
196
                return (bool) $this->driver;
1✔
197
        }
198

199

200
        /**
201
         * Returns configuration variable. If no $key is passed, returns the entire array.
202
         * @see self::__construct
203
         */
204
        final public function getConfig(?string $key = null, mixed $default = null): mixed
1✔
205
        {
206
                return $key === null
1✔
UNCOV
207
                        ? $this->config
×
208
                        : ($this->config[$key] ?? $default);
1✔
209
        }
210

211

212
        /**
213
         * Returns the driver and connects to a database in lazy mode.
214
         */
215
        final public function getDriver(): Drivers\Connection
216
        {
217
                if (!$this->driver) {
1✔
UNCOV
218
                        $this->connect();
×
219
                        assert($this->driver !== null);
220
                }
221

222
                return $this->driver;
1✔
223
        }
224

225

226
        /**
227
         * Generates (translates) and executes SQL query.
228
         * @throws Exception
229
         */
230
        final public function query(#[Language('GenericSQL')] mixed ...$args): Result
1✔
231
        {
232
                return $this->nativeQuery($this->translate(...$args));
1✔
233
        }
234

235

236
        /**
237
         * Generates SQL query.
238
         * @throws Exception
239
         */
240
        final public function translate(#[Language('GenericSQL')] mixed ...$args): string
1✔
241
        {
242
                if (!$this->driver) {
1✔
243
                        $this->connect();
1✔
244
                }
245

246
                assert($this->translator !== null);
247
                return (clone $this->translator)->translate($args);
1✔
248
        }
249

250

251
        /**
252
         * Generates and prints SQL query.
253
         */
254
        final public function test(#[Language('GenericSQL')] mixed ...$args): bool
255
        {
256
                try {
UNCOV
257
                        Helpers::dump($this->translate(...$args));
×
258
                        return true;
×
259

UNCOV
260
                } catch (Exception $e) {
×
261
                        if ($e->getSql()) {
×
262
                                Helpers::dump($e->getSql());
×
263
                        } else {
UNCOV
264
                                echo $e::class . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
×
265
                        }
266

UNCOV
267
                        return false;
×
268
                }
269
        }
270

271

272
        /**
273
         * Generates (translates) and returns SQL query as DataSource.
274
         * @throws Exception
275
         */
276
        final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
1✔
277
        {
278
                return new DataSource($this->translate(...$args), $this);
1✔
279
        }
280

281

282
        /**
283
         * Executes the SQL query.
284
         * @throws Exception
285
         */
286
        final public function nativeQuery(#[Language('SQL')] string $sql): Result
1✔
287
        {
288
                if (!$this->driver) {
1✔
UNCOV
289
                        $this->connect();
×
290
                        assert($this->driver !== null);
291
                }
292

293
                \dibi::$sql = $sql;
1✔
294
                $event = $this->onEvent ? new Event($this, Event::QUERY, $sql) : null;
1✔
295
                try {
296
                        $res = $this->driver->query($sql);
1✔
297

298
                } catch (DriverException $e) {
1✔
299
                        if ($event) {
1✔
UNCOV
300
                                $this->onEvent($event->done($e));
×
301
                        }
302

303
                        throw $e;
1✔
304
                }
305

306
                $res = $this->createResultSet($res ?? new Drivers\Dummy\Result(max(0, $this->driver->getAffectedRows())));
1✔
307
                if ($event) {
1✔
UNCOV
308
                        $this->onEvent($event->done($res));
×
309
                }
310

311
                return $res;
1✔
312
        }
313

314

315
        /**
316
         * Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
317
         * @throws Exception
318
         */
319
        public function getAffectedRows(): int
320
        {
321
                if (!$this->driver) {
1✔
UNCOV
322
                        $this->connect();
×
323
                        assert($this->driver !== null);
324
                }
325

326
                $rows = $this->driver->getAffectedRows();
1✔
327
                if ($rows === null || $rows < 0) {
1✔
UNCOV
328
                        throw new Exception('Cannot retrieve number of affected rows.');
×
329
                }
330

331
                return $rows;
1✔
332
        }
333

334

335
        /**
336
         * Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
337
         * @throws Exception
338
         */
339
        public function getInsertId(?string $sequence = null): int
1✔
340
        {
341
                if (!$this->driver) {
1✔
UNCOV
342
                        $this->connect();
×
343
                        assert($this->driver !== null);
344
                }
345

346
                $id = $this->driver->getInsertId($sequence);
1✔
347
                if ($id === null) {
1✔
UNCOV
348
                        throw new Exception('Cannot retrieve last generated ID.');
×
349
                }
350

351
                return $id;
1✔
352
        }
353

354

355
        /**
356
         * Begins a transaction (if supported).
357
         */
358
        public function begin(?string $savepoint = null): void
1✔
359
        {
360
                if ($this->transactionDepth !== 0) {
1✔
361
                        throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
1✔
362
                }
363

364
                if (!$this->driver) {
1✔
UNCOV
365
                        $this->connect();
×
366
                        assert($this->driver !== null);
367
                }
368

369
                $event = $this->onEvent ? new Event($this, Event::BEGIN, $savepoint) : null;
1✔
370
                try {
371
                        $this->driver->begin($savepoint);
1✔
372
                        if ($event) {
1✔
373
                                $this->onEvent($event->done());
1✔
374
                        }
UNCOV
375
                } catch (DriverException $e) {
×
376
                        if ($event) {
×
377
                                $this->onEvent($event->done($e));
×
378
                        }
379

UNCOV
380
                        throw $e;
×
381
                }
382
        }
1✔
383

384

385
        /**
386
         * Commits statements in a transaction.
387
         */
388
        public function commit(?string $savepoint = null): void
1✔
389
        {
390
                if ($this->transactionDepth !== 0) {
1✔
391
                        throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
1✔
392
                }
393

394
                if (!$this->driver) {
1✔
UNCOV
395
                        $this->connect();
×
396
                        assert($this->driver !== null);
397
                }
398

399
                $event = $this->onEvent ? new Event($this, Event::COMMIT, $savepoint) : null;
1✔
400
                try {
401
                        $this->driver->commit($savepoint);
1✔
402
                        if ($event) {
1✔
403
                                $this->onEvent($event->done());
1✔
404
                        }
UNCOV
405
                } catch (DriverException $e) {
×
406
                        if ($event) {
×
407
                                $this->onEvent($event->done($e));
×
408
                        }
409

UNCOV
410
                        throw $e;
×
411
                }
412
        }
1✔
413

414

415
        /**
416
         * Rollback changes in a transaction.
417
         */
418
        public function rollback(?string $savepoint = null): void
1✔
419
        {
420
                if ($this->transactionDepth !== 0) {
1✔
421
                        throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
1✔
422
                }
423

424
                if (!$this->driver) {
1✔
UNCOV
425
                        $this->connect();
×
426
                        assert($this->driver !== null);
427
                }
428

429
                $event = $this->onEvent ? new Event($this, Event::ROLLBACK, $savepoint) : null;
1✔
430
                try {
431
                        $this->driver->rollback($savepoint);
1✔
432
                        if ($event) {
1✔
433
                                $this->onEvent($event->done());
1✔
434
                        }
UNCOV
435
                } catch (DriverException $e) {
×
436
                        if ($event) {
×
437
                                $this->onEvent($event->done($e));
×
438
                        }
439

UNCOV
440
                        throw $e;
×
441
                }
442
        }
1✔
443

444

445
        /**
446
         * @template T
447
         * @param  callable(self): T  $callback
448
         * @return T
449
         */
450
        public function transaction(callable $callback): mixed
1✔
451
        {
452
                if ($this->transactionDepth === 0) {
1✔
453
                        $this->begin();
1✔
454
                }
455

456
                $this->transactionDepth++;
1✔
457
                try {
458
                        $res = $callback($this);
1✔
459
                } catch (\Throwable $e) {
1✔
460
                        $this->transactionDepth--;
1✔
461
                        if ($this->transactionDepth === 0) {
1✔
462
                                $this->rollback();
1✔
463
                        }
464

465
                        throw $e;
1✔
466
                }
467

468
                $this->transactionDepth--;
1✔
469
                if ($this->transactionDepth === 0) {
1✔
470
                        $this->commit();
1✔
471
                }
472

473
                return $res;
1✔
474
        }
475

476

477
        /**
478
         * Result set factory.
479
         */
480
        public function createResultSet(Drivers\Result $resultDriver): Result
1✔
481
        {
482
                return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
1✔
483
                        ->setFormats($this->formats);
1✔
484
        }
485

486

487
        /********************* fluent SQL builders ****************d*g**/
488

489

490
        public function command(): Fluent
491
        {
492
                return new Fluent($this);
1✔
493
        }
494

495

496
        public function select(mixed ...$args): Fluent
1✔
497
        {
498
                return $this->command()->select(...$args);
1✔
499
        }
500

501

502
        /**
503
         * @param  string|string[]  $table
504
         * @param  iterable<string, mixed>  $args
505
         */
506
        public function update($table, iterable $args): Fluent
1✔
507
        {
508
                return $this->command()->update('%n', $table)->set($args);
1✔
509
        }
510

511

512
        /** @param  iterable<string, mixed>  $args */
513
        public function insert(string $table, iterable $args): Fluent
1✔
514
        {
515
                if ($args instanceof Traversable) {
1✔
UNCOV
516
                        $args = iterator_to_array($args);
×
517
                }
518

519
                return $this->command()->insert()
1✔
520
                        ->into('%n', $table, '(%n)', array_keys($args))->values('%l', $args);
1✔
521
        }
522

523

524
        public function delete(string $table): Fluent
1✔
525
        {
526
                return $this->command()->delete()->from('%n', $table);
1✔
527
        }
528

529

530
        /********************* substitutions ****************d*g**/
531

532

533
        /**
534
         * Returns substitution hashmap.
535
         */
536
        public function getSubstitutes(): HashMap
537
        {
538
                return $this->substitutes;
1✔
539
        }
540

541

542
        /**
543
         * Provides substitution.
544
         */
545
        public function substitute(string $value): string
1✔
546
        {
547
                return str_contains($value, ':')
1✔
548
                        ? preg_replace_callback('#:([^:\s]*):#', fn(array $m) => $this->substitutes->{$m[1]}, $value)
1✔
549
                        : $value;
1✔
550
        }
551

552

553
        /********************* value objects translation ****************d*g**/
554

555

556
        /**
557
         * @param  callable(object): Expression  $translator
558
         */
559
        public function setObjectTranslator(callable $translator): void
1✔
560
        {
561
                if (!$translator instanceof \Closure) {
1✔
UNCOV
562
                        $translator = \Closure::fromCallable($translator);
×
563
                }
564

565
                $param = (new \ReflectionFunction($translator))->getParameters()[0] ?? null;
1✔
566
                $type = $param?->getType();
1✔
567
                $types = match (true) {
1✔
568
                        $type instanceof \ReflectionNamedType => [$type],
1✔
569
                        $type instanceof \ReflectionUnionType => $type->getTypes(),
1✔
570
                        default => throw new Exception('Object translator must have exactly one parameter with class typehint.'),
1✔
571
                };
572

573
                foreach ($types as $type) {
1✔
574
                        if (!$type instanceof \ReflectionNamedType) {
1✔
UNCOV
575
                                continue;
×
576
                        }
577

578
                        if ($type->isBuiltin() || $type->allowsNull()) {
1✔
579
                                throw new Exception("Object translator must have exactly one parameter with non-nullable class typehint, got '$type'.");
1✔
580
                        }
581
                        $this->translators[$type->getName()] = $translator;
1✔
582
                }
583
                $this->sortTranslators = true;
1✔
584
        }
1✔
585

586

587
        public function translateObject(object $object): ?Expression
1✔
588
        {
589
                if ($this->sortTranslators) {
1✔
590
                        $this->translators = array_filter($this->translators);
1✔
591
                        uksort($this->translators, fn($a, $b) => is_subclass_of($a, $b) ? -1 : 1);
1✔
592
                        $this->sortTranslators = false;
1✔
593
                }
594

595
                if (!array_key_exists($object::class, $this->translators)) {
1✔
596
                        $translator = null;
1✔
597
                        foreach ($this->translators as $class => $t) {
1✔
598
                                if ($object instanceof $class) {
1✔
599
                                        $translator = $t;
1✔
600
                                        break;
1✔
601
                                }
602
                        }
603
                        $this->translators[$object::class] = $translator;
1✔
604
                }
605

606
                $translator = $this->translators[$object::class];
1✔
607
                if ($translator === null) {
1✔
608
                        return null;
1✔
609
                }
610

611
                $result = $translator($object);
1✔
612
                if (!$result instanceof Expression) {
1✔
613
                        throw new Exception(sprintf(
1✔
614
                                "Object translator for class '%s' returned '%s' but %s expected.",
1✔
615
                                $object::class,
1✔
616
                                get_debug_type($result),
1✔
617
                                Expression::class,
1✔
618
                        ));
619
                }
620

621
                return $result;
1✔
622
        }
623

624

625
        /********************* shortcuts ****************d*g**/
626

627

628
        /**
629
         * Executes SQL query and fetch result - shortcut for query() & fetch().
630
         * @throws Exception
631
         */
632
        public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
1✔
633
        {
634
                return $this->query($args)->fetch();
1✔
635
        }
636

637

638
        /**
639
         * Executes SQL query and fetch results - shortcut for query() & fetchAll().
640
         * @return list<Row|mixed[]>
641
         * @throws Exception
642
         */
643
        public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
644
        {
UNCOV
645
                return $this->query($args)->fetchAll();
×
646
        }
647

648

649
        /**
650
         * Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
651
         * @throws Exception
652
         */
653
        public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
1✔
654
        {
655
                return $this->query($args)->fetchSingle();
1✔
656
        }
657

658

659
        /**
660
         * Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
661
         * @return mixed[]
662
         * @throws Exception
663
         */
664
        public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
665
        {
UNCOV
666
                return $this->query($args)->fetchPairs();
×
667
        }
668

669

670
        public static function literal(string $value): Literal
671
        {
UNCOV
672
                return new Literal($value);
×
673
        }
674

675

676
        public static function expression(mixed ...$args): Expression
677
        {
UNCOV
678
                return new Expression(...$args);
×
679
        }
680

681

682
        /********************* misc ****************d*g**/
683

684

685
        /**
686
         * Import SQL dump from file.
687
         * @param  ?(callable(int, ?float): void)  $onProgress
688
         * @return int  count of sql commands
689
         */
690
        public function loadFile(string $file, ?callable $onProgress = null): int
1✔
691
        {
692
                return Helpers::loadFromFile($this, $file, $onProgress);
1✔
693
        }
694

695

696
        public function getDatabaseEngine(): Drivers\Engine
697
        {
698
                if (!$this->driver) {
1✔
NEW
699
                        $this->connect();
×
700
                }
701

702
                assert($this->driver !== null);
703
                return $this->engine ??= $this->driver->getReflector();
1✔
704
        }
705

706

707
        /**
708
         * Gets a information about the current database.
709
         */
710
        public function getDatabaseInfo(): Reflection\Database
711
        {
712
                if (!$this->driver) {
1✔
UNCOV
713
                        $this->connect();
×
714
                        assert($this->driver !== null);
715
                }
716

717
                return new Reflection\Database($this->getDatabaseEngine(), $this->config['database'] ?? null);
1✔
718
        }
719

720

721
        /**
722
         * Prevents unserialization.
723
         * @param  array<string, mixed>  $_
724
         */
725
        public function __unserialize(array $_): never
726
        {
UNCOV
727
                throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
×
UNCOV
728
        }
×
729

730

731
        /**
732
         * Prevents serialization.
733
         * @return array<string, mixed>
734
         */
735
        public function __serialize(): array
736
        {
UNCOV
737
                throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
×
738
        }
739

740

741
        protected function onEvent(Event $arg): void
742
        {
UNCOV
743
                foreach ($this->onEvent as $handler) {
×
UNCOV
744
                        $handler($arg);
×
745
                }
746
        }
747
}
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