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

j-schumann / symfony-addons / 23250665064

18 Mar 2026 02:47PM UTC coverage: 53.789% (-0.1%) from 53.904%
23250665064

push

github

j-schumann
fix: tests on mssql

0 of 3 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

504 of 937 relevant lines covered (53.79%)

3.46 hits per line

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

60.64
/src/PHPUnit/RefreshDatabaseTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Vrok\SymfonyAddons\PHPUnit;
6

7
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
8
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
9
use Doctrine\DBAL\DriverManager;
10
use Doctrine\DBAL\Platforms\MariaDBPlatform;
11
use Doctrine\DBAL\Platforms\MySQLPlatform;
12
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
13
use Doctrine\DBAL\Platforms\SQLServerPlatform;
14
use Doctrine\DBAL\Schema\SQLiteSchemaManager;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Tools\SchemaTool;
17
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
18
use Symfony\Component\DependencyInjection\ContainerInterface;
19
use Symfony\Component\HttpKernel\KernelInterface;
20

21
/**
22
 * Idea from DoctrineTestBundle & hautelook/AliceBundle:
23
 * We want to force the test DB to have the current schema and load all test
24
 * fixtures (group=test) so the DB is in a known state for each test (meaning
25
 * each time the kernel is booted).
26
 *
27
 * static::$fixtureGroups can be customized in setUpBeforeClass()
28
 *
29
 * The cleanup method for the database can be overwritten by setting the ENV
30
 * DB_CLEANUP_METHOD (e.g. in phpunit.xml.dist).
31
 *
32
 * "purge" will update the DB schema once and afterward only purges
33
 *  all tables, may require Vrok\DoctrineAddons\DBAL\Platforms\{Mariadb|PostgreSQL}TestPlatform
34
 *  to disable foreign keys / cascade purge or reset identities before running.
35
 *
36
 * "dropSchema" will drop all tables (and indices) and recreate them before each
37
 * test, use this for databases that do not support disabling foreign keys like
38
 * MS SqlServer. From experience, is also much faster than purge, at least on
39
 * Postgres.
40
 *
41
 * "dropDatabase" will drop the entire database and recreate it before each test
42
 * run, this should be even faster than "dropSchema".
43
 */
