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

j-schumann / symfony-addons / 16373063365

18 Jul 2025 02:28PM UTC coverage: 53.846% (+0.2%) from 53.664%
16373063365

push

github

j-schumann
fix: RefreshDatabaseTrait warning

1 of 1 new or added line in 1 file covered. (100.0%)

22 existing lines in 1 file now uncovered.

497 of 923 relevant lines covered (53.85%)

3.43 hits per line

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

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

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

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

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

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

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

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

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

83
                static::updateSchema($entityManager, true);
1✔
84
                break;
1✔
85

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

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

107
                break;
14✔
108
        }
109

110
        // now load any fixtures configured for "test" (or overwritten groups)
111
        $fixtures = static::getFixtures($container);
16✔
112
        if ([] !== $fixtures) {
16✔
UNCOV
113
            $executor->execute($fixtures, true);
×
114
        }
115

116
        return $kernel;
16✔
117
    }
118

119
    protected static function ensureKernelTestCase(): void
120
    {
121
        if (!is_a(static::class, KernelTestCase::class, true)) {
16✔
122
            throw new \LogicException(\sprintf('The test class must extend "%s" to use "%s".', KernelTestCase::class, static::class));
×
123
        }
124
    }
125

126
    /**
127
     * (Drops and re-) creates the (test) database if it does not exist.
128
     * This code tries to duplicate the behavior of the doctrine:database:drop
129
     * / doctrine:schema:create commands in the DoctrineBundle.
130
     *
131
     * @param bool $drop If true, the method will delete an existing database
132
     *                   before recreating it. If false, the database will only
133
     *                   be created if it doesn't exist.
134
     */
135
    protected static function recreateDatabase(
136
        EntityManagerInterface $em,
137
        bool $drop = false,
138
    ): void {
139
        $connection = $em->getConnection();
3✔
140
        $params = $connection->getParams();
3✔
141
        if (isset($params['primary'])) {
3✔
UNCOV
142
            $params = $params['primary'];
×
143
        }
144

145
        // this name will already contain the dbname_suffix (and the TEST_TOKEN)
146
        // if any is configured
147
        $dbName = $params['path'] ?? $params['dbname'] ?? false;
3✔
148
        if (!$dbName) {
3✔
UNCOV
149
            throw new \RuntimeException("Connection does not contain a 'dbname' or 'path' parameter, don't know how to proceed, aborting.");
×
150
        }
151

152
        unset($params['dbname'], $params['path']);
3✔
153
        if ($connection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
3✔
UNCOV
154
            $params['dbname'] = $params['default_dbname'] ?? 'postgres';
×
155
        }
156

157
        $tempConnection = DriverManager::getConnection($params, $connection->getConfiguration());
3✔
158
        $schemaManager = $tempConnection->createSchemaManager();
3✔
159

160
        // SQLite does not support checking for existing / dropping / creating
161
        // databases via Doctrine -> special handling here
162
        if ($schemaManager instanceof SQLiteSchemaManager) {
3✔
163
            if ($drop && file_exists($dbName)) {
3✔
164
                unlink($dbName);
1✔
165
            }
166

167
            // the database file will be automatically created on first use,
168
            // no need to create it here
169
            return;
3✔
170
        }
171

172
        // only check this with the new connection, as it would fail with the
173
        // old connection when the database indeed does not exist
UNCOV
174
        $dbExists = \in_array($dbName, $schemaManager->listDatabases());
×
175

UNCOV
176
        if ($drop && $dbExists) {
×
177
            // close the current connection in the em, it would be invalid
178
            // anyway after the drop
UNCOV
179
            $connection->close();
×
180

181
            // For Postgres, closing the old connection is not
182
            // enough to prevent: 'ERROR: database "db_test" is being accessed by other users'
UNCOV
183
            if ($tempConnection->getDatabasePlatform() instanceof PostgreSQLPlatform) {
×
UNCOV
184
                $tempConnection->executeStatement(
×
UNCOV
185
                    'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = ? AND pid <> pg_backend_pid()',
×
UNCOV
186
                    [$dbName]
×
UNCOV
187
                );
×
188
            }
189

UNCOV
190
            $schemaManager->dropDatabase($dbName);
×
UNCOV
191
            $dbExists = false;
×
192
        }
193

194
        // Create the database only if it doesn't already exist. Skip for SQLite
195
        // as it creates database files automatically and this call would throw
196
        // an exception.
UNCOV
197
        if (!$dbExists && !$schemaManager instanceof SQLiteSchemaManager) {
×
UNCOV
198
            $schemaManager->createDatabase($dbName);
×
199
        }
200

UNCOV
201
        $tempConnection->close();
×
202
    }
203

204
    /**
205
     * Brings the db schema to the newest version.
206
     *
207
     * @param bool $drop If true, the method will drop the current schema, e.g.
208
     *                   to reset all data, as dropping & recreating the schema
209
     *                   will often be faster than truncating all tables.
210
     */
211
    protected static function updateSchema(
212
        EntityManagerInterface $em,
213
        bool $drop = false,
214
    ): void {
215
        $metadatas = $em->getMetadataFactory()->getAllMetadata();
4✔
216
        if (empty($metadatas)) {
4✔
UNCOV
217
            return;
×
218
        }
219

220
        $schemaTool = new SchemaTool($em);
4✔
221

222
        if ($drop) {
4✔
223
            $schemaTool->dropDatabase();
1✔
224
        }
225

226
        $schemaTool->updateSchema($metadatas);
4✔
227
    }
228

229
    /**
230
     * Use a static fixture cache as we need them before each test.
231
     */
232
    protected static function getFixtures(ContainerInterface $container): array
233
    {
234
        if ([] === static::$fixtureGroups) {
16✔
235
            // the fixture loader returns all possible fixtures if called
236
            // with an empty array -> catch here
UNCOV
237
            return [];
×
238
        }
239

240
        if (\is_array(static::$fixtures)) {
16✔
241
            return static::$fixtures;
15✔
242
        }
243

244
        $fixturesLoader = $container->get('doctrine.fixtures.loader');
2✔
245
        static::$fixtures = $fixturesLoader->getFixtures(static::$fixtureGroups);
2✔
246

247
        return static::$fixtures;
2✔
248
    }
249

250
    /**
251
     * Returns a new executor instance, we need it before each test execution.
252
     */
253
    protected static function getExecutor(EntityManagerInterface $em): ORMExecutor
254
    {
255
        $purger = new ORMPurger($em);
16✔
256
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
16✔
257

258
        // don't use a static Executor, it contains the EM which could be closed
259
        // through (expected) exceptions and would not work
260
        return new ORMExecutor($em, $purger);
16✔
261
    }
262

263
    protected static function fixtureCleanup(): void
264
    {
UNCOV
265
        static::$fixtures = null;
×
266
    }
267
}
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