Tables created through createTable() for this class. */ private static array $tables = []; /** * Clears Model's static query-builder state before every test. * * A query that dies mid-flight (unsupported function, constraint * violation, ...) never reaches Model's internal resetQuery(), so * leftovers would leak into the next test otherwise. This keeps the * suite order-independent regardless of PHPUnit's executionOrder. */ protected function setUp(): void { parent::setUp(); (new \ReflectionMethod(Model::class, 'resetQuery'))->invoke(null); } /** * Returns the PDO connection to the in-memory test database. * * @return PDO */ protected static function db(): PDO { return Database::getInstance(DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS); } /** * DDL fragments per engine for each logical column type. * * @var array> */ private const TYPE_DDL = [ 'sqlite' => [ 'pk' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'string' => 'VARCHAR(255)', 'text' => 'TEXT', 'number' => 'REAL', 'bool' => 'INTEGER', 'timestamp' => 'TEXT', ], 'mysql' => [ 'pk' => 'INT AUTO_INCREMENT PRIMARY KEY', 'string' => 'VARCHAR(255)', 'text' => 'TEXT', 'number' => 'DOUBLE', 'bool' => 'TINYINT(1)', 'timestamp' => 'DATETIME', ], 'pgsql' => [ 'pk' => 'SERIAL PRIMARY KEY', 'string' => 'VARCHAR(255)', 'text' => 'TEXT', 'number' => 'DOUBLE PRECISION', 'bool' => 'BOOLEAN', 'timestamp' => 'TIMESTAMP', ], ]; /** * Creates a table in the test database from logical column types and * registers it for cleanup. The DDL is translated per engine, so the * same declaration works on sqlite, MySQL/MariaDB and PostgreSQL. * * @param string $name * Table name (avoid SQL reserved words). * * @param array $columns * Map of column name to logical type: * pk|string|text|number|bool|timestamp. */ protected static function createTable(string $name, array $columns): void { $types = self::TYPE_DDL[DB_TYPE] ?? throw new \InvalidArgumentException( 'No type translation for DB_TYPE ' . DB_TYPE ); $ddl = []; foreach ($columns as $column => $type) { $fragment = $types[$type] ?? throw new \InvalidArgumentException( "Unknown logical type '{$type}' for column '{$column}'; " . 'expected: ' . implode(', ', array_keys($types)) ); $ddl[] = $column . ' ' . $fragment; } static::db()->exec("CREATE TABLE IF NOT EXISTS {$name} (" . implode(', ', $ddl) . ')'); static::$tables[] = $name; } /** * Never let an aborted test leak an open transaction into the next one. */ protected function tearDown(): void { if (static::db()->inTransaction()) { static::db()->rollBack(); } parent::tearDown(); } /** * Drops every table registered through createTable() during this class. */ public static function tearDownAfterClass(): void { foreach (array_unique(static::$tables) as $table) { static::db()->exec("DROP TABLE IF EXISTS {$table}"); } static::$tables = []; } }