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

j-schumann / symfony-addons / 23239593086

18 Mar 2026 10:11AM UTC coverage: 53.813% (-0.1%) from 53.929%
23239593086

push

github

j-schumann
fix: tests on mssql

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

501 of 931 relevant lines covered (53.81%)

3.44 hits per line

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

61.36
/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\Exception\DatabaseRequired;
11
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
12
use Doctrine\DBAL\Platforms\SQLServerPlatform;
13
use Doctrine\DBAL\Schema\SQLiteSchemaManager;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\Tools\SchemaTool;
16
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\HttpKernel\KernelInterface;
19

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

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

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

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

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

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

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

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

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

98
                // In SQLServer, TRUNCATE does not work with foreign keys, also
99
                // using "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
100
                // does not help here. So we switch to simple delete, but this
101
                // requires us to manually reset auto-increments afterward.
102
                if ($entityManager->getConnection()->getDatabasePlatform() instanceof SQLServerPlatform) {
14✔
103
                    $executor->getPurger()->setPurgeMode(ORMPurger::PURGE_MODE_DELETE);
×
104
                }
105

106
                // Purge even when no fixtures are defined, e.g. for tests that
107
                // require an empty database, like import tests.
108
                // Fix for PHP8: purge separately from inserting the fixtures,
109
                // as execute() would wrap the TRUNCATE in a transaction which
110
                // is auto-committed by MySQL when DDL queries are executed which
111
                // throws an exception in the entityManager ("There is no active
112
                // transaction", @see https://github.com/doctrine/migrations/issues/1104)
113
                // because he does not check if a transaction is still open
114
                // before calling commit().
115
                $executor->purge();
14✔
116

117
                // See above, we have to manually reset identities on SQLServer
118
                if ($entityManager->getConnection()->getDatabasePlatform() instanceof SQLServerPlatform) {
14✔
119
                    $entityManager->getConnection()->executeStatement(
×
120
                        "EXEC sp_MSforeachtable 'IF OBJECTPROPERTY(OBJECT_ID(''?''), ''TableHasIdentity'') = 1 DBCC CHECKIDENT (''?'', RESEED, 0)'"
×
121
                    );
×
122
                }
123

124
                break;
14✔
125
        }
126

127
        // now load any fixtures configured for "test" (or overwritten groups)
128
        $fixtures = static::getFixtures($container);
16✔
129
        if ([] !== $fixtures) {
16✔
130
            $executor->execute($fixtures, true);
×
131
        }
132

133
        return $kernel;
16✔
134
    }
135

136
    protected static function ensureKernelTestCase(): void
137
    {
138
        if (!is_a(static::class, KernelTestCase::class, true)) {
16✔
139
            throw new \LogicException(\sprintf('The test class must extend "%s" to use "%s".', KernelTestCase::class, static::class));
×
140
        }
141
    }
142

143
    /**
144
     * (Drops and re-) creates the (test) database if it does not exist.
145
     * This code tries to duplicate the behavior of the doctrine:database:drop
146
     * / doctrine:schema:create commands in the DoctrineBundle.
147
     *
148
     * @param bool $drop If true, the method will delete an existing database
149
     *                   before recreating it. If false, the database will only
150
     *                   be created if it doesn't exist.
151
     */
152
    protected static function recreateDatabase(
153
        EntityManagerInterface $em,
154
        bool $drop = false,
155
    ): void {
156
        $connection = $em->getConnection();
3✔
157
        $params = $params['primary'] ?? $connection->getParams();
3✔
158

159
        // this name will already contain the dbname_suffix (and the TEST_TOKEN)
160
        // if any is configured
161
        $dbName = $params['path'] ?? $params['dbname'] ?? false;
3✔
162
        if (!$dbName) {
3✔
163
            throw new \RuntimeException("Connection does not contain a 'dbname' or 'path' parameter, don't know how to proceed, aborting.");
×
164
        }
165

166
        unset($params['dbname'], $params['path']);
3✔
167
        if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
3✔
168
            $params['dbname'] = $params['default_dbname'] ?? 'postgres';
×
169
        }
170

171
        $tempConnection = DriverManager::getConnection($params, $connection->getConfiguration());
3✔
172
        $schemaManager = $tempConnection->createSchemaManager();
3✔
173

174
        // SQLite does not support checking for existing / dropping / creating
175
        // databases via Doctrine -> special handling here
176
        if ($schemaManager instanceof SQLiteSchemaManager) {
3✔
177
            if ($drop && file_exists($dbName)) {
3✔
178
                unlink($dbName);
1✔
179
            }
180

181
            // the database file will be automatically created on first use,
182
            // no need to create it here
183
            return;
3✔
184
        }
185

186
        // only check this with the new connection, as it would fail with the
187
        // old connection when the database indeed does not exist
188
        try {
189
            $dbNames = array_map(
×
190
                static fn($n) => $n->getIdentifier()->getValue(),
×
191
                $schemaManager->introspectDatabaseNames()
×
192
            );
×
193
            $dbExists = \in_array($dbName, $dbNames, true);
×
194
        }
195
        catch (DatabaseRequired) {
×
196
            $dbExists = false;
×
197
        }
198

199
        if ($drop && $dbExists) {
×
200
            // close the current connection in the em, it would be invalid
201
            // anyway after the drop
202
            $connection->close();
×
203

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

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

217
            $schemaManager->dropDatabase($dbName);
×
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
    protected static function fixtureCleanup(): void
293
    {
294
        static::$fixtures = null;
×
295
    }
296
}
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