Skip to content

Commit

Permalink
pkp#2493 Introduce migrations-based schema tools
Browse files Browse the repository at this point in the history
  • Loading branch information
asmecher committed May 9, 2020
1 parent 46d80b4 commit 1cd857b
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 55 deletions.
30 changes: 30 additions & 0 deletions classes/core/PKPApplication.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
define('WORKFLOW_TYPE_EDITORIAL', 'editorial');
define('WORKFLOW_TYPE_AUTHOR', 'author');

use Illuminate\Database\Capsule\Manager as Capsule;

interface iPKPApplicationInfoProvider {
/**
* Get the top-level context DAO.
Expand Down Expand Up @@ -196,6 +198,34 @@ public function __construct() {
fatalError('Database connection failed!');
}
}

// Map valid config options to Illuminate database drivers
$driver = strtolower(Config::getVar('database', 'driver'));
if (substr($driver, 0, 8) === 'postgres') {
$driver = 'pgsql';
} else {
$driver = 'mysql';
}

// Always use `utf8` unless `latin1` is specified
$charset = Config::getVar('i18n', 'connection_charset');
if ($charset !== 'latin1') {
$charset = 'utf8';
}

$capsule = new Capsule;
$capsule->addConnection([
'driver' => $driver,
'host' => Config::getVar('database', 'host'),
'database' => Config::getVar('database', 'name'),
'username' => Config::getVar('database', 'username'),
'port' => Config::getVar('database', 'port'),
'unix_socket'=> Config::getVar('database', 'unix_socket'),
'password' => Config::getVar('database', 'password'),
'charset' => $charset,
'collation' => 'utf8_general_ci',
]);
$capsule->setAsGlobal();
}

// Register custom autoloader functions for namespaces
Expand Down
32 changes: 22 additions & 10 deletions classes/install/Installer.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -359,19 +359,31 @@ function addInstallAction($node) {
function executeAction($action) {
switch ($action['type']) {
case 'schema':
$fileName = $action['file'];
$this->log(sprintf('schema: %s', $action['file']));
// Schema information can be specified in two ways:
if (isset($action['file'])) {
// 1) ADODB's XMLSchema toolset...
$fileName = $action['file'];
$this->log(sprintf('schema: %s', $action['file']));

$schemaXMLParser = new adoSchema($this->dbconn);
$dict = $schemaXMLParser->dict;
$sql = $schemaXMLParser->parseSchema($fileName);
$schemaXMLParser->destroy();
$schemaXMLParser = new adoSchema($this->dbconn);
$dict = $schemaXMLParser->dict;
$sql = $schemaXMLParser->parseSchema($fileName);
$schemaXMLParser->destroy();

if ($sql) {
return $this->executeSQL($sql);
if ($sql) {
return $this->executeSQL($sql);
} else {
$this->setError(INSTALLER_ERROR_DB, str_replace('{$file}', $fileName, __('installer.installParseDBFileError')));
return false;
}
} else {
$this->setError(INSTALLER_ERROR_DB, str_replace('{$file}', $fileName, __('installer.installParseDBFileError')));
return false;
// 2) Laravel's schema toolset
assert(isset($action['attr']['class']));
$fullClassName = $action['attr']['class'];
import($fullClassName);
$shortClassName = substr($fullClassName, strrpos($fullClassName, '.')+1);
$schema = new $shortClassName();
$schema->up();
}
break;
case 'data':
Expand Down
26 changes: 24 additions & 2 deletions classes/install/PKPInstall.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import('lib.pkp.classes.install.Installer');
import('classes.core.Services');

use Illuminate\Database\Capsule\Manager as Capsule;

class PKPInstall extends Installer {

/**
Expand Down Expand Up @@ -68,13 +70,13 @@ function preInstall() {

// Connect to database
$conn = new DBConnection(
$this->getParam('databaseDriver'),
$driver = $this->getParam('databaseDriver'),
$this->getParam('databaseHost'),
$this->getParam('databaseUsername'),
$this->getParam('databasePassword'),
$this->getParam('createDatabase') ? null : $this->getParam('databaseName'),
false,
$this->getParam('connectionCharset') == '' ? false : $this->getParam('connectionCharset')
$connectionCharset = $this->getParam('connectionCharset') == '' ? false : $this->getParam('connectionCharset')
);

$this->dbconn =& $conn->getDBConn();
Expand All @@ -84,6 +86,26 @@ function preInstall() {
return false;
}

// Map valid config options to Illuminate database drivers
$driver = strtolower($driver);
if (substr($driver, 0, 8) === 'postgres') {
$driver = 'pgsql';
} else {
$driver = 'mysql';
}

$capsule = new Capsule;
$capsule->addConnection([
'driver' => $driver,
'host' => $this->getParam('databaseHost'),
'database' => $this->getParam('databaseName'),
'username' => $this->getParam('databaseUsername'),
'password' => $this->getParam('databasePassword'),
'charset' => $connectionCharset == 'latin1'?'latin1':'utf8',
'collation' => 'utf8_general_ci',
]);
$capsule->setAsGlobal();

DBConnection::getInstance($conn);

return parent::preInstall();
Expand Down
42 changes: 0 additions & 42 deletions classes/services/queryBuilders/BaseQueryBuilder.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,10 @@

namespace PKP\Services\QueryBuilders;

use Illuminate\Database\Capsule\Manager as Capsule;
use \Config;

abstract class BaseQueryBuilder {

/** @var object capsule */
protected $capsule = null;

/**
* Constructor
*/
public function __construct() {
$this->bootstrap();
}

/**
* bootstrap query builder
*/
protected function bootstrap() {

// Map valid config options to Illuminate database drivers
$driver = strtolower(Config::getVar('database', 'driver'));
if (substr($driver, 0, 8) === 'postgres') {
$driver = 'pgsql';
} else {
$driver = 'mysql';
}

// Always use `utf8` unless `latin1` is specified
$charset = Config::getVar('i18n', 'connection_charset');
if ($charset !== 'latin1') {
$charset = 'utf8';
}

$capsule = new Capsule;
$capsule->addConnection(array(
'driver' => $driver,
'host' => Config::getVar('database', 'host'),
'database' => Config::getVar('database', 'name'),
'username' => Config::getVar('database', 'username'),
'port' => Config::getVar('database', 'port'),
'unix_socket'=> Config::getVar('database', 'unix_socket'),
'password' => Config::getVar('database', 'password'),
'charset' => $charset,
'collation' => 'utf8_general_ci',
));
$capsule->setAsGlobal();
}
}
4 changes: 3 additions & 1 deletion dtd/install.dtd
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
maxversion CDATA #IMPLIED>
<!ELEMENT schema EMPTY>
<!ATTLIST schema
file CDATA #REQUIRED>
file CDATA #IMPLIED>
<!ATTLIST schema
class CDATA #IMPLIED>
<!ELEMENT data EMPTY>
<!ATTLIST data
file CDATA #REQUIRED
Expand Down
176 changes: 176 additions & 0 deletions tools/xmlSchemaToMigration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php

