mirror of https://github.com/flightphp/core
Merge dd39d4adad into 2ab26aa326
commit
78b5535d79
@ -0,0 +1,27 @@
|
||||
# FlightPHP/Core Project Instructions
|
||||
|
||||
## Overview
|
||||
This is the main FlightPHP core library for building fast, simple, and extensible PHP web applications. It is dependency-free for core usage and supports PHP 7.4+.
|
||||
|
||||
## Project Guidelines
|
||||
- PHP 7.4 must be supported. PHP 8 or greater also supported, but avoid PHP 8+ only features.
|
||||
- Keep the core library dependency-free (no polyfills or interface-only repositories).
|
||||
- All Flight projects are meant to be kept simple and fast. Performance is a priority.
|
||||
- Flight is extensible and when implementing new features, consider how they can be added as plugins or extensions rather than bloating the core library.
|
||||
- Any new features built into the core should be well-documented and tested.
|
||||
- Any new features should be added with a focus on simplicity and performance, avoiding unnecessary complexity.
|
||||
- This is not a Laravel, Yii, Code Igniter or Symfony clone. It is a simple, fast, and extensible framework that allows you to build applications quickly without the overhead of large frameworks.
|
||||
|
||||
## Development & Testing
|
||||
- Run tests: `composer test` (uses phpunit/phpunit and spatie/phpunit-watcher)
|
||||
- Run test server: `composer test-server` or `composer test-server-v2`
|
||||
- Lint code: `composer lint` (uses phpstan/phpstan, level 6)
|
||||
- Beautify code: `composer beautify` (uses squizlabs/php_codesniffer, PSR1)
|
||||
- Check code style: `composer phpcs`
|
||||
- Test coverage: `composer test-coverage`
|
||||
|
||||
## Coding Standards
|
||||
- Follow PSR1 coding standards (enforced by PHPCS)
|
||||
- Use strict comparisons (`===`, `!==`)
|
||||
- PHPStan level 6 compliance
|
||||
- Focus on PHP 7.4 compatibility (avoid PHP 8+ only features)
|
||||
@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests;
|
||||
|
||||
use flight\database\SimplePdo;
|
||||
use flight\util\Collection;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SimplePdoTest extends TestCase
|
||||
{
|
||||
private SimplePdo $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = new SimplePdo('sqlite::memory:');
|
||||
// Create a test table and insert 3 rows of data
|
||||
$this->db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
|
||||
$this->db->exec('INSERT INTO users (name, email) VALUES ("John", "john@example.com")');
|
||||
$this->db->exec('INSERT INTO users (name, email) VALUES ("Jane", "jane@example.com")');
|
||||
$this->db->exec('INSERT INTO users (name, email) VALUES ("Bob", "bob@example.com")');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->db->exec('DROP TABLE users');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Constructor Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDefaultFetchModeIsAssoc(): void
|
||||
{
|
||||
$fetchMode = $this->db->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
|
||||
$this->assertEquals(PDO::FETCH_ASSOC, $fetchMode);
|
||||
}
|
||||
|
||||
public function testCustomFetchModeCanBeSet(): void
|
||||
{
|
||||
$db = new SimplePdo('sqlite::memory:', null, null, [
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ
|
||||
]);
|
||||
$fetchMode = $db->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
|
||||
$this->assertEquals(PDO::FETCH_OBJ, $fetchMode);
|
||||
}
|
||||
|
||||
public function testApmTrackingOffByDefault(): void
|
||||
{
|
||||
// APM is off by default, so logQueries should not trigger events
|
||||
// We test this indirectly by calling logQueries and ensuring no error
|
||||
$this->db->logQueries();
|
||||
$this->assertTrue(true); // If we get here, no exception was thrown
|
||||
}
|
||||
|
||||
public function testApmTrackingCanBeEnabled(): void
|
||||
{
|
||||
$db = new SimplePdo('sqlite::memory:', null, null, null, [
|
||||
'trackApmQueries' => true
|
||||
]);
|
||||
$db->exec('CREATE TABLE test (id INTEGER PRIMARY KEY)');
|
||||
$db->runQuery('SELECT * FROM test');
|
||||
$db->logQueries(); // Should work without error
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// runQuery Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testRunQueryReturnsStatement(): void
|
||||
{
|
||||
$stmt = $this->db->runQuery('SELECT * FROM users');
|
||||
$this->assertInstanceOf(PDOStatement::class, $stmt);
|
||||
}
|
||||
|
||||
public function testRunQueryWithParams(): void
|
||||
{
|
||||
$stmt = $this->db->runQuery('SELECT * FROM users WHERE name = ?', ['John']);
|
||||
$rows = $stmt->fetchAll();
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertEquals('John', $rows[0]['name']);
|
||||
}
|
||||
|
||||
public function testRunQueryWithoutParamsWithMaxQueryMetrics(): void
|
||||
{
|
||||
$db = new class ('sqlite::memory:', null, null, null, ['maxQueryMetrics' => 2, 'trackApmQueries' => true]) extends SimplePdo {
|
||||
public function getQueryMetrics(): array
|
||||
{
|
||||
return $this->queryMetrics;
|
||||
}
|
||||
};
|
||||
$db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
|
||||
$db->exec('INSERT INTO users (name, email) VALUES ("John", "john@example.com")');
|
||||
$db->exec('INSERT INTO users (name, email) VALUES ("Jane", "jane@example.com")');
|
||||
$db->exec('INSERT INTO users (name, email) VALUES ("Bob", "bob@example.com")');
|
||||
|
||||
$db->runQuery('SELECT * FROM users WHERE 1 = 1');
|
||||
$db->runQuery('SELECT * FROM users WHERE 1 = 2');
|
||||
$this->assertEquals(2, count($db->getQueryMetrics()));
|
||||
$db->runQuery('SELECT * FROM users WHERE 1 = 3');
|
||||
$dbMetrics = $db->getQueryMetrics();
|
||||
$this->assertEquals(2, count($dbMetrics));
|
||||
$this->assertEquals('SELECT * FROM users WHERE 1 = 2', $dbMetrics[0]['sql']);
|
||||
$this->assertEquals('SELECT * FROM users WHERE 1 = 3', $dbMetrics[1]['sql']);
|
||||
}
|
||||
|
||||
public function testRunQueryInsert(): void
|
||||
{
|
||||
$stmt = $this->db->runQuery('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com']);
|
||||
$this->assertEquals(1, $stmt->rowCount());
|
||||
}
|
||||
|
||||
public function testRunQueryThrowsOnPrepareFailure(): void
|
||||
{
|
||||
$this->expectException(PDOException::class);
|
||||
$this->db->runQuery('SELECT * FROM nonexistent_table');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fetchRow Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testFetchRowReturnsCollection(): void
|
||||
{
|
||||
$row = $this->db->fetchRow('SELECT * FROM users WHERE id = ?', [1]);
|
||||
$this->assertInstanceOf(Collection::class, $row);
|
||||
$this->assertEquals('John', $row['name']);
|
||||
}
|
||||
|
||||
public function testFetchRowReturnsNullWhenNoResults(): void
|
||||
{
|
||||
$row = $this->db->fetchRow('SELECT * FROM users WHERE id = ?', [999]);
|
||||
$this->assertNull($row);
|
||||
}
|
||||
|
||||
public function testFetchRowAddsLimitAutomatically(): void
|
||||
{
|
||||
// Even though there are 3 rows, fetchRow should only return 1
|
||||
$row = $this->db->fetchRow('SELECT * FROM users');
|
||||
$this->assertInstanceOf(Collection::class, $row);
|
||||
$this->assertEquals(1, $row['id']);
|
||||
}
|
||||
|
||||
public function testFetchRowDoesNotDuplicateLimitClause(): void
|
||||
{
|
||||
// Query already has LIMIT - should not add another
|
||||
$row = $this->db->fetchRow('SELECT * FROM users ORDER BY id DESC LIMIT 1');
|
||||
$this->assertInstanceOf(Collection::class, $row);
|
||||
$this->assertEquals(3, $row['id']); // Should be Bob (id=3)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fetchAll Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testFetchAllReturnsArrayOfCollections(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users');
|
||||
$this->assertIsArray($rows);
|
||||
$this->assertCount(3, $rows);
|
||||
$this->assertInstanceOf(Collection::class, $rows[0]);
|
||||
}
|
||||
|
||||
public function testFetchAllReturnsEmptyArrayWhenNoResults(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE 1 = 0');
|
||||
$this->assertIsArray($rows);
|
||||
$this->assertCount(0, $rows);
|
||||
}
|
||||
|
||||
public function testFetchAllWithParams(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE name LIKE ?', ['J%']);
|
||||
$this->assertCount(2, $rows); // John and Jane
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fetchColumn Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testFetchColumnReturnsFlatArray(): void
|
||||
{
|
||||
$names = $this->db->fetchColumn('SELECT name FROM users ORDER BY id');
|
||||
$this->assertIsArray($names);
|
||||
$this->assertEquals(['John', 'Jane', 'Bob'], $names);
|
||||
}
|
||||
|
||||
public function testFetchColumnWithParams(): void
|
||||
{
|
||||
$ids = $this->db->fetchColumn('SELECT id FROM users WHERE name LIKE ?', ['J%']);
|
||||
$this->assertEquals([1, 2], $ids);
|
||||
}
|
||||
|
||||
public function testFetchColumnReturnsEmptyArrayWhenNoResults(): void
|
||||
{
|
||||
$result = $this->db->fetchColumn('SELECT id FROM users WHERE 1 = 0');
|
||||
$this->assertEquals([], $result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fetchPairs Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testFetchPairsReturnsKeyValueArray(): void
|
||||
{
|
||||
$pairs = $this->db->fetchPairs('SELECT id, name FROM users ORDER BY id');
|
||||
$this->assertEquals([1 => 'John', 2 => 'Jane', 3 => 'Bob'], $pairs);
|
||||
}
|
||||
|
||||
public function testFetchPairsWithParams(): void
|
||||
{
|
||||
$pairs = $this->db->fetchPairs('SELECT id, email FROM users WHERE name = ?', ['John']);
|
||||
$this->assertEquals([1 => 'john@example.com'], $pairs);
|
||||
}
|
||||
|
||||
public function testFetchPairsReturnsEmptyArrayWhenNoResults(): void
|
||||
{
|
||||
$pairs = $this->db->fetchPairs('SELECT id, name FROM users WHERE 1 = 0');
|
||||
$this->assertEquals([], $pairs);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IN Statement Processing Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testInStatementWithArrayOfIntegers(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE id IN(?)', [[1, 2]]);
|
||||
$this->assertCount(2, $rows);
|
||||
}
|
||||
|
||||
public function testInStatementWithArrayOfStrings(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE name IN(?)', [['John', 'Jane']]);
|
||||
$this->assertCount(2, $rows);
|
||||
}
|
||||
|
||||
public function testInStatementWithEmptyArray(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE id IN(?)', [[]]);
|
||||
$this->assertCount(0, $rows); // IN(NULL) matches nothing
|
||||
}
|
||||
|
||||
public function testInStatementWithSingleValue(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE id IN(?)', [1]);
|
||||
$this->assertCount(1, $rows);
|
||||
}
|
||||
|
||||
public function testMultipleInStatements(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll(
|
||||
'SELECT * FROM users WHERE id IN(?) AND name IN(?)',
|
||||
[[1, 2, 3], ['John', 'Bob']]
|
||||
);
|
||||
$this->assertCount(2, $rows); // John (id=1) and Bob (id=3)
|
||||
}
|
||||
|
||||
public function testInStatementWithOtherParams(): void
|
||||
{
|
||||
$rows = $this->db->fetchAll(
|
||||
'SELECT * FROM users WHERE id > ? AND name IN(?)',
|
||||
[0, ['John', 'Jane']]
|
||||
);
|
||||
$this->assertCount(2, $rows);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// insert() Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testInsertSingleRow(): void
|
||||
{
|
||||
$id = $this->db->insert('users', ['name' => 'Alice', 'email' => 'alice@example.com']);
|
||||
$this->assertEquals('4', $id);
|
||||
|
||||
$row = $this->db->fetchRow('SELECT * FROM users WHERE id = ?', [$id]);
|
||||
$this->assertEquals('Alice', $row['name']);
|
||||
$this->assertEquals('alice@example.com', $row['email']);
|
||||
}
|
||||
|
||||
public function testInsertBulkRows(): void
|
||||
{
|
||||
$id = $this->db->insert('users', [
|
||||
['name' => 'Alice', 'email' => 'alice@example.com'],
|
||||
['name' => 'Charlie', 'email' => 'charlie@example.com'],
|
||||
]);
|
||||
|
||||
// Last insert ID should be 5 (Charlie)
|
||||
$this->assertEquals('5', $id);
|
||||
|
||||
// Verify both rows were inserted
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE id > 3 ORDER BY id');
|
||||
$this->assertCount(2, $rows);
|
||||
$this->assertEquals('Alice', $rows[0]['name']);
|
||||
$this->assertEquals('Charlie', $rows[1]['name']);
|
||||
}
|
||||
|
||||
public function testInsertBulkWithEmptyArrayThrows(): void
|
||||
{
|
||||
$this->expectException(PDOException::class);
|
||||
$this->expectExceptionMessage('Cannot perform bulk insert with empty data array');
|
||||
|
||||
$this->db->insert('users', [[]]);
|
||||
}
|
||||
|
||||
public function testInsertBulkWithMismatchedColumnCountThrows(): void
|
||||
{
|
||||
$this->expectException(PDOException::class);
|
||||
$this->expectExceptionMessage('columns');
|
||||
|
||||
$this->db->insert('users', [
|
||||
['name' => 'Alice', 'email' => 'alice@example.com'],
|
||||
['name' => 'Charlie'], // Missing email column
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// update() Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateReturnsAffectedRowCount(): void
|
||||
{
|
||||
$count = $this->db->update('users', ['name' => 'Updated'], 'name LIKE ?', ['J%']);
|
||||
$this->assertEquals(2, $count); // John and Jane
|
||||
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users WHERE name = ?', ['Updated']);
|
||||
$this->assertCount(2, $rows);
|
||||
}
|
||||
|
||||
public function testUpdateSingleRow(): void
|
||||
{
|
||||
$count = $this->db->update('users', ['email' => 'newemail@example.com'], 'id = ?', [1]);
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
$row = $this->db->fetchRow('SELECT * FROM users WHERE id = ?', [1]);
|
||||
$this->assertEquals('newemail@example.com', $row['email']);
|
||||
}
|
||||
|
||||
public function testUpdateNoMatchingRows(): void
|
||||
{
|
||||
$count = $this->db->update('users', ['name' => 'Nobody'], 'id = ?', [999]);
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// delete() Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDeleteReturnsDeletedRowCount(): void
|
||||
{
|
||||
$count = $this->db->delete('users', 'name LIKE ?', ['J%']);
|
||||
$this->assertEquals(2, $count); // John and Jane
|
||||
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users');
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertEquals('Bob', $rows[0]['name']);
|
||||
}
|
||||
|
||||
public function testDeleteSingleRow(): void
|
||||
{
|
||||
$count = $this->db->delete('users', 'id = ?', [1]);
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users');
|
||||
$this->assertCount(2, $rows);
|
||||
}
|
||||
|
||||
public function testDeleteNoMatchingRows(): void
|
||||
{
|
||||
$count = $this->db->delete('users', 'id = ?', [999]);
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// transaction() Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testTransactionCommitsOnSuccess(): void
|
||||
{
|
||||
$result = $this->db->transaction(function ($db) {
|
||||
$db->runQuery('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com']);
|
||||
return $db->lastInsertId();
|
||||
});
|
||||
|
||||
$this->assertEquals('4', $result);
|
||||
|
||||
$row = $this->db->fetchRow('SELECT * FROM users WHERE id = ?', [4]);
|
||||
$this->assertEquals('Alice', $row['name']);
|
||||
}
|
||||
|
||||
public function testTransactionRollsBackOnException(): void
|
||||
{
|
||||
try {
|
||||
$this->db->transaction(function ($db) {
|
||||
$db->runQuery('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com']);
|
||||
throw new \RuntimeException('Something went wrong');
|
||||
});
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->assertEquals('Something went wrong', $e->getMessage());
|
||||
}
|
||||
|
||||
// Verify the insert was rolled back
|
||||
$rows = $this->db->fetchAll('SELECT * FROM users');
|
||||
$this->assertCount(3, $rows); // Still only the original 3 rows
|
||||
}
|
||||
|
||||
public function testTransactionReturnsCallbackValue(): void
|
||||
{
|
||||
$result = $this->db->transaction(function () {
|
||||
return 'hello world';
|
||||
});
|
||||
|
||||
$this->assertEquals('hello world', $result);
|
||||
}
|
||||
|
||||
public function testTransactionRethrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Test exception');
|
||||
|
||||
$this->db->transaction(function () {
|
||||
throw new \InvalidArgumentException('Test exception');
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fetchField (inherited from PdoWrapper) Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testFetchFieldReturnsValue(): void
|
||||
{
|
||||
$name = $this->db->fetchField('SELECT name FROM users WHERE id = ?', [1]);
|
||||
$this->assertEquals('John', $name);
|
||||
}
|
||||
|
||||
public function testFetchFieldReturnsFirstColumn(): void
|
||||
{
|
||||
$id = $this->db->fetchField('SELECT id, name FROM users WHERE id = ?', [1]);
|
||||
$this->assertEquals(1, $id);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue