Skip to content

Commit

Permalink
Explore: Setup SQLite database integration without creating wp-conten…
Browse files Browse the repository at this point in the history
…t/db.php (#1382)

Related to #1379

Loads the SQLite database integration without creating a drop-in
`wp-content/db.php` plugin. Instead, it leans on the `auto_prepend_file`
PHP option to provide a fake global `$wpdb` variable. This prevents
WordPress from trying to connect to MySQL. The first time that `$wpdb`
is used for reading or a method call, it loads the actual SQLite
database integration.

## Follow-up work

Use the same SQLite setup method in the web version of Playground. This
could happen either when a non-minified WordPress build is used or, for
simplicity, always if we remove the SQLite integration plugin from the
minified build.

## Testing instructions

Run `bun packages/playground/cli/src/cli.ts server --login` and confirm
the server starts without any database errors.

Test coverage coming as a part of the larger boot protocol discussion,
see #1379.
  • Loading branch information
adamziel authored May 14, 2024
1 parent 7531696 commit 0b05edf
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 39 deletions.
40 changes: 8 additions & 32 deletions packages/playground/cli/src/setup-wp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
readAsFile,
} from './download';
import { withPHPIniValues } from './setup-php';
import { playgroundMuPlugin } from '@wp-playground/wordpress';
import {
playgroundMuPlugin,
preloadSqliteIntegration,
} from '@wp-playground/wordpress';

/**
* Ensures a functional WordPress installation in php document root.
Expand Down Expand Up @@ -49,7 +52,9 @@ export async function setupWordPress(
monitor
),
]);
await prepareWordPress(php, wpZip, sqliteZip);

await prepareWordPress(php, wpZip);
await preloadSqliteIntegration(php, sqliteZip);

const preinstalledWpContentPath = path.join(
CACHE_FOLDER,
Expand Down Expand Up @@ -105,7 +110,7 @@ export async function setupWordPress(
* accept the limitation, and switch to the PHP implementation as soon
* as that's viable.
*/
async function prepareWordPress(php: NodePHP, wpZip: File, sqliteZip: File) {
async function prepareWordPress(php: NodePHP, wpZip: File) {
php.mkdir('/internal/shared/mu-plugins');
php.writeFile(
'/internal/shared/mu-plugins/0-playground.php',
Expand All @@ -128,33 +133,4 @@ async function prepareWordPress(php: NodePHP, wpZip: File, sqliteZip: File) {
'/wordpress/wp-config.php',
php.readFileAsText('/wordpress/wp-config-sample.php')
);
// }}}

// Setup the SQLite integration {{{
php.mkdir('/tmp/sqlite-database-integration');
await unzip(php, {
zipFile: sqliteZip,
extractToPath: '/tmp/sqlite-database-integration',
});
php.mv(
'/tmp/sqlite-database-integration/sqlite-database-integration-main',
'/internal/shared/mu-plugins/sqlite-database-integration'
);

const dbPhp = php
.readFileAsText(
'/internal/shared/mu-plugins/sqlite-database-integration/db.copy'
)
.replace(
"'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'",
"'/internal/shared/mu-plugins/sqlite-database-integration/'"
)
.replace(
"'{SQLITE_PLUGIN}'",
"'/internal/shared/mu-plugins/sqlite-database-integration/load.php'"
);
// @TODO do not create the db.php file. Either find a way to mount it, or
// load the custom $wpdb object in a different way.
php.writeFile('/wordpress/wp-content/db.php', dbPhp);
// }}}
}
3 changes: 0 additions & 3 deletions packages/playground/wordpress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,5 @@
"publishConfig": {
"access": "public",
"directory": "../../../dist/packages/playground/wordpress"
},
"dependencies": {
"@php-wasm/universal": "^0.6.6"
}
}
5 changes: 1 addition & 4 deletions packages/playground/wordpress/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"packages/playground/wordpress/**/*.ts",
"packages/playground/wordpress/package.json"
]
"lintFilePatterns": ["packages/playground/wordpress/**/*.ts"]
}
}
},
Expand Down
112 changes: 112 additions & 0 deletions packages/playground/wordpress/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { UniversalPHP } from '@php-wasm/universal';
import { joinPaths, phpVar } from '@php-wasm/util';
import { unzip } from '@wp-playground/blueprints';

export * from './rewrite-rules';

export const RecommendedPHPVersion = '8.0';
Expand Down Expand Up @@ -105,3 +109,111 @@ export const specificMuPlugins = {
export const playgroundMuPlugin = Object.values(specificMuPlugins)
.map((p) => p.trim())
.join('\n');

export async function preloadSqliteIntegration(
php: UniversalPHP,
sqliteZip: File
) {
if (await php.isDir('/tmp/sqlite-database-integration')) {
await php.rmdir('/tmp/sqlite-database-integration', {
recursive: true,
});
}
await php.mkdir('/tmp/sqlite-database-integration');
await unzip(php, {
zipFile: sqliteZip,
extractToPath: '/tmp/sqlite-database-integration',
});
const SQLITE_PLUGIN_FOLDER = '/internal/shared/sqlite-database-integration';
await php.mv(
'/tmp/sqlite-database-integration/sqlite-database-integration-main',
SQLITE_PLUGIN_FOLDER
);
const dbCopy = await php.readFileAsText(
joinPaths(SQLITE_PLUGIN_FOLDER, 'db.copy')
);
const dbPhp = dbCopy
.replace(
"'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'",
phpVar(SQLITE_PLUGIN_FOLDER)
)
.replace(
"'{SQLITE_PLUGIN}'",
phpVar(joinPaths(SQLITE_PLUGIN_FOLDER, 'load.php'))
);
const SQLITE_MUPLUGIN_PATH =
'/internal/shared/mu-plugins/sqlite-database-integration.php';
await php.writeFile(SQLITE_MUPLUGIN_PATH, dbPhp);
await php.writeFile(
`/internal/shared/preload/0-sqlite.php`,
`<?php
/**
* Loads the SQLite integration plugin before WordPress is loaded
* and without creating a drop-in "db.php" file.
*
* Technically, it creates a global $wpdb object whose only two
* purposes are to:
*
* * Exist – because the require_wp_db() WordPress function won't
* connect to MySQL if $wpdb is already set.
* * Load the SQLite integration plugin the first time it's used
* and replace the global $wpdb reference with the SQLite one.
*
* This lets Playground keep the WordPress installation clean and
* solves dillemas like:
*
* * Should we include db.php in Playground exports?
* * Should we remove db.php from Playground imports?
* * How should we treat stale db.php from long-lived OPFS sites?
*
* @see https://github.com/WordPress/wordpress-playground/discussions/1379 for
* more context.
*/
class Playground_SQLite_Integration_Loader {
public function __call($name, $arguments) {
$this->load_sqlite_integration();
if($GLOBALS['wpdb'] === $this) {
throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded');
}
return call_user_func_array(
array($GLOBALS['wpdb'], $name),
$arguments
);
}
public function __get($name) {
$this->load_sqlite_integration();
if($GLOBALS['wpdb'] === $this) {
throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded');
}
return $GLOBALS['wpdb']->$name;
}
public function __set($name, $value) {
$this->load_sqlite_integration();
if($GLOBALS['wpdb'] === $this) {
throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded');
}
$GLOBALS['wpdb']->$name = $value;
}
protected function load_sqlite_integration() {
require_once ${phpVar(SQLITE_MUPLUGIN_PATH)};
}
}
$wpdb = $GLOBALS['wpdb'] = new Playground_SQLite_Integration_Loader();
`
);
/**
* Ensure the SQLite integration is loaded and clearly communicate
* if it isn't. This is useful because WordPress database errors
* may be cryptic and won't mention the SQLite integration.
*/
await php.writeFile(
`/internal/shared/mu-plugins/sqlite-test.php`,
`<?php
global $wpdb;
if(!($wpdb instanceof WP_SQLite_DB)) {
var_dump(isset($wpdb));
die("SQLite integration not loaded " . get_class($wpdb));
}
`
);
}

0 comments on commit 0b05edf

Please sign in to comment.