Files
alrahma_sunday_school_api/tools/sync_fillable_from_codeigniter.php
T
2026-04-23 00:04:35 -04:00

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);
}