/**
* @file tools/xmlSchemaToMigration.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2003-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class xmlSchemaToMigration
* @ingroup tools
*/

require(dirname(dirname(dirname(dirname(__FILE__)))) . '/tools/bootstrap.inc.php');

class xmlSchemaToMigration extends CommandLineTool {
/** @var string Name of source file/directory */
protected $source;

/** @var string Name of the generated PHP class */
protected $className;

/**
* Constructor
*/
function __construct($argv = array()) {
parent::__construct($argv);

array_shift($argv); // Shift the tool name off the top

$this->source = array_shift($argv);
$this->className = array_shift($argv);

// The source file/directory must be specified and exist.
if (empty($this->source) || !file_exists($this->source)) {
$this->usage();
exit(2);
}

if (empty($this->className) | !preg_match('/^[a-zA-Z]+$/', $this->className)) {
$this->usage();
exit(3);
}
}

/**
* Print command usage information.
*/
function usage() {
echo "Script to convert ADODB XMLSchema-based schema descriptors to Illuminate migrations\n\n"
. "Usage: {$this->scriptName} input-schema-file.xml GenerateClassNamed\n\n";
}

/**
* Convert XML locale content to PO format.
*/
function execute() {
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->loadXML(file_get_contents($this->source));
if ($doc->documentElement->nodeName != 'schema') throw new Exception('Invalid document element ' . $this->documentElement->nodeName);

echo "<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Builder;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Capsule\Manager as Capsule;
class $this->className extends Migration {
/**
* Run the migrations.
* @return void
*/
public function up() {\n";
foreach ($doc->documentElement->childNodes as $tableNode) {
if ($tableNode->nodeType == XML_COMMENT_NODE) continue; // Skip comments
if ($tableNode->nodeName != 'table') throw new Exception('Unexpected table node name ' . $tableNode->nodeName);
foreach ($tableNode->childNodes as $tableChild) {
if ($tableChild->nodeName == 'descr') echo "\t\t// " . $tableChild->nodeValue . "\n";
}
echo "\t\tCapsule::schema()->create('" . $tableNode->getAttribute('name') . "', function (Blueprint \$table) {\n";
foreach ($tableNode->childNodes as $tableChild) switch (true) {
case $tableChild->nodeType == XML_COMMENT_NODE: throw new Exception('Unexpected comment!');
case $tableChild->nodeName == 'field':
// Preprocess comments
foreach ($tableChild->childNodes as $fieldChild) switch (true) {
case $fieldChild->nodeType == XML_COMMENT_NODE:
echo "\t\t\t// " . $fieldChild->nodeValue . "\n";
break;
}
echo "\t\t\t";
switch ($tableChild->getAttribute('type')) {
case 'X':
echo "\$table->text('" . $tableChild->getAttribute('name') . "')";
break;
case 'I1':
echo "\$table->boolean('" . $tableChild->getAttribute('name') . "')";
break;
case 'I2':
echo "\$table->smallInteger('" . $tableChild->getAttribute('name') . "')";
break;
case 'I4':
echo "\$table->integer('" . $tableChild->getAttribute('name') . "')";
break;
case 'I8':
echo "\$table->bigInteger('" . $tableChild->getAttribute('name') . "')";
break;
case 'F':
echo "\$table->float('" . $tableChild->getAttribute('name') . "', 8, 2)";
break;
case 'T':
echo "\$table->timestamp('" . $tableChild->getAttribute('name') . "')";
break;
case 'D':
echo "\$table->date('" . $tableChild->getAttribute('name') . "')";
break;
case 'C':
echo "\$table->char('" . $tableChild->getAttribute('name') . "', " . (int) $tableChild->getAttribute('size') . ")";
break;
case 'C2':
echo "\$table->string('" . $tableChild->getAttribute('name') . "', " . (int) $tableChild->getAttribute('size') . ")";
break;
default: throw new Exception('Unspecified or unknown table type ' . $tableChild->getAttribute('type') . ' in column ' . $tableChild->getAttribute('name') . ' of table ' . $tableNode->getAttribute('name'));
}
$nullable = true;
$autoIncrement = false;
$keys = [];
foreach ($tableChild->childNodes as $fieldChild) switch (true) {
case $fieldChild->nodeType == XML_COMMENT_NODE: break; // Already processed above
case $fieldChild->nodeName == 'NOTNULL': $nullable = false; break;
case $fieldChild->nodeName == 'AUTOINCREMENT': $autoIncrement = true; $nullable = false; break;
case $fieldChild->nodeName == 'KEY': $keys[] = $tableChild->getAttribute('name'); break;
case $fieldChild->nodeName == 'DEFAULT': echo "->default(" . var_export($fieldChild->getAttribute('VALUE'), true) . ")"; break;
case $fieldChild->nodeName == 'descr': echo "->comment(" . var_export($fieldChild->nodeValue, true) . ")"; break;
case $fieldChild->nodeType == XML_TEXT_NODE:
if (trim($fieldChild->nodeValue) !== '') throw new Exception('Unexpected content in field node!');
break;
default: throw new Exception('Unhandled child node (type ' . $fieldChild->nodeType . ') to column ' . $tableChild->getAttribute('name') . ' of table ' . $tableNode->getAttribute('name'));
}
if ($autoIncrement) echo "->autoIncrement()";
if ($nullable) echo "->nullable()";
echo ";\n";
if (!empty($keys)) {
echo "\t\t\t\$table->unique(['" . implode("', '", $keys) . "']);\n";
}
break;
case $tableChild->nodeName == 'index':
if (!$tableChild->hasAttribute('name')) throw new Exception('Unnamed index on table ' . $tableNode->getAttribute('name'));
$indexType = 'index';
$columns = [];
foreach ($tableChild->childNodes as $indexChild) switch (true) {
case $indexChild->nodeType == XML_COMMENT_NODE:
echo "\t\t\t// " . $indexChild->nodeValue . "\n";
break;
case $indexChild->nodeName == 'UNIQUE': $indexType = 'unique'; break;
case $indexChild->nodeName == 'col': $columns[] = trim($indexChild->nodeValue);
break;
default: throw new Exception('Unhandled index node child ' . $indexChild->nodeName);
}
if (empty($columns)) throw new Exception('Empty column list for index on table ' . $tableNode->getAttribute('name') . '))!');
echo "\t\t\t\$table->$indexType(['" . implode("', '", $columns) . "'], '" . $tableChild->getAttribute('name') . "');\n";
break;
case $tableChild->nodeName == 'descr': break; // Handled above.
break;
default: throw new Exception('Don\'t know how to handle this table child node (' . $tableChild->nodeName . '))!');
}
echo "\t\t});\n\n";
}
echo "\t}\n}";
}
}

$tool = new xmlSchemaToMigration(isset($argv) ? $argv : array());
$tool->execute();

0 comments on commit 1cd857b

Please sign in to comment.