44
trait RefreshDatabaseTrait
45
{
46
    /**
47
     * @var array fixture group(s) to apply
48
     */
49
    protected static array $fixtureGroups = ['test'];
50

51
    /**
52
     * @var array|null fixture cache
53
     */
54
    protected static ?array $fixtures = null;
55

56
    /**
57
     * @var bool Flag whether the db setup is done (db exists, schema is up to
58
     *           date)
59
     */
60
    protected static bool $setupComplete = false;
61

62
    /**
63
     * Called on each test that calls bootKernel() or uses createClient().
64
     */
65
    protected static function bootKernel(array $options = []): KernelInterface
66
    {
67
        static::ensureKernelTestCase();
16✔
68

69
        $kernel = parent::bootKernel($options);
16✔
70
        $container = static::getContainer();
16✔
71
        $entityManager = $container->get('doctrine')->getManager();
16✔
72
        $executor = static::getExecutor($entityManager);
16✔
73

74
        switch ($_ENV['DB_CLEANUP_METHOD'] ?? 'purge') {
16✔
75
            case 'dropDatabase':
16✔
76
                static::recreateDatabase($entityManager, true);
1✔
77
                static::updateSchema($entityManager);
1✔
78
                break;
1✔
79

80
            case 'dropSchema':
15✔
81
                if (!static::$setupComplete) {
1✔
82
                    static::recreateDatabase($entityManager);
×
83
                    static::$setupComplete = true;
×
84
                }
85

86
                static::updateSchema($entityManager, true);
1✔
87
                break;
1✔
88

89
            case 'purge':
14✔
90
            default:
91
                // only required on the first test: make sure the db exists and
92
                // the schema is up to date
93
                if (!static::$setupComplete) {
14✔
94
                    static::recreateDatabase($entityManager);
2✔
95
                    static::updateSchema($entityManager);
2✔
96
                    static::$setupComplete = true;
2✔
97
                }
98

99
                $connection = $executor->getObjectManager()->getConnection();
14✔
100
                $platform = $connection->getDatabasePlatform();
14✔
101

102
                // In MySQL/MariaDB we need to disable foreign key checks, as
103
                // the automatic table ordering does not help us when we have
104
                // self-referencing tables.
105
                if ($platform instanceof MySQLPlatform || $platform instanceof MariaDBPlatform) {
14✔
106
                    $connection->executeStatement('SET FOREIGN_KEY_CHECKS = 0');
×
107
                }
108

109
                // In SQLServer, TRUNCATE does not work with foreign keys, also
110
                // using "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
111
                // does not help here. So we switch to simple delete, but this
112
                // requires us to manually reset auto-increments afterward.
113
                if ($platform instanceof SQLServerPlatform) {
14✔
114
                    $executor->getPurger()->setPurgeMode(ORMPurger::PURGE_MODE_DELETE);
×
115
                }
116

117
                // Purge even when no fixtures are defined, e.g., for tests that
118
                // require an empty database, like import tests.
119
                // Fix for PHP8: purge separately from inserting the fixtures,
120
                // as execute() would wrap the TRUNCATE in a transaction which
121
                // MySQL auto-commits when DDL queries are executed, which
122
                // throws an exception in the entityManager ("There is no active
123
                // transaction", @see https://github.com/doctrine/migrations/issues/1104)
124
                // because he does not check if a transaction is still open
125
                // before calling commit().
126
                $executor->purge();
14✔
127

128
                // See above, we have to manually reset identities on SQLServer
129
                if ($platform instanceof SQLServerPlatform) {
14✔
130
                    $executor->getObjectManager()->getConnection()->executeStatement(
×
131
                        "EXEC sp_MSforeachtable 'IF OBJECTPROPERTY(OBJECT_ID(''?''), ''TableHasIdentity'') = 1 DBCC CHECKIDENT (''?'', RESEED, 0)'"
×
132
                    );
×
133
                }
134

135
                break;
14✔
136
        }
137

138
        // now load any fixtures configured for "test" (or overwritten groups)
139
        $fixtures = static::getFixtures($container);
16✔
140
        if ([] !== $fixtures) {
16✔
141
            $executor->execute($fixtures, true);
×
142
        }
143

144
        return $kernel;
16✔
145
    }
146

147
    protected static function ensureKernelTestCase(): void
148
    {
149
        if (!is_a(static::class, KernelTestCase::class, true)) {
16✔
150
            throw new \LogicException(\sprintf('The test class must extend "%s" to use "%s".', KernelTestCase::class, static::class));
×
151
        }
152
    }
153

154
    /**
155
     * (Drops and re-) creates the (test) database if it does not exist.
156
     * This code tries to duplicate the behavior of the doctrine:database:drop
157
     * / doctrine:schema:create commands in the DoctrineBundle.
158
     *
159
     * @param bool $drop If true, the method will delete an existing database
160
     *                   before recreating it. If false, the database will only
161
     *                   be created if it doesn't exist.
162
     */
163
    protected static function recreateDatabase(
164
        EntityManagerInterface $em,
165
        bool $drop = false,
166
    ): void {
167
        $connection = $em->getConnection();
3✔
168
        $params = $params['primary'] ?? $connection->getParams();
3✔
169

170
        // this name will already contain the dbname_suffix (and the TEST_TOKEN)
171
        // if any is configured
172
        $dbName = $params['path'] ?? $params['dbname'] ?? false;
3✔
173
        if (!$dbName) {
3✔
174
            throw new \RuntimeException("Connection does not contain a 'dbname' or 'path' parameter, don't know how to proceed, aborting.");
×
175
        }
176

177
        unset($params['dbname'], $params['path']);
3✔
178
        if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
3✔
179
            $params['dbname'] = $params['default_dbname'] ?? 'postgres';
×
180
        }
181

182
        $tempConnection = DriverManager::getConnection($params, $connection->getConfiguration());
3✔
183
        $schemaManager = $tempConnection->createSchemaManager();
3✔
184

185
        // SQLite does not support checking for existing / dropping / creating
186
        // databases via Doctrine -> special handling here
187
        if ($schemaManager instanceof SQLiteSchemaManager) {
3✔
188
            if ($drop && file_exists($dbName)) {
3✔
189
                unlink($dbName);
1✔
190
            }
191

192
            // the database file will be automatically created on first use,
193
            // no need to create it here
194
            return;
3✔
195
        }
196

197
        $dbExists = self::databaseExists($em, $dbName);
×
198
        if ($drop && $dbExists) {
×
199
            // close the current connection in the em, it would be invalid
200
            // anyway after the drop
201
            $connection->close();
×
202

203
            // For Postgres, closing the old connection is not
204
            // enough to prevent: 'ERROR: database "db_test" is being accessed by other users'
205
            if ($tempConnection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
×
206
                $tempConnection->executeStatement(
×
207
                    'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = ? AND pid <> pg_backend_pid()',
×
208
                    [$dbName]
×
209
                );
×
210
            }
211

NEW
212
            if ($tempConnection->getDatabasePlatform() instanceof SQLServerPlatform) {
×
NEW
213
                $tempConnection->executeStatement('USE master');
×
214
            }
215

UNCOV
216
            $schemaManager->dropDatabase($dbName);
×
217

218
            $dbExists = false;
×
219
        }
220

221
        // Create the database only if it doesn't already exist. Skip for SQLite
222
        // as it creates database files automatically and this call would throw
223
        // an exception.
224
        if (!$dbExists && !$schemaManager instanceof SQLiteSchemaManager) {
×
225
            $schemaManager->createDatabase($dbName);
×
226
        }
227

228
        $tempConnection->close();
×
229
    }
230

231
    /**
232
     * Brings the db schema to the newest version.
233
     *
234
     * @param bool $drop If true, the method will drop the current schema, e.g.
235
     *                   to reset all data, as dropping & recreating the schema
236
     *                   will often be faster than truncating all tables.
237
     */
238
    protected static function updateSchema(
239
        EntityManagerInterface $em,
240
        bool $drop = false,
241
    ): void {
242
        $metadatas = $em->getMetadataFactory()->getAllMetadata();
4✔
243
        if ([] === $metadatas) {
4✔
244
            return;
×
245
        }
246

247
        $schemaTool = new SchemaTool($em);
4✔
248

249
        if ($drop) {
4✔
250
            // The method name is misleading; it only drops the elements within
251
            // the database, not the db itself...
252
            $schemaTool->dropDatabase();
1✔
253
        }
254

255
        $schemaTool->updateSchema($metadatas);
4✔
256
    }
257

258
    /**
259
     * Use a static fixture cache as we need them before each test.
260
     */
261
    protected static function getFixtures(ContainerInterface $container): array
262
    {
263
        if ([] === static::$fixtureGroups) {
16✔
264
            // the fixture loader returns all possible fixtures if called
265
            // with an empty array -> catch here
266
            return [];
×
267
        }
268

269
        if (\is_array(static::$fixtures)) {
16✔
270
            return static::$fixtures;
15✔
271
        }
272

273
        $fixturesLoader = $container->get('doctrine.fixtures.loader');
2✔
274
        static::$fixtures = $fixturesLoader->getFixtures(static::$fixtureGroups);
2✔
275

276
        return static::$fixtures;
2✔
277
    }
278

279
    /**
280
     * Returns a new executor instance, we need it before each test execution.
281
     */
282
    protected static function getExecutor(EntityManagerInterface $em): ORMExecutor
283
    {
284
        $purger = new ORMPurger($em);
16✔
285
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
16✔
286

287
        // don't use a static Executor, it contains the EM which could be closed
288
        // through (expected) exceptions and would not work
289
        return new ORMExecutor($em, $purger);
16✔
290
    }
291

292
    /**
293
     * Returns the list of databases the current connection sees.
294
     * Must be called with the "old" entityManager, that has the database name
295
     * set in its connection, or we will receive "A database is required for the
296
     * method: Doctrine\DBAL\Platforms\MySQL\MySQLMetadataProvider" on MariaDB
297
     * and MySQL.
298
     */
299
    private static function databaseExists(EntityManagerInterface $em, string $dbName): bool
300
    {
301
        // We cannot use schemaManager->introspectDatabaseNames, as this would
302
        // require the $tempConnection to have a DB name set for MySQL/MariaDB.
303
        // But we can't set one, as it would fail when the database indeed does
304
        // not exist, that's why we created the $tempConnection in the first
305
        // place.
306
        // All was working well when schemaManager->listDatabases was not yet
307
        // deprecated...
308
        $connection = $em->getConnection();
×
309

310
        // So instead, we create the provider manually with the old connection,
311
        // in the hope that this works even with the EM closed from previous
312
        // test run exceptions...
313
        $metaProvider = $connection->getDatabasePlatform()
×
314
            ->createMetadataProvider($connection);
×
315

316
        $dbNames = array_map(
×
NEW
317
            static fn ($n) => $n->getDatabaseName(),
×
318
            iterator_to_array($metaProvider->getAllDatabaseNames())
×
319
        );
×
320

321
        return \in_array($dbName, $dbNames, true);
×
322
    }
323

324
    protected static function fixtureCleanup(): void
325
    {
326
        static::$fixtures = null;
×
327
    }
328
}
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