Skip to content

Database

Foundation Database provides a configured Doctrine DBAL connection, convenient application tables, and reliable WordPress transactions. Use Foundation’s fluent queries for ordinary reads, joins, and writes. Use the shared Doctrine connection directly for transactions, specialized SQL, and native database operations.

Install the runtime package:

composer require stellarwp/foundation-database

Foundation requires PHP 8.3, mysqli, and Doctrine DBAL 4.5.x (~4.5.0).

Database services borrow the active WordPress connection. Use the primary MySQL or MariaDB database and InnoDB tables for transactional application data.

Set a stable, unique application prefix in your root configuration:

<?php declare(strict_types=1);

return [
	'foundation' => [
		'root'   => __DIR__,
		'prefix' => $_ENV['FOUNDATION_PREFIX'] ?? 'your-plugin',
	],
];

After creating the application container, register the database provider before application providers in src/App.php:

use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Database\DatabaseProvider;

/** @var list<class-string<Provider>> */
private const array PROVIDERS = [
	DatabaseProvider::class,
];

DatabaseProvider supplies one shared Doctrine\DBAL\Connection. Tables and repositories resolved from the container participate in that connection’s transactions. Registration is lazy: it creates no database tables. Register an application provider afterward when you need custom service bindings.

Application tables declare their stable name and inherit their infrastructure constructor:

In src/Database/Tables/Reports_Table.php:

<?php declare(strict_types=1);

namespace Plugin\Database\Tables;

use StellarWP\Foundation\Database\Table\Table;

final readonly class Reports_Table extends Table {

	public function unprefixedName(): string {
		return 'your_plugin_reports';
	}
}

Inject Reports_Table into your service and call its operations:

$id = $this->reports->insertGetId( [
	'title' => 'Weekly report',
] );

$report = $this->reports->query()
	->select( 'id', 'title' )
	->where( 'id', $id )
	->first();

Create the table through a migration, then use the query and transaction guide for results, writes, and failure handling.

Table is a supported base class. Inherit its constructor and implement unprefixedName(); application query methods may live on the subclass or in a composed repository. Foundation preserves supported behavior and subclass compatibility within a release line, including when considering new base-class methods.

A shared container may be reused after switch_to_blog() between complete operations. Tables resolve the active prefix when called; a query already built retains its original table name. Build a fresh query after switching sites. Run migrations separately for each site, for example wp --url=https://site.example your-plugin migrate:run.

Transactions and migrations capture their starting scope and reject detected site or connection changes. Never switch sites, even temporarily, during those operations.

Applications can replace DatabaseScope and TableNameResolver when they own a different naming policy. Register replacements after DatabaseProvider, before resolving database services. Replacing the Doctrine connection also requires preserving Foundation’s documented transaction and migration guarantees, including terminal failure tracking and acknowledged commits. When using Foundation Migrations, a replacement must also supply a coherent Database\Contracts\AdvisorySession using the same connection: every execution must check ownership and retain terminal failures. Registering an unrelated lock implementation cannot supply those guarantees. A plain DBAL connection alone does not provide those guarantees.

Database\Contracts\AdvisorySession is the supported integration for running work under a database-session advisory lock. DatabaseProvider binds it to the same session used by the managed DBAL connection. Foundation Migrations uses it automatically; ordinary table and transaction consumers continue using their existing APIs.

Implementations acquire once without waiting, retain the original site and connection across DDL and nested transactions, check ownership before execution and completion, and preserve a caught database failure until the locked operation ends. An escaping callback exception takes precedence over release failure. Connection changes must never reconnect or replay protected work. Contention raises AdvisoryLockContended; failed or uncertain ownership raises AdvisoryLockInterrupted.

Database owns the supported DBAL constraint. When upgrading it, verify the migration schema APIs and failure-handling tests as well as queries and transactions. Foundation Migrations requires a compatible Database release; applications should update their Composer lock file and run their migration tests before deployment.

Contribute Doctrine driver middleware through DatabaseProvider::MIDDLEWARE to add logging or instrumentation to the shared connection. Foundation keeps its WordPress connection integration installed.

For example, Doctrine’s supplied logging middleware sends query and transaction events to a PSR logger. In src/Database/Query_Logging_Provider.php:

<?php declare( strict_types=1 );

namespace Plugin\Database;

use Doctrine\DBAL\Logging\Middleware;
use Psr\Log\LoggerInterface;
use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Container\Contracts\Resolver as C;
use StellarWP\Foundation\Database\DatabaseProvider;

final class Query_Logging_Provider extends Provider {

	/**
	 * Send database events to the application's logger.
	 */
	public function register(): void {
		$this->container->mergeArrayVar(
			DatabaseProvider::MIDDLEWARE,
			static fn ( C $container ): array => [
				new Middleware( $container->get( LoggerInterface::class ) ),
			],
		);
	}
}

Register the providers in dependency order in src/App.php:

use Plugin\Database\Query_Logging_Provider;
use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Database\DatabaseProvider;
use StellarWP\Foundation\Log\LogProvider;

/** @var list<class-string<Provider>> */
private const array PROVIDERS = [
	DatabaseProvider::class,
	LogProvider::class,
	Query_Logging_Provider::class,
];

Configure your logging handler to accept debug records for SQL execution events. The supplied middleware includes SQL and bound parameter values, which may contain sensitive application data. Enable it only where that logging is appropriate. It does not update $wpdb->num_queries or integrate with Query Monitor.

Contribute middleware objects in an array, or a lazy factory returning that array as shown above. Register all contributions before first resolving Doctrine\DBAL\Connection, including through tables or repositories. Foundation resolves the factories when constructing that shared connection; registering them alone does not open the database. Adding contributions afterward does not reconfigure an existing connection.

Foundation’s WordPress middleware is applied first. Application middleware is applied in contribution order, with each wrapping the previous driver. For contributions [A, B], calls enter B, then A, then Foundation’s WordPress driver. Your middleware must delegate to the wrapped driver and preserve exceptions, results, and transaction behavior; opening a separate connection or retrying failed work would violate the shared connection’s guarantees.