Overview
Description
Caldera Database is an abstraction layer for database operations.
As with the other Caldera components it has been built to be swappable and modular.
Installation
The easisest way to install it is to use Composer:
composer require vecode/caldera-database
Requires
php: >=8.1ext-mbstring: *ext-pdo: *
Basic usage
Getting started
First you will need to connect to the database, but for doing so you need an adapter.
The class is an abstract wrapper for database operations, so the adapters are required to bridge the logic to the actual database engine.
Out of the box two adapters are included, MySQLAdapter and SQLiteAdapter, both of them implement the AdapterInterface through the PDOAdapter abstract class, so that you may implement your own should you need to.
So, for example you may do the following for MySQL:
use Caldera\Database\Database;
use Caldera\Database\Adapter\MySQLAdapter;
$options = [
'host' => getenv('DB_HOST'),
'name' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'),
];
$adapter = new MySQLAdapter($options);
$database = new Database($adapter);
Or for SQLite:
use Caldera\Database\Database;
use Caldera\Database\Adapter\SQLiteAdapter;
$options = [
'file' => dirname(__FILE__) . '/database.sqlite'
];
$adapter = new SQLiteAdapter($options);
$database = new Database($adapter);
You can check the connection status with isConnected:
if ( $database->isConnected() ) {
// Good to go
}
Simple queries
To execute a simple query just use the query method:
$database->query("DROP TABLE IF EXISTS test");
In case you need to pass parameters just add another argument:
$database->query("DELETE FROM user WHERE id = ?", [$user_id]);
Also you may pass a callback that receives the PDOStatement so that you can change the fetch mode or use a custom model for example:
use App\Model\User;
$rows = $database->query("SELECT * FROM user WHERE id = ?", [$user_id], function($stmt) {
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
return $stmt->fetchAll();
});
For INSERT queries you can use the lastInsertId method to get the ID:
$database->query("INSERT INTO test (id, name, created, updated) VALUES (0, 'Foo', NOW(), NOW())");
$id = $database->lastInsertId();
Selection queries
To retrieve data from the database use the select method; usually you will need to pass parameters and you may do so by passing an array as the last argument:
$rows = $database->select("SELECT id, name FROM user WHERE name = ?", [$name]);
You may also use named parameters:
$rows = $database->select("SELECT id, name FROM user WHERE name = :name", ['name' => $name]);
For scalar results you can use the scalar method:
$count = $database->scalar("SELECT COUNT(id) FROM user");
Finally, you can chunk your results with the chunk method:
// Chunk the select in groups of 100 rows
$database->chunk(100, "SELECT id, name FROM user", [], function($rows) {
foreach ($rows as $row) {
// Do something with the row
}
});
Transactions
You can easily execute transactions, either manual or automatic.
For manual transactions use the begin, commit and rollback methods:
$database->begin();
$database->query("UPDATE user SET name = ? WHERE id = ?", ['Foo', 3]);
$database->query("UPDATE user SET name = ? WHERE id = ?", ['Bar', 2]);
$database->commit();
And the there's the automatic transactions; they will automatically execute the rollback method if there's an exception thrown, otherwise they will call commit:
$database->transaction(function($database) {
$database->query("UPDATE user SET name = ? WHERE id = ?", ['Foo', 3]);
$database->query("UPDATE user SET name = ? WHERE id = ?", ['Bar', 2]);
});
Schema builder
With the schema builder you can easily create. modify and delete tables, just create an instance of Schema:
use Caldera\Database\Schema\Schema;
$schema = new Schema($database);
Just pass a $database object and you're good to go.
Getting tables, columns and keys
Get all the tables on the currently selected database with the getTables method:
$tables = $schema->getTables();
To retrieve the columns or keys of an specific table, use the corresponding method:
# Get the columns on USERS
$columns = $schema->getColumns('users');
# Get the keys on USERS
$keys = $schema->getKeys('users');
You may also check for the existence of any of the three entities:
# Check if USERS table exists
$schema->hasTable('users');
# Check if USERS has an ID column
$schema->hasColumn('users', 'id');
# Check if USERS has a PK_ID key
$schema->hasKey('users', 'pk_id');
Creating tables
To create a table use the create or createIfNotExists methods:
$schema->create('items', function(Table $table) {
$table->bigInteger('id')->autoIncrement();
$table->string('name', 120);
$table->string('status', 50);
$table->string('type', 50);
$table->integer('points')->nullable()->default(0);
$table->datetime('created')->nullable();
$table->datetime('updated')->nullable();
$table->primary('pk_id', 'id');
});
You will need to pass the table name and a Closure which will be called back with a Table instance, with which you will be able to specify the operations required to create the table.
In the above example several columns are added, some with nullable or default values; also a primary key is specified.
The following are the supported column types (support depends on the database adapter):
bigInteger- The equivalent ofBIGINT, receives the following parameters:string $namebinary- The equivalent ofBINARY, receives the following parameters:string $nameboolean- The equivalent ofBOOLEAN, receives the following parameters:string $namechar- The equivalent ofCHAR, receives the following parameters:string $name, int $length = 100date- The equivalent ofDATE, receives the following parameters:string $namedatetime- The equivalent ofDATETIME, receives the following parameters:string $namedecimal- The equivalent ofDECIMAL, receives the following parameters:string $name, int $precision = 5, int $scale = 2double- The equivalent ofDOUBLE, receives the following parameters:string $name, int $precision = 15enum- The equivalent ofENUM, receives the following parameters:string $name, array $options = []float- The equivalent ofFLOAT, receives the following parameters:string $nameinteger- The equivalent ofINT, receives the following parameters:string $namejson- The equivalent ofJSON, receives the following parameters:string $namelongText- The equivalent ofLONGTEXT, receives the following parameters:string $namemediumInteger- The equivalent ofMEDIUMINT, receives the following parameters:string $namemediumText- The equivalent ofMEDIUMTEXT, receives the following parameters:string $namesmallInteger- The equivalent ofSMALLINT, receives the following parameters:string $nametinyInteger- The equivalent ofTINYINT, receives the following parameters:string $namestring- The equivalent ofSTRING, receives the following parameters:string $name, int $length = 100text- The equivalent ofTEXT, receives the following parameters:string $nametime- The equivalent ofTIME, receives the following parameters:string $nametimestamp- The equivalent ofTIMESTAMP, receives the following parameters:string $name
Also there are the following key types:
index- The equivalent ofINDEXprimary- The equivalent ofPRIMARY KEYunique- The equivalent ofUNIQUE INDEXforeign- The equivalent ofFOREIGN KEY
All of the key methods receive $name, an string and $columns, an string|array<string> specifying the columns that make up the key.
Altering tables
To modify a table use the table method:
$schema->table('users', function(Table $table) {
$table->index('pk')->name('pk_id_email')->columns(['id', 'email']);
$table->dropIndex('pk_id');
$table->renameColumn('login', 'email');
$table->datetime('modified')->after('created')->nullable();
$table->string('type')->modify()->default('Subscriber')->nullable();
$table->dropColumn('permissions');
});
Similar to the create method, it receives the table name and a Closure that will be called to perform the modifications.
You can rename and drop columns, and drop keys using the following methods:
renameColumn(string $from, string $to)- Rename a column, pass the current name and the new onedropColumn(string $name)- Drop a column, just pass its namedropIndex(string $name)- Drop an index by namedropPrimary(string $name)- Drop a primary key by namedropUnique(string $name)- Drop a unique key by namedropForeign(string $name)- Drop a foreign key by name
Migrations
Migrations are a powerfull tool to deploy your projects and to ensure a consistent database structure is always available.
To begin, you create a migration file, using the following naming convention:
<date(YYYYMMDD)>_<time(HHMMSS)>-<migration_name>.php
For example, create a 20230615_162311-CreateFileTable.php file and put the following inside:
<?php
declare(strict_types = 1);
namespace App\Database\Migrations;
use Caldera\Database\Migrations\AbstractMigration;
use Caldera\Database\Schema\Schema;
use Caldera\Database\Schema\Table;
class CreateFileTable extends AbstractMigration {
/**
* Migration up
* @return bool
*/
public function up(): bool {
$schema = new Schema($this->database);
$schema->createIfNotExists('file', function(Table $table) {
$table->bigInteger('id');
$table->string('name');
$table->string('type');
$table->string('status');
$table->datetime('created');
$table->datetime('updated');
$table->primary('pk_id', 'id');
});
return true;
}
/**
* Migration down
* @return bool
*/
public function down(): bool {
$schema = new Schema($this->database);
$schema->dropIfExists('file');
return true;
}
}
As you can see there are two important methods:
up- Executed when the migration is applieddown- Executed when the migration is rolled back
Usually you do changes in the up method and undo them on the down one.
Working with migrations
Now to actually execute the migrations you will need the Migrations class.
This class is commonly accesed through framework means, for example, via a CLI command. If you are not using a framework you can still call the methods directly, just create an instance of the Migrations class:
use Caldera\Database\Migrations\Migrations;
$migrations = new Migrations($database);
$migrations->setup();
You will need to pass the current $database instance, so it's recommended to use a container implementation for dependency injection.
Also note the call to the setup method: it ensures that the required tables are already in place, otherwise creates them.
The first thing you will need to do is add a folder to the migrations path, using the path method:
$migrations->path( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'migrations' );
You can add several folders to separate and modularize your applications logic.
To execute all the pending migrations, use the migrate method:
$migrations->migrate();
Or you can use the status method to check which migrations have been applied and which ones are pending:
$migrations->status();
In cases where you want to undo the last applied changes you can use the rollback method:
$migrations->rollback();
By default, it will undo the last set of migrations executed, that is, all the migrations that ran on the last call of the migrate method; bu you can also specify a number of steps, for example:
$migrations->rollback(2);
Will undo the last two sets of migrations. You can also pass -1 to undo all the executed migrations so far.
And if you need to nuke the entire database and then rebuild it, use the reset method:
$migrations->reset();
This is the same as calling $migrations->rollback(-1) and then $migrations->migrate().
Seeding
Seeding allows you to quickly populate the database with data, and together with migrations are essential parts of the deployment process.
To begin, you create a Seeder, in this case naming is not so important because seeds are executed on demand, so we can create a TermSeeder class for example:
<?php
declare(strict_types = 1);
namespace App\Database\Seeds;
use Caldera\Database\Seeds\AbstractSeeder;
class TermSeeder extends AbstractSeeder {
/**
* Run seeder
* @return bool
*/
public function run(): bool {
# TBD: Using $this->database insert some rows into the TERMS table for example
return true;
}
}
By extending the AbstractSeeder class you make sure that the class has a $database property, so in the run method you can use $this->database to run queries against the database.
You may also load the data from a JSON or CSV file, query a webservice or API, etc.
Working with seeds
Similar to migrations, you interact with seeders through the Seeds class:
use Caldera\Database\Seeds\Seeds;
$seeds = new Seeds($database);
You pass the current $database instance as with the Migrations class;
Again, you add some paths where your seeders reside:
$seeds->path( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Seeds' );
And execute an specific seeder with the seed method:
$seeds->seed(TermSeeder::class);