cycle/database
Cycle DBAL provides a secure PDO-based database layer with support for MySQL, PostgreSQL, SQLite, and SQL Server. Includes schema introspection/declaration, migrations, smart identifier quoting, query builders, nested queries, and transactions.
Installation:
composer require cycle/database
Requires PHP 8.0+ and PDO extensions for your target database (MySQL, PostgreSQL, SQLite, SQLServer).
Basic Configuration:
Create a Config\DatabaseConfig instance and initialize DatabaseManager:
use Cycle\Database\{Config, DatabaseManager};
$dbm = new DatabaseManager(new Config\DatabaseConfig([
'databases' => ['default' => ['connection' => 'mysql']],
'connections' => [
'mysql' => new Config\MySQLDriverConfig(
connection: new Config\MySQL\TcpConnectionConfig(
host: 'localhost',
port: 3306,
user: 'root',
password: '',
database: 'test_db',
),
),
],
]));
First Use Case: Access a table and define its schema:
$users = $dbm->database('default')->table('users');
$schema = $users->getSchema();
$schema->primary('id')->integer();
$schema->string('name')->nullable();
$schema->save(); // Creates table if it doesn't exist
Key Entry Points:
DatabaseManager → Manages multiple database connections.Table → Represents a database table (e.g., $dbm->database()->table('users')).Schema → Defines table structure (columns, constraints, etc.).QueryBuilder → For CRUD operations (e.g., $users->select()->where(['name' => 'John'])).Define schemas programmatically for consistency:
$schema = $users->getSchema();
$schema->primary('id')->integer()->autoIncrement();
$schema->string('email')->unique()->notNull();
$schema->datetime('created_at')->default(fn() => new DateTimeImmutable());
$schema->save(); // Syncs with the database
Inspect existing tables:
$schema = $users->getSchema();
$columns = $schema->getColumns(); // Returns ColumnInterface[] for introspection
foreach ($columns as $column) {
echo $column->getName() . ': ' . $column->getType() . "\n";
}
Use SchemaMigration for version-controlled schema changes:
use Cycle\Database\Migration\MigrationInterface;
use Cycle\Database\Migration\SchemaMigration;
class CreateUsersTable implements MigrationInterface
{
public function up(SchemaMigration $migration): void
{
$migration->table('users')->create(function (Schema $schema) {
$schema->primary('id')->integer();
$schema->string('name');
});
}
public function down(SchemaMigration $migration): void
{
$migration->table('users')->drop();
}
}
// Insert
$users->insertOne(['name' => 'Alice']);
// Select
foreach ($users->select()->where(['name' => 'Alice']) as $user) {
print_r($user);
}
// Update
$users->update(['name' => 'Bob'])->where(['id' => 1]);
// Delete
$users->delete()->where(['id' => 1]);
Subqueries:
$subQuery = $users->select(['id'])->where(['active' => true]);
$activeUsersCount = $users->select()->where(fn(SelectQuery $q) => $q->whereIn('id', $subQuery));
Joins with wrapOnWhere():
$query = $users->select()
->join('orders', fn(Join $join) => $join->on('users.id', 'orders.user_id'))
->wrapOnWhere(fn(SelectQuery $q) => $q->where('orders.status', 'completed'));
Server-Side Cursors (for large datasets):
$cursor = $users->select()->cursor();
foreach ($cursor as $user) {
// Process chunked results
}
ON CONFLICT (Upsert):
$users->insertOne(['name' => 'Charlie'])
->onConflict(['name'])
->doUpdate(['updated_at' => new DateTimeImmutable()]);
$dbm->transaction(function (DatabaseManager $dbm) {
$users = $dbm->database()->table('users');
$users->insertOne(['name' => 'Dave']);
$users->insertOne(['name' => 'Eve']);
// Both inserts succeed or fail together
});
Use DatabaseIsolation for read/write isolation:
$isolation = new DatabaseIsolation($dbm);
$isolation->beginReadOnly(); // Starts a read-only transaction
$users = $isolation->database()->table('users');
foreach ($users->select() as $user) {
// Read-only operations
}
$isolation->commit();
$page = $users->select()->paginate(10, 2); // Page 2, 10 items per page
foreach ($page as $user) {
// Process paginated results
}
$users->select()->chunk(50, function (array $chunk) {
// Process 50 records at a time
});
$primaryDb = $dbm->database('default');
$replicaDb = $dbm->database('replica');
// Read from replica, write to primary
$replicaDb->table('users')->select()->where(['active' => true]);
$primaryDb->table('users')->insertOne(['name' => 'Frank']);
$schema = $users->getSchema();
$schema->sync(); // Syncs schema to all configured databases
Schema Caching:
save()) are cached. Clear the cache manually if needed:
$dbm->getDriver()->clearCache();
Schema::sync() to force a refresh.MySQL Boolean Columns:
BOOLEAN columns default to TINYINT(1). Use boolean()->unsigned() for clarity:
$schema->boolean('is_active')->unsigned();
Subquery Parameters:
$subQuery = $users->select(['id'])->where(['active' => true]);
$query = $users->select()->whereIn('id', $subQuery->getParameters());
Server-Side Cursors:
cursor() only for MySQL/PostgreSQL:
if ($dbm->getDriver() instanceof \Cycle\Database\Driver\MySQLDriver) {
$cursor = $users->select()->cursor();
}
ON CONFLICT Limitations:
ON CONFLICT is PostgreSQL/MySQL-specific. For SQLite, use insertOrIgnore() or replace().Case Sensitivity:
Query Logging:
Enable query logging via DatabaseManager:
$dbm = new DatabaseManager(new Config\DatabaseConfig([
'logging' => true,
// ... other config
]));
Logs will appear in PHP error logs or a custom logger.
Parameter Binding:
Named parameters (e.g., :name) are supported but require explicit binding:
$users->select()->where(['name' => ':name'])->bind(['name' => 'Alice']);
Schema Validation: Validate schemas before saving:
$schema->validate(); // Throws exceptions for invalid schemas
Connection Issues:
new Config\MySQLDriverConfig(
connection: new Config\MySQL\TcpConnectionConfig(
// ... config
reconnect: true,
),
);
Cycle\Database\Schema\Column for custom types (e.g., ULID, SNOWFLAKE):
class UlidColumn extends AbstractColumn
{
public function getType(): string
{
return 'ULID';
}
}
How can I help you explore Laravel packages today?