nur-sery/nur_src/m/pdo/mysql/MysqlMigrations.php

1121 lines
38 KiB
PHP

<?php
namespace nur\m\pdo\mysql;
use Exception;
use nur\A;
use nur\b\ValueException;
use nur\base;
use nur\func;
use nur\m\base\AbstractRow;
use nur\md;
use nur\php\SrcGenerator;
use nur\str;
use ReflectionClass;
/**
* Class MysqlMigrations
*
* Dans la classe destination (celle à partir de laquelle sont générées les
* migrations), les constantes suivantes peuvent être définies:
* - DBTRAIT: classe qui est utilisée comme dbtrait pour *toutes* les migrations
* - DEFAULT_DBTRAIT: classe qui est utilisée comme dbtrait pour les migrations
* qui n'en définissent pas
* - MERGE: une classe XxxMigration qu'il faut aussi considérer
* - MERGES: une liste de classes XxxMigration qu'il faut aussi considérer.
*
* Les constantes MERGE et MERGES sont calculées récursivement. Les constantes
* DBTRAIT et DEFAULT_DBTRAIT ne sont consultées que dans la classe destination.
*/
class MysqlMigrations {
const UPDATE_DIRECTIVES = [
"+name", # nom de base du fichier sql généré
"+desc", # description à insérer dans le fichier sql
"+dbtrait", # trait qui fourni verifix_conn(). Si cette valeur n'est pas fournie, la constante DBTRAIT de la classe destination donne la valeur par défaut
"+prod", # cette configuration a-t-elle été déployée en prod? (ne plus modifier)
];
const TABLE_DIRECTIVES = [
"+name", # nom effectif de la table
"+desc", # description à insérer en commentaire
"+type", # type: table (par défaut) ou view
# directives spécifiques aux tables
"+suffix", # suffixe des colonnes de cette table
"+primary key",
"+foreign key",
"+index",
"+unique index",
# directives spécifiques aux vues
"+view_tables", # tables participant aux vues
"+view_def", # définition de la vue
];
###
private static $trace;
private static function trace($msg) {
if (self::$trace !== null) func::call(self::$trace, $msg);
}
/**
* tester si le fichier doit être écrasé
*
* si $overwrite n'est pas booléen, ce doit être un nombre maximum de ligne au
* delà duquel le fichier n'est pas écrasé
*/
private static function should_overwrite($file, $overwrite) {
if (!is_int($overwrite)) $overwrite = boolval($overwrite);
if ($overwrite === true || $overwrite === false) return $overwrite;
if (!is_file($file)) return true;
$inf = fopen($file, "rb");
try {
while ($overwrite > 0) {
$line = fgets($inf);
if ($line === false) return true;
$overwrite--;
}
$line = fgets($inf);
return $line === false || $line === "";
} finally {
fclose($inf);
}
}
private static function get_ovsuf($file, $overwrite) {
if (is_file($file)) {
if ($overwrite) return " (OVERWRITE)";
else return "";
} else return " (NEW)";
}
private static function write($line, $w, $tmpf) {
$r = fwrite($w, $line);
if ($r === false) {
fclose($w);
throw new Exception("$tmpf: erreur lors de l'écriture du fichier");
}
}
private static function write_lines($lines, $outf, $overwrite=true, $final_nl=true) {
if (is_file($outf) && !$overwrite) {
return false;
}
$dir = dirname($outf);
if (!is_dir($dir)) {
$r = mkdir($dir, 0777, true);
if ($r === false) {
throw new Exception("$dir: erreur lors de la création du répertoire");
}
}
$tmpf = "$outf.tmp";
$w = fopen($tmpf, "w+b");
if ($w === false) {
throw new Exception("$tmpf: erreur d'ouverture du fichier");
}
$first = true;
foreach ($lines as $line) {
if ($first) $first = false;
else self::write("\n", $w, $tmpf);
self::write($line, $w, $tmpf);
}
if ($final_nl) self::write("\n", $w, $tmpf);
$r = fclose($w);
if ($r === false) {
throw new Exception("$tmpf: erreur lors de la fermeture du fichier");
}
$r = rename($tmpf, $outf);
if ($r === false) {
throw new Exception("$outf: erreur lors du renommage du fichier");
}
return true;
}
###
private static function is_update(string $method) {
return str::starts_with("update", $method);
}
private static function get_index(string $method) {
return str::without_prefix("update", $method);
}
private static function update_methods(array &$methods, string $class): void {
$all_methods = get_class_methods($class);
if ($all_methods === null) {
throw new Exception("$class: classe introuvable");
}
$prefix = str_replace("\\", "_", $class);
foreach ($all_methods as $method) {
if (!self::is_update($method)) continue;
$index = self::get_index($method);
$name = "${prefix}_$index";
$methods[$name] = [
"class" => $class,
"name" => $method,
"index" => $index,
"method" => [$class, $method],
"prefix" => $prefix,
];
}
$merge_classes = [];
$c = new ReflectionClass($class);
$merges = $c->getConstant("MERGES");
if (base::nz($merges)) A::merge($merge_classes, $merges);
$merge = $c->getConstant("MERGE");
if (base::nz($merge)) $merge_classes[] = $merge;
foreach ($merge_classes as $class) {
self::update_methods($methods, $class);
}
}
private static function get_methods($class) {
$methods = [];
self::update_methods($methods, $class);
ksort($methods, SORT_NATURAL);
return $methods;
}
###
private static function parse_col($def): array {
if (preg_match('/^([a-zA-Z0-9_]+)(?:\((\d+)\s*(?:,\s*(\d+))?\))?\s*(.*)\s*(?:--\s*(.*)\s*)?(?:--\s*(.*)\s*)?$/', $def, $ms)) {
$size = base::vn(A::get($ms, 2));
if ($size !== null) $size = intval($size);
$precision = base::vn(A::get($ms, 3));
if ($precision !== null) $precision = intval($precision);
return [
"type" => A::get($ms, 1),
"size" => $size,
"precision" => $precision,
"options" => base::vn(A::get($ms, 4)),
"title" => base::vn(A::get($ms, 5)),
"desc" => base::vn(A::get($ms, 6)),
];
} else {
throw new ValueException("erreur de syntaxe: $def");
}
}
private static function is_multi_fkdef($def, &$ms) {
return preg_match('/^\((.+)\)$/', $def, $ms);
}
private static function is_single_fkref($ref, &$ms) {
return preg_match('/^([a-zA-Z0-9_]+)\(([a-zA-Z0-9_]+)\)$/', $ref, $ms);
}
private static $tables = [];
private static function has_table($table) {
return A::get(self::$tables, $table, false) !== false;
}
private static function has_col($table, $col) {
return (self::has_table($table) &&
A::get(self::$tables[$table], $col, false) !== false);
}
private static function add_table($table, $suffix=null, $dbtrait=null, $name=null): ?array {
if (self::has_table($table)) return null;
if ($name === null) $name = $table;
return self::$tables[$table] = [
"name" => $name,
"suffix" => $suffix,
"cols" => [],
"pk" => null,
"dbtrait" => $dbtrait,
];
}
private static function set_table_pk($table, $pk) {
self::add_table($table);
self::$tables[$table]["pk"] = A::with($pk);
}
private static function add_col($table, $col, $def=null): array {
self::add_table($table);
if ($def !== null) {
[
"type" => $type, "size" => $size, "precision" => $precision,
"options" => $options,
"title" => $title, "desc" => $desc,
] = self::parse_col($def);
#XXX calculer le type du schema: bool si integer(1), integer si integer, etc.
$pk_col = boolval(preg_match('/\bprimary\s+key\b/i', $options));
$auto_col = boolval(preg_match('/\bauto_increment\b/i', $options));
if (str::starts_with("datetime", $type)) {
$hot_type = "date"; #XXX ajouter le support datetime
} elseif (str::starts_with("date", $type)) {
$hot_type = "date";
} elseif (str::starts_with("int", $type)) {
$hot_type = "numeric";
} else {
$hot_type = "text";
}
$cinfos = [
"name" => $col,
"def" => $def,
"pk" => $pk_col,
"auto" => $auto_col,
"hot_type" => $hot_type,
#
"schema" => [
"name" => $col,
"title" => $title,
"desc" => $desc,
"mysql" => [
"type" => $type,
"size" => $size,
"precision" => $precision,
"options" => $options,
"pk" => $pk_col,
"auto" => $auto_col,
],
"hot" => [
"type" => $hot_type,
],
],
];
} else {
$cinfos = [
"name" => $col,
];
}
return self::$tables[$table]["cols"][$col] = $cinfos;
}
private static function del_table($table) {
unset(self::$tables[$table]);
}
private static function del_col($table, $col) {
if (!self::has_table($table)) return;
unset(self::$tables[$table]["cols"][$col]);
}
private static $views = [];
private static function has_view($view) {
return A::get(self::$views, $view, false) !== false;
}
private static function add_view($view, $tables, $dbtrait=null, $name=null) {
if (self::has_view($view)) return;
if ($name === null) $name = $view;
self::$views[$view] = [
"name" => $name,
"tables" => $tables,
"dbtrait" => $dbtrait,
];
}
private static function del_view($view) {
unset(self::$views[$view]);
}
private static $t1links = [];
private static $tmlinks = [];
private static function add_t1link($scol, $stable, $dcol, $dtable) {
self::$t1links[] = [$scol, $stable, $dcol, $dtable];
}
private static function reset($options=null, $tables_only=false) {
self::$tables = [];
self::$views = [];
self::$t1links = [];
self::$tmlinks = [];
if (!$tables_only) {
self::$trace = A::get($options, "trace");
}
}
###
static function gensql($class, $options=null) {
self::reset($options);
$destdir = A::get($options, "destdir", ".");
$overwrite = A::get($options, "overwrite");
$verbose = A::get($options, "verbose");
$prefix = A::get($options, "prefix");
self::trace("# gensql");
self::trace(" class: $class");
self::trace(" destdir: $destdir");
$methods = self::get_methods($class);
foreach ($methods as $minfos) {
self::reset(null, true);
$update = func::call($minfos["method"]);
# par défaut, ne pas écraser les fichiers qui sont en +prod
$prod = A::get($update, "+prod");
$overwrite_sql = $overwrite === null? !$prod: $overwrite;
self::trace(" ## $minfos[class]::$minfos[name]");
$lines = [
"-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8",
"-- fichier autogénéré: toute modification locale risque d'être perdue",
];
$desc = A::get($update, "+desc");
if ($desc) $lines[] = "-- $desc";
foreach ($update as $uname => $uinfos) {
# ignorer les directives de mise à jour: elles ont été traitées ci-dessus
if (str::starts_with("+", $uname)) {
if (!in_array($uname, self::UPDATE_DIRECTIVES)) {
trigger_error("$uname: unknown update directive", E_USER_WARNING);
}
continue;
}
$type = A::get($uinfos, "+type", "table");
if ($type === "table") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$table = $uname;
$table_name = A::get($uinfos, "+name", $table);
$cols = $uinfos;
#####################################################################
# suppression de table
if ($cols === null) {
$lines[] = "drop table $table_name;";
self::del_table($table);
continue;
}
# liste de champs "normale"
if (!self::has_table($table)) {
###################################################################
# nouvelle table
$lines[] = "";
$desc = A::get($cols, "+desc");
if ($desc) $lines[] = "-- $desc";
$lines[] = "create table $table_name (";
$suffix = A::get($cols, "+suffix");
self::add_table($table, $suffix, null, $table_name);
$first = true;
foreach ($cols as $col => $def) {
# les directives seront traitées plus tard
if (str::starts_with("+", $col)) continue;
$lines[] = ($first? " ": ",")."$col $def";
$cinfos = self::add_col($table, $col, $def);
if ($cinfos["pk"]) self::set_table_pk($table, $col);
$first = false;
}
foreach ($cols as $col => $value) {
if (!str::starts_with("+", $col)) continue;
switch ($col) {
case "+primary key":
$pk = A::with($value);
self::set_table_pk($table, $pk);
$pk = implode(", ", $pk);
$lines[] = ",primary key ($pk)";
break;
case "+foreign key":
foreach (A::with($value) as $fk => $ref) {
$ms = null;
if (self::is_multi_fkdef($fk, $ms)) $fk = $ms[1];
$lines[] = ",foreign key ($fk) references $ref";
$ms = null;
if (self::is_single_fkref($ref, $ms)) {
self::add_t1link($fk, $table, $ms[2], $ms[1]);
}
}
break;
case "+index":
foreach (A::with($value) as $name => $cols) {
if (is_numeric($name)) $name = false;
$cols = implode(", ", A::with($cols));
$lines[] = ",index $name($cols)";
}
break;
case "+unique index":
foreach (A::with($value) as $name => $cols) {
if (is_numeric($name)) $name = false;
$cols = implode(", ", A::with($cols));
$lines[] = ",unique index $name($cols)";
}
break;
default:
if (!in_array($col, self::TABLE_DIRECTIVES)) {
trigger_error("$table: $col: unknown table directive", E_USER_WARNING);
}
break;
}
}
$lines[] = ");";
} else {
###################################################################
# maj d'une table existante
$lines[] = "";
$desc = A::get($cols, "+desc");
if ($desc) $lines[] = "-- $desc";
foreach ($cols as $col => $def) {
# les directives seront traitées plus tard
if (!str::starts_with("+", $col)) {
if ($def === null) {
## suppression d'un champ
if (!self::has_col($table, $col)) {
trigger_error("$table.$col: deleting a non-existent column", E_USER_WARNING);
}
$lines[] = "alter table table_name drop column $col;";
self::del_col($table, $col);
} else {
if (self::has_col($table, $col)) {
## maj d'un champ
$lines[] = "alter table table_name change column $col $col $def;";
# NB: pour simplifier, ne pas tenir compte du fait que ce
# nouveau champ pourrait maintenant être une clé primaire
} else {
## ajout d'un champ
$lines[] = "alter table table_name add column $col $def;";
self::add_col($table, $col, $def);
# NB: pour simplifier, ne pas tenir compte du fait que ce
# nouveau champ pourrait être une clé primaire
}
}
} else {
## maj directive
trigger_error("$table: $col: table directive update is not (yet) supported", E_USER_WARNING);
}
}
}
} elseif ($type === "view") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$view = $uname;
$view_name = A::get($uinfos, "+name", $view);
$vinfos = $uinfos;
#####################################################################
# suppression de vue
if ($vinfos === null) {
$lines[] = "drop view $view_name;";
self::del_view($view);
continue;
}
# définition "normale"
if (!self::has_view($view)) {
###################################################################
# nouvelle vue
$tables = A::get($vinfos, "+view_tables");
$def = A::get($vinfos, "+view_def");
if ($def === null) trigger_error("$view: missing +view_def", E_USER_WARNING);
self::add_view($view, $tables, null, $view_name);
$lines[] = "";
$desc = A::get($vinfos, "+desc");
if ($desc) $lines[] = "-- $desc";
$lines[] = "create view $view_name as";
$lines[] = $def;
$lines[] = ";";
} else {
###################################################################
# maj d'une vue existante
trigger_error("$view: view update is not (yet) supported", E_USER_WARNING);
}
} else {
trigger_error("$type: unknown type, expected table or view", E_USER_WARNING);
}
}
if (self::$tables || self::$views) {
$name = A::get($update, "+name", "update");
$index = $minfos["index"];
$outfname = "$index-$name.sql";
if ($prefix) $outfname = "$minfos[prefix]--$outfname";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite_sql);
if ($ovsuf || $verbose) {
self::trace(" sql output: $outfname$ovsuf");
self::write_lines($lines, "$outf", $overwrite_sql);
}
# les fichiers csv ne sont écrasés que s'ils n'ont pas de données
# automatiquement
$overwrite_csv_threshold = $overwrite_sql? 1: false;
foreach (self::$tables as $table => $tinfos) {
$table_name = $tinfos["name"];
$cols = array_keys($tinfos["cols"]);
$lines = [implode(",", $cols)];
$index++;
$outfname = "$index-$table_name.csv";
if ($prefix) $outfname = "$minfos[prefix]--$outfname";
$outf = "$destdir/$outfname";
$overwrite_csv = self::should_overwrite($outf, $overwrite_csv_threshold);
$ovsuf = self::get_ovsuf($outf, $overwrite_csv);
if ($ovsuf || $verbose) {
self::trace(" csv output: $outfname$ovsuf");
self::write_lines($lines, "$outf", $overwrite_csv);
}
}
}
}
}
static function gendoc($class, $options=null) {
self::reset($options);
$destdir = A::get($options, "destdir", ".");
$overwrite = A::get($options, "overwrite");
$verbose = A::get($options, "verbose");
# par défaut, écraser les fichiers lors de la génération de la doc
if ($overwrite === null) $overwrite = true;
self::trace("# gendoc");
self::trace(" class: $class");
self::trace(" destdir: $destdir");
$methods = self::get_methods($class);
foreach ($methods as $minfos) {
$update = func::call($minfos["method"]);
foreach ($update as $uname => $uinfos) {
# ignorer les directives de mise à jour: elles ont été traitées ci-dessus
if (str::starts_with("+", $uname)) {
if (!in_array($uname, self::UPDATE_DIRECTIVES)) {
trigger_error("$uname: unknown update directive", E_USER_WARNING);
}
continue;
}
$type = A::get($uinfos, "+type", "table");
if ($type === "table") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$table = $uname;
$cols = $uinfos;
#####################################################################
# suppression de table
if ($cols === null) {
self::del_table($table);
continue;
}
# liste de champs "normale"
if (!self::has_table($table)) {
###################################################################
# nouvelle table
self::add_table($table);
foreach ($cols as $col => $value) {
if (!str::starts_with("+", $col)) {
self::add_col($table, $col);
} else {
switch ($col) {
case "+foreign key":
foreach (A::with($value) as $fk => $ref) {
$ms = null;
if (self::is_multi_fkdef($fk, $ms)) $fk = $ms[1];
if (self::is_single_fkref($ref, $ms)) {
self::add_t1link($fk, $table, $ms[2], $ms[1]);
}
}
break;
default:
if (!in_array($col, self::TABLE_DIRECTIVES)) {
trigger_error("$table: $col: unknown table directive", E_USER_WARNING);
}
break;
}
}
}
} else {
###################################################################
# maj d'une table existante
foreach ($cols as $col => $def) {
# les directives seront traitées plus tard
if (!str::starts_with("+", $col)) {
if ($def === null) {
## suppression d'un champ
if (!self::has_col($table, $col)) {
trigger_error("$table.$col: deleting a non-existent column", E_USER_WARNING);
}
self::del_col($table, $col);
} else {
if (!self::has_col($table, $col)) {
self::add_col($table, $col);
}
}
} else {
## maj directive
trigger_error("$table: $col: table directive update is not (yet) supported", E_USER_WARNING);
}
}
}
} elseif ($type === "view") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$view = $uname;
$vinfos = $uinfos;
#####################################################################
# suppression de vue
if ($vinfos === null) {
self::del_view($view);
continue;
}
# définition "normale"
if (!self::has_view($view)) {
###################################################################
# nouvelle vue
$tables = A::get($vinfos, "+view_tables", null);
self::add_view($view, $tables);
} else {
###################################################################
# maj d'une vue existante
trigger_error("$view: view update is not (yet) supported", E_USER_WARNING);
}
} else {
trigger_error("$type: unknown type, expected table or view", E_USER_WARNING);
}
}
}
$lines = [
"# -*- coding: utf-8 mode: plantuml -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8",
"# fichier autogénéré: toute modification locale risque d'être perdue",
"",
"##@startuml",
"hide empty members",
"hide empty methods",
"hide circle",
"!ifndef SHOW_DETAILS",
"hide members",
"!endif",
];
foreach (self::$tables as $table => $tinfos) {
$lines[] = "";
$lines[] = "class $table {";
$cols = $tinfos["cols"];
foreach (array_keys($cols) as $col) {
$lines[] = " $col";
}
$lines[] = "}";
}
foreach (self::$views as $view => $vinfos) {
$lines[] = "";
$lines[] = "class $view<view> {";
$tables = $vinfos["tables"];
foreach ($tables as $table) {
$cols = self::$tables[$table]["cols"];
foreach (array_keys($cols) as $col) {
$lines[] = " $col";
}
}
$lines[] = "}";
}
$lines[] = "";
$lines[] = "##@enduml";
$outfname = "classes.iuml";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite);
if ($ovsuf || $verbose) {
self::trace(" puml output: $outfname$ovsuf");
self::write_lines($lines, $outf, $overwrite, true);
}
$lines = [];
foreach (self::$tables as $table => $tinfos) {
$lines[] = "'hide $table";
}
$first = true;
foreach (self::$t1links as $link) {
if ($first) $lines[] = "";
list($scol, $stable, $dcol, $dtable) = $link;
$lines[] = "$stable \"*\" -- \"1\" $dtable";
$first = false;
}
A::merge($lines, [
"~~~",
"",
"-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary",
]);
$slines = [
"![Modèle simplifié](model-simple.png)",
"~~~puml",
"# output=model-simple.png",
"!include classes.iuml",
];
A::merge($slines, $lines);
$outfname = "model-simple.md";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite);
if ($ovsuf || $verbose) {
self::trace(" md output: $outfname$ovsuf");
self::write_lines($slines, $outf, $overwrite, false);
}
$dlines = [
"![Modèle détaillé](model-details.png)",
"~~~puml",
"# output=model-details.png",
"!define SHOW_DETAILS",
"!include classes.iuml",
];
A::merge($dlines, $lines);
$outfname = "model-details.md";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite);
if ($ovsuf || $verbose) {
self::trace(" md output: $outfname$ovsuf");
self::write_lines($dlines, $outf, $overwrite, false);
}
}
private static function add_vtables(&$lines, $cols) {
$lines[] = "";
$lines[] = " const VTABLES_HEADERS = self::COLUMNS;";
foreach ($cols as $col => $cinfos) {
$vcol = strtoupper($col)."_VCOL";
$lines[] = " const $vcol = [";
$lines[] = " \"data\" => \"$col\",";
$lines[] = " \"type\" => \"$cinfos[hot_type]\",";
if ($cinfos["hot_type"] == "date") {
$lines[] = " \"dateFormat\" => \"DD/MM/YYYY\",";
$lines[] = " \"correctFormat\" => true,";
}
$lines[] = " ];";
}
$lines[] = " const VTABLES_SCHEMA = [";
foreach (array_keys($cols) as $col) {
$vcol = strtoupper($col)."_VCOL";
$lines[] = " self::$vcol,";
}
$lines[] = " ];";
}
private static function add_data_schema(&$lines, $cols) {
$cols = array_keys($cols);
$lines[] = "";
foreach ($cols as $col) {
$dcol = strtoupper($col)."_DCOL";
$lines[] = " const $dcol = [";
$lines[] = " \"name\" => \"$col\",";
$lines[] = " \"key\" => \"$col\",";
$lines[] = " ];";
}
$lines[] = " const DATA_SCHEMA = [";
foreach ($cols as $col) {
$dcol = strtoupper($col)."_DCOL";
$lines[] = " self::$dcol,";
}
$lines[] = " ];";
$lines[] = " const CSV_SCHEMA = self::DATA_SCHEMA;";
$lines[] = " const HTML_SCHEMA = self::DATA_SCHEMA;";
}
private static function add_schema(array &$lines, array $cols, string $indent): void {
$schema = [];
foreach ($cols as $col) {
$schema[$col["name"]] = $col["schema"];
}
md::normalize_mschema($schema);
$generator = new SrcGenerator($indent);
$generator->genConst("SCHEMA", $schema);
$generator->mergeInto($lines);
}
static function genclass($class, $options=null) {
$c = new ReflectionClass($class);
$force_dbtrait = $c->getConstant("DBTRAIT");
$default_dbtrait = $c->getConstant("DEFAULT_DBTRAIT");
self::reset($options);
$destdir = A::get($options, "destdir", ".");
$package = A::get($options, "package");
$baserow = A::get($options, "baserow");
$overwrite = A::get($options, "overwrite");
$verbose = A::get($options, "verbose");
if ($baserow === null) $baserow = AbstractRow::class;
$pos = strrpos($class, "\\");
$default_package = $pos !== false? substr($class, 0, $pos): false;
if ($package === null) $package = $default_package;
elseif (substr($package, 0, 1) != "\\") $package = "$default_package\\$package";
else $package = substr($package, 1);
# par défaut ne pas écraser les fichiers lors de la génération des classes
if ($overwrite === null) $overwrite = false;
# ne pas écraser les classes dont la définition est en +prod
$no_overwrites = [];
self::trace("# genclass");
self::trace(" class: $class");
self::trace(" destdir: $destdir");
$methods = self::get_methods($class);
foreach ($methods as $minfos) {
$update = func::call($minfos["method"]);
$dbtrait = $force_dbtrait;
if (!$dbtrait) $dbtrait = A::get($update, "+dbtrait");
if (!$dbtrait) $dbtrait = $default_dbtrait;
$prod = A::get($update, "+prod");
foreach ($update as $uname => $uinfos) {
# ignorer les directives de mise à jour: elles ont été traitées ci-dessus
if (str::starts_with("+", $uname)) {
if (!in_array($uname, self::UPDATE_DIRECTIVES)) {
trigger_error("$uname: unknown update directive", E_USER_WARNING);
}
continue;
}
$type = A::get($uinfos, "+type", "table");
if ($type === "table") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$table = $uname;
$table_name = A::get($uinfos, "+name", $table);
$cols = $uinfos;
#######################################################################
# suppression de table
if ($cols === null) {
self::del_table($table);
if ($prod) unset($no_overwrites[$table]);
continue;
}
# liste de champs "normale"
if (!self::has_table($table)) {
#####################################################################
# nouvelle table
$suffix = A::get($cols, "+suffix");
self::add_table($table, $suffix, $dbtrait, $table_name);
if ($prod) $no_overwrites[$table] = true;
foreach ($cols as $col => $value) {
if (!str::starts_with("+", $col)) {
$def = $value;
$cinfos = self::add_col($table, $col, $def);
if ($cinfos["pk"]) self::set_table_pk($table, $col);
} else {
switch ($col) {
case "+primary key":
$pk = A::with($value);
self::set_table_pk($table, $pk);
break;
case "+foreign key":
foreach (A::with($value) as $fk => $ref) {
$ms = null;
if (self::is_multi_fkdef($fk, $ms)) $fk = $ms[1];
if (self::is_single_fkref($ref, $ms)) {
self::add_t1link($fk, $table, $ms[2], $ms[1]);
}
}
break;
default:
if (!in_array($col, self::TABLE_DIRECTIVES)) {
trigger_error("$table: $col: unknown table directive", E_USER_WARNING);
}
break;
}
}
}
} else {
#####################################################################
# maj d'une table existante
foreach ($cols as $col => $def) {
# les directives seront traitées plus tard
if (!str::starts_with("+", $col)) {
if ($def === null) {
## suppression d'un champ
if (!self::has_col($table, $col)) {
trigger_error("$table.$col: deleting a non-existent column", E_USER_WARNING);
}
self::del_col($table, $col);
} else {
if (!self::has_col($table, $col)) {
self::add_col($table, $col, $def);
}
}
} else {
## maj directive
trigger_error("$table: $col: table directive update is not (yet) supported", E_USER_WARNING);
}
}
}
} elseif ($type === "view") {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$view = $uname;
$view_name = A::get($uinfos, "+name", $view);
$vinfos = $uinfos;
#####################################################################
# suppression de vue
if ($vinfos === null) {
self::del_view($view);
if ($prod) unset($no_overwrites[$view]);
continue;
}
# définition "normale"
if (!self::has_view($view)) {
###################################################################
# nouvelle vue
$tables = A::get($vinfos, "+view_tables");
self::add_view($view, $tables, $dbtrait, $view_name);
if ($prod) $no_overwrites[$view] = true;
} else {
###################################################################
# maj d'une vue existante
trigger_error("$view: view update is not (yet) supported", E_USER_WARNING);
}
} else {
trigger_error("$type: unknown type, expected table or view", E_USER_WARNING);
}
}
}
foreach (self::$tables as $table => $tinfos) {
$class = str::upperw($table);
$class = preg_replace('/[^a-zA-Z0-9]*/', "", $class);
$suffix = $tinfos["suffix"];
$suffix = $suffix? "\"$suffix\"": "null";
$pk = $tinfos["pk"];
$pk_key = [];
if ($pk !== null) {
foreach ($pk as $key) {
$pk_key[] = "\"$key\"";
}
}
if (!$pk_key) {
$pk_key = "false";
} elseif (\count($pk_key) == 1) {
$pk_key = $pk_key[0];
} else {
$pk_key = "[".implode(", ", $pk_key)."]";
}
$pk_auto = false;
$cols = $tinfos["cols"];
foreach ($cols as $cinfos) {
if ($cinfos["auto"]) {
$pk_auto = true;
break;
}
}
$pk_auto = $pk_auto? "true": "false";
$dbtrait = $tinfos["dbtrait"];
$table_name = $tinfos["name"];
###
$lines = ["<?php"];
if ($package) $lines[] = "namespace $package;";
$lines[] = "";
$lines[] = "class _$class extends \\$baserow {";
if ($dbtrait) {
$lines[] = " use \\$dbtrait;";
$lines[] = "";
}
$lines[] = " const TABLE_NAME = \"$table_name\";";
$lines[] = " const COL_SUFFIX = $suffix;";
$lines[] = " const PK_KEY = $pk_key;";
$lines[] = " const PK_AUTO = $pk_auto;";
$lines[] = " const COLUMNS = [";
foreach (array_keys($cols) as $col) {
$lines[] = " \"$col\",";
}
$lines[] = " ];";
#XXX désactiver pour le moment
#self::add_vtables($lines, $cols);
#self::add_data_schema($lines, $cols);
$lines[] = "}";
$overwrite_class = $overwrite || !A::has($no_overwrites, $table);
$outfname = "_$class.php";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite_class);
if ($ovsuf || $verbose) {
self::trace(" php output: $outfname$ovsuf");
self::write_lines($lines, $outf, $overwrite_class, true);
}
###
$lines = ["<?php"];
if ($package) $lines[] = "namespace $package;";
$lines[] = "";
$lines[] = "class $class extends _$class {";
$lines[] = "}";
# les fichiers Class ne sont jamais écrasés automatiquement
$overwrite_class = false; #$overwrite && !A::has($no_overwrites, $table);
$outfname = "$class.php";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite_class);
if ($ovsuf || $verbose) {
self::trace(" php output: $outfname$ovsuf");
self::write_lines($lines, $outf, $overwrite_class, true);
}
}
foreach (self::$views as $view => $vinfos) {
$class = str::upperw($view);
$class = preg_replace('/[^a-zA-Z0-9]*/', "", $class);
$cols = [];
foreach ($vinfos["tables"] as $table) {
$cols = array_merge($cols, self::$tables[$table]["cols"]);
}
$dbtrait = $tinfos["dbtrait"];
$view_name = $vinfos["name"];
###
$lines = ["<?php"];
if ($package) $lines[] = "namespace $package;";
$lines[] = "";
$lines[] = "class _$class extends $baserow {";
if ($dbtrait) {
$lines[] = " use \\$dbtrait;";
$lines[] = "";
}
$lines[] = " const TABLE_NAME = \"$view_name\";";
$lines[] = " const COL_SUFFIX = null;";
$lines[] = " const PK_KEY = false;";
$lines[] = " const PK_AUTO = false;";
$lines[] = " const COLUMNS = [";
foreach (array_keys($cols) as $col) {
$lines[] = " \"$col\",";
}
$lines[] = " ];";
#XXX désactiver pour le moment
#self::add_vtables($lines, $cols);
#self::add_data_schema($lines, $cols);
$lines[] = "}";
$overwrite_class = $overwrite || !A::has($no_overwrites, $table);
$outfname = "_$class.php";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite_class);
if ($ovsuf || $verbose) {
self::trace(" php output: $outfname$ovsuf");
self::write_lines($lines, $outf, $overwrite_class, true);
}
###
$lines = ["<?php"];
if ($package) $lines[] = "namespace $package;";
$lines[] = "";
$lines[] = "class $class extends _$class {";
$lines[] = "}";
# les fichiers Class ne sont jamais écrasés automatiquement
$overwrite_class = false; #$overwrite && !A::has($no_overwrites, $view);
$outfname = "$class.php";
$outf = "$destdir/$outfname";
$ovsuf = self::get_ovsuf($outf, $overwrite_class);
if ($ovsuf || $verbose) {
self::trace(" php output: $outfname$ovsuf");
self::write_lines($lines, $outf, $overwrite_class, true);
}
}
}
}