232 lines
6.2 KiB
PHP
232 lines
6.2 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Sync Laravel models from CodeIgniter 4 *Model.php siblings (mass-assignment parity).
|
|
*
|
|
* Updates only:
|
|
* - protected $fillable ← CI protected $allowedFields (order preserved)
|
|
* - public $timestamps ← CI protected $useTimestamps
|
|
*
|
|
* Does NOT overwrite $table / $primaryKey (schemas may diverge: e.g. flag vs current_flag).
|
|
*
|
|
* Skips Laravel: Teacher (extends User), Communication, EventCharge, ClassModel, Calendar aliases.
|
|
*
|
|
* Usage (from app_laravel directory):
|
|
* php tools/sync_fillable_from_codeigniter.php
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
const NL = "\n";
|
|
|
|
$laravelRoot = dirname(__DIR__);
|
|
$workspaceRoot = dirname($laravelRoot);
|
|
$ciModelsDir = $workspaceRoot.DIRECTORY_SEPARATOR.'app_codeigniter'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Models';
|
|
$laravelModelsDir = $laravelRoot.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Models';
|
|
|
|
if (! is_dir($ciModelsDir)) {
|
|
fwrite(STDERR, "CI models dir not found: {$ciModelsDir}".NL);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
/** CI Model basename => Laravel model basename */
|
|
$manualCiToLaravel = [
|
|
'FlagModel.php' => 'CurrentFlag.php',
|
|
];
|
|
|
|
/** Laravel files skipped (aliases / inheritance) */
|
|
$skipLaravelBasenames = [
|
|
'Teacher.php',
|
|
'Communication.php',
|
|
'EventCharge.php',
|
|
'ClassModel.php',
|
|
'Calendar.php',
|
|
];
|
|
|
|
function ciFileToLaravelFile(string $ciBasename, array $manual): ?string
|
|
{
|
|
if (isset($manual[$ciBasename])) {
|
|
return $manual[$ciBasename];
|
|
}
|
|
if (! preg_match('/^(.+)Model\.php$/', $ciBasename, $m)) {
|
|
return null;
|
|
}
|
|
$base = $m[1];
|
|
if ($base === 'Parent' || $base === 'Class') {
|
|
return $base.'Model.php';
|
|
}
|
|
|
|
return $base.'.php';
|
|
}
|
|
|
|
function extractFirstBracketArray(string $src, string $needle): ?string
|
|
{
|
|
$pos = strpos($src, $needle);
|
|
if ($pos === false) {
|
|
return null;
|
|
}
|
|
$pos += strlen($needle);
|
|
$len = strlen($src);
|
|
while ($pos < $len && ctype_space($src[$pos])) {
|
|
$pos++;
|
|
}
|
|
if ($pos >= $len || $src[$pos] !== '[') {
|
|
return null;
|
|
}
|
|
$start = $pos;
|
|
$depth = 0;
|
|
for ($i = $pos; $i < $len; $i++) {
|
|
$c = $src[$i];
|
|
if ($c === '[') {
|
|
$depth++;
|
|
} elseif ($c === ']') {
|
|
$depth--;
|
|
if ($depth === 0) {
|
|
return substr($src, $start, $i - $start + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** @return list<string> */
|
|
function parseQuotedStringsFromArrayLiteral(string $arrayLiteral): array
|
|
{
|
|
$out = [];
|
|
if (! preg_match_all("/'((?:\\\\'|[^'])*)'/", $arrayLiteral, $m)) {
|
|
return $out;
|
|
}
|
|
foreach ($m[1] as $raw) {
|
|
$out[] = str_replace("\\'", "'", $raw);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** Render inner array only: [\n 'a',\n ] */
|
|
function renderFillableArray(array $items): string
|
|
{
|
|
$lines = [];
|
|
foreach ($items as $item) {
|
|
$escaped = addcslashes($item, "\\'");
|
|
$lines[] = " '".$escaped."',";
|
|
}
|
|
|
|
return "[\n".implode("\n", $lines)."\n ]";
|
|
}
|
|
|
|
function replaceFillableBlock(string $laravel, string $newArrayLiteral): string
|
|
{
|
|
$re = '/protected\s+\$fillable\s*=\s*\[[\s\S]*?\];/m';
|
|
if (! preg_match($re, $laravel)) {
|
|
return $laravel;
|
|
}
|
|
|
|
return preg_replace($re, 'protected $fillable = '.$newArrayLiteral.';', $laravel, 1);
|
|
}
|
|
|
|
function replaceTimestampsLine(string $laravel, bool $useTimestamps): string
|
|
{
|
|
$line = $useTimestamps
|
|
? ' public $timestamps = true;'
|
|
: ' public $timestamps = false;';
|
|
|
|
if (preg_match('/public\s+\$timestamps\s*=\s*(true|false)\s*;/', $laravel)) {
|
|
return preg_replace(
|
|
'/public\s+\$timestamps\s*=\s*(true|false)\s*;/',
|
|
$line,
|
|
$laravel,
|
|
1
|
|
);
|
|
}
|
|
|
|
// Insert after class opening brace line (first line after "class X extends Y")
|
|
return preg_replace(
|
|
'/(class\s+\w+[^\n]*\n\{[\t ]*\n)/',
|
|
'$1'.$line.NL.NL,
|
|
$laravel,
|
|
1
|
|
) ?: preg_replace('/(class\s+\w+[^\n]*\n\{)/', '$1'.NL.$line, $laravel, 1);
|
|
}
|
|
|
|
$updated = 0;
|
|
$skipped = 0;
|
|
$missing = [];
|
|
|
|
foreach (glob($ciModelsDir.DIRECTORY_SEPARATOR.'*Model.php') ?: [] as $ciPath) {
|
|
$ciBasename = basename($ciPath);
|
|
$laravelBasename = ciFileToLaravelFile($ciBasename, $manualCiToLaravel);
|
|
if ($laravelBasename === null) {
|
|
continue;
|
|
}
|
|
if (in_array($laravelBasename, $skipLaravelBasenames, true)) {
|
|
fwrite(STDERR, "Skip (alias/base): {$laravelBasename}".NL);
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$lvPath = $laravelModelsDir.DIRECTORY_SEPARATOR.$laravelBasename;
|
|
if (! is_file($lvPath)) {
|
|
$missing[] = $laravelBasename.' (from '.$ciBasename.')';
|
|
|
|
continue;
|
|
}
|
|
|
|
$ciSrc = (string) file_get_contents($ciPath);
|
|
$lvSrc = (string) file_get_contents($lvPath);
|
|
|
|
$useTs = true;
|
|
if (preg_match('/protected\s+\$useTimestamps\s*=\s*(true|false)\s*;/', $ciSrc, $tm)) {
|
|
$useTs = $tm[1] === 'true';
|
|
}
|
|
|
|
$allowedBody = extractFirstBracketArray($ciSrc, 'protected $allowedFields = ');
|
|
if ($allowedBody === null) {
|
|
fwrite(STDERR, "No allowedFields in {$ciBasename}".NL);
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$fields = parseQuotedStringsFromArrayLiteral($allowedBody);
|
|
if ($fields === []) {
|
|
fwrite(STDERR, "Empty allowedFields in {$ciBasename}".NL);
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! preg_match('/protected\s+\$fillable\s*=\s*\[/', $lvSrc)) {
|
|
fwrite(STDERR, "No \$fillable array in {$laravelBasename} — add manually".NL);
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$rendered = renderFillableArray($fields);
|
|
$newLv = replaceFillableBlock($lvSrc, $rendered);
|
|
$newLv = replaceTimestampsLine($newLv, $useTs);
|
|
|
|
if ($newLv === $lvSrc) {
|
|
fwrite(STDERR, "Unchanged: {$laravelBasename}".NL);
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
file_put_contents($lvPath, $newLv);
|
|
fwrite(STDOUT, "Updated {$laravelBasename}".NL);
|
|
$updated++;
|
|
}
|
|
|
|
fwrite(STDOUT, NL.'Done. Updated: '.$updated.', skipped: '.$skipped.NL);
|
|
if ($missing !== []) {
|
|
fwrite(STDERR, 'Missing Laravel files ('.count($missing).'): '.implode(', ', array_slice($missing, 0, 20)).(count($missing) > 20 ? '…' : '').NL);
|
|
|
|
exit(2);
|
|
}
|