626 lines
25 KiB
PHP
626 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Support\UsernamePolicy;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
final class CheckArtworkUserReferencesCommand extends Command
|
|
{
|
|
protected $signature = 'artworks:check-user-refs
|
|
{--chunk=1000 : Number of artworks to process per chunk}
|
|
{--show-missing=25 : Maximum number of missing references to print}
|
|
{--artwork-id= : Only check/copy the user referenced by this specific artwork ID}
|
|
{--copy-missing-from-legacy : Copy missing referenced users from the legacy users table into the new users table using the same id}
|
|
{--create-placeholder : Create a placeholder tmpu{id} stub user when the legacy user cannot be found}
|
|
{--dry-run-copy : Preview legacy user copies without writing them}
|
|
{--legacy-connection=legacy : Legacy database connection name}
|
|
{--legacy-users-table=users : Legacy users table name}
|
|
{--json : Output the summary as JSON}';
|
|
|
|
protected $description = 'Check that every artworks.user_id points to an existing users.id row.';
|
|
|
|
public function handle(): int
|
|
{
|
|
$chunkSize = max(1, (int) $this->option('chunk'));
|
|
$showMissing = max(0, (int) $this->option('show-missing'));
|
|
$copyMissingFromLegacy = (bool) $this->option('copy-missing-from-legacy');
|
|
$createPlaceholder = (bool) $this->option('create-placeholder');
|
|
$dryRunCopy = (bool) $this->option('dry-run-copy');
|
|
$legacyConnection = (string) $this->option('legacy-connection');
|
|
$legacyUsersTable = (string) $this->option('legacy-users-table');
|
|
|
|
$artworkId = $this->option('artwork-id') !== null ? (int) $this->option('artwork-id') : null;
|
|
|
|
$this->line(sprintf('Auditing artworks.user_id references in chunks of %d...', $chunkSize));
|
|
|
|
$audit = $this->auditArtworkUserReferences($chunkSize, $showMissing, $artworkId);
|
|
$copySummary = null;
|
|
|
|
if ($copyMissingFromLegacy) {
|
|
$this->newLine();
|
|
$this->line(sprintf(
|
|
'%s missing referenced users from legacy connection "%s" table "%s".',
|
|
$dryRunCopy ? 'Previewing copy of' : 'Copying',
|
|
$legacyConnection,
|
|
$legacyUsersTable,
|
|
));
|
|
|
|
try {
|
|
$copySummary = $this->copyMissingUsersFromLegacy(
|
|
array_keys($audit['missing_user_ids']),
|
|
$legacyConnection,
|
|
$legacyUsersTable,
|
|
$dryRunCopy,
|
|
$createPlaceholder,
|
|
);
|
|
} catch (\Throwable $exception) {
|
|
$this->error($exception->getMessage());
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if (! $dryRunCopy && ($copySummary['copied'] ?? 0) > 0) {
|
|
$audit = $this->auditArtworkUserReferences($chunkSize, $showMissing, $artworkId);
|
|
}
|
|
}
|
|
|
|
if ((bool) $this->option('json')) {
|
|
$payload = [
|
|
'summary' => $audit['summary'],
|
|
'sample_missing' => $audit['sample_missing'],
|
|
];
|
|
|
|
if ($copySummary !== null) {
|
|
$payload['copy_summary'] = $copySummary;
|
|
}
|
|
|
|
$this->line(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
|
|
return ((int) ($audit['summary']['missing'] ?? 0)) === 0 ? self::SUCCESS : self::FAILURE;
|
|
}
|
|
|
|
$this->renderAuditSummary($audit['summary'], $audit['sample_missing']);
|
|
|
|
if ($copySummary !== null) {
|
|
$this->renderCopySummary($copySummary);
|
|
}
|
|
|
|
return ((int) ($audit['summary']['missing'] ?? 0)) === 0 ? self::SUCCESS : self::FAILURE;
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* summary: array{checked:int, valid:int, missing:int, null_user_ids:int},
|
|
* sample_missing: array<int, array{artwork_id:int, user_id:string, title:string}>,
|
|
* missing_user_ids: array<int, true>
|
|
* }
|
|
*/
|
|
private function auditArtworkUserReferences(int $chunkSize, int $showMissing, ?int $artworkId = null): array
|
|
{
|
|
$checked = 0;
|
|
$valid = 0;
|
|
$missing = 0;
|
|
$nullUserIds = 0;
|
|
$sampleRows = [];
|
|
$missingUserIds = [];
|
|
|
|
DB::table('artworks')
|
|
->leftJoin('users', 'users.id', '=', 'artworks.user_id')
|
|
->select([
|
|
'artworks.id',
|
|
'artworks.user_id',
|
|
'artworks.title',
|
|
DB::raw('users.id as matched_user_id'),
|
|
])
|
|
->when($artworkId !== null, fn ($q) => $q->where('artworks.id', $artworkId))
|
|
->orderBy('artworks.id')
|
|
->chunkById($chunkSize, function ($artworks) use (&$checked, &$valid, &$missing, &$nullUserIds, &$sampleRows, &$missingUserIds, $showMissing): void {
|
|
foreach ($artworks as $artwork) {
|
|
$checked++;
|
|
|
|
if ($artwork->matched_user_id !== null) {
|
|
$valid++;
|
|
continue;
|
|
}
|
|
|
|
$missing++;
|
|
|
|
if ($artwork->user_id === null) {
|
|
$nullUserIds++;
|
|
} else {
|
|
$missingUserIds[(int) $artwork->user_id] = true;
|
|
}
|
|
|
|
if (count($sampleRows) < $showMissing) {
|
|
$sampleRows[] = [
|
|
'artwork_id' => (int) $artwork->id,
|
|
'user_id' => $artwork->user_id === null ? '[null]' : (string) $artwork->user_id,
|
|
'title' => (string) ($artwork->title ?? ''),
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($this->isVerboseOutput()) {
|
|
$this->line(sprintf(
|
|
' audited %d artworks so far; missing=%d, null_user_id=%d.',
|
|
$checked,
|
|
$missing,
|
|
$nullUserIds,
|
|
));
|
|
}
|
|
}, 'artworks.id', 'id');
|
|
|
|
return [
|
|
'summary' => [
|
|
'checked' => $checked,
|
|
'valid' => $valid,
|
|
'missing' => $missing,
|
|
'null_user_ids' => $nullUserIds,
|
|
],
|
|
'sample_missing' => $sampleRows,
|
|
'missing_user_ids' => $missingUserIds,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int|string> $legacyIds
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function copyMissingUsersFromLegacy(array $legacyIds, string $legacyConnection, string $legacyUsersTable, bool $dryRun, bool $createPlaceholder = false): array
|
|
{
|
|
$result = [
|
|
'requested_users' => count($legacyIds),
|
|
'copied' => 0,
|
|
'placeholders_created' => 0,
|
|
'would_copy' => 0,
|
|
'conflicts' => 0,
|
|
'not_found_in_legacy' => 0,
|
|
'errors' => 0,
|
|
'dry_run' => $dryRun,
|
|
'sample_copied_ids' => [],
|
|
'sample_placeholder_ids' => [],
|
|
'sample_conflict_ids' => [],
|
|
'sample_not_found_ids' => [],
|
|
'sample_error_messages' => [],
|
|
];
|
|
|
|
if ($legacyIds === []) {
|
|
if ($this->isVerboseOutput()) {
|
|
$this->line('No missing non-null user ids were found to copy from the legacy users table.');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
$this->ensureLegacyConnectionIsUsable($legacyConnection, $legacyUsersTable);
|
|
|
|
$normalizedLegacyIds = array_values(array_unique(array_map('intval', $legacyIds)));
|
|
|
|
foreach (array_chunk($normalizedLegacyIds, 200) as $chunkIndex => $chunk) {
|
|
$legacyRows = DB::connection($legacyConnection)
|
|
->table($legacyUsersTable)
|
|
->whereIn('user_id', $chunk)
|
|
->get()
|
|
->keyBy(fn (object $row): int => (int) $row->user_id);
|
|
|
|
if ($this->isVerboseOutput()) {
|
|
$this->line(sprintf(
|
|
' processing legacy chunk %d with %d requested ids; found %d legacy rows.',
|
|
$chunkIndex + 1,
|
|
count($chunk),
|
|
$legacyRows->count(),
|
|
));
|
|
}
|
|
|
|
foreach ($chunk as $legacyId) {
|
|
if (DB::table('users')->where('id', $legacyId)->exists()) {
|
|
$result['conflicts']++;
|
|
if (count($result['sample_conflict_ids']) < 10) {
|
|
$result['sample_conflict_ids'][] = $legacyId;
|
|
}
|
|
|
|
if ($this->isVerboseOutput()) {
|
|
$this->warn(sprintf('[skip-conflict] user #%d already exists in the new users table.', $legacyId));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$legacyUser = $legacyRows->get($legacyId);
|
|
|
|
if (! $legacyUser) {
|
|
$result['not_found_in_legacy']++;
|
|
if (count($result['sample_not_found_ids']) < 10) {
|
|
$result['sample_not_found_ids'][] = $legacyId;
|
|
}
|
|
|
|
if ($this->isVerboseOutput()) {
|
|
$this->warn(sprintf(
|
|
'[missing-legacy] user #%d was not found in %s.%s.',
|
|
$legacyId,
|
|
$legacyConnection,
|
|
$legacyUsersTable,
|
|
));
|
|
}
|
|
|
|
if ($createPlaceholder) {
|
|
if ($dryRun) {
|
|
$result['would_copy']++;
|
|
$this->line(sprintf('[dry-run] would create placeholder tmpu%d for user #%d', $legacyId, $legacyId));
|
|
} else {
|
|
try {
|
|
$this->createPlaceholderUser($legacyId);
|
|
$result['placeholders_created']++;
|
|
if (count($result['sample_placeholder_ids']) < 10) {
|
|
$result['sample_placeholder_ids'][] = $legacyId;
|
|
}
|
|
$this->info(sprintf('[placeholder] created tmpu%d for user #%d', $legacyId, $legacyId));
|
|
} catch (\Throwable $exception) {
|
|
$result['errors']++;
|
|
$message = sprintf('#%d (placeholder): %s', $legacyId, $exception->getMessage());
|
|
if (count($result['sample_error_messages']) < 10) {
|
|
$result['sample_error_messages'][] = $message;
|
|
}
|
|
$this->error('[placeholder-error] ' . $message);
|
|
}
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($dryRun) {
|
|
$result['would_copy']++;
|
|
if (count($result['sample_copied_ids']) < 10) {
|
|
$result['sample_copied_ids'][] = $legacyId;
|
|
}
|
|
|
|
$this->line(sprintf('[dry-run] would import legacy user %s', $this->describeLegacyUser($legacyUser, $legacyId)));
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$this->importLegacyUserBySameId($legacyUser, $legacyId);
|
|
$result['copied']++;
|
|
if (count($result['sample_copied_ids']) < 10) {
|
|
$result['sample_copied_ids'][] = $legacyId;
|
|
}
|
|
|
|
$this->info(sprintf('[copied] imported legacy user %s', $this->describeLegacyUser($legacyUser, $legacyId)));
|
|
} catch (\Throwable $exception) {
|
|
$result['errors']++;
|
|
$message = sprintf('#%d: %s', $legacyId, $exception->getMessage());
|
|
if (count($result['sample_error_messages']) < 10) {
|
|
$result['sample_error_messages'][] = $message;
|
|
}
|
|
|
|
$this->error('[copy-error] ' . $message);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function importLegacyUserBySameId(object $legacyUser, int $legacyId): void
|
|
{
|
|
$now = now();
|
|
$username = $this->resolveImportUsername($legacyUser, $legacyId);
|
|
$email = $this->resolveImportEmail($legacyUser, $legacyId);
|
|
$name = (string) ($this->legacyField($legacyUser, 'real_name') ?: $username);
|
|
$createdAt = $this->parseLegacyDate($this->legacyField($legacyUser, 'joinDate')) ?? $now;
|
|
$lastVisitAt = $this->parseLegacyDate($this->legacyField($legacyUser, 'LastVisit'));
|
|
$countryCode = $this->legacyField($legacyUser, 'country_code');
|
|
|
|
DB::transaction(function () use ($legacyId, $legacyUser, $username, $email, $name, $createdAt, $lastVisitAt, $countryCode, $now): void {
|
|
if (DB::table('users')->where('id', $legacyId)->exists()) {
|
|
throw new RuntimeException(sprintf('Conflict: user id %d already exists in the new users table.', $legacyId));
|
|
}
|
|
|
|
DB::table('users')->insert([
|
|
'id' => $legacyId,
|
|
'username' => $username,
|
|
'username_changed_at' => $now,
|
|
'name' => $name,
|
|
'email' => $email,
|
|
'password' => Hash::make(Str::random(64)),
|
|
'is_active' => (int) ($this->legacyField($legacyUser, 'active') ?? 1) === 1,
|
|
'needs_password_reset' => true,
|
|
'role' => 'user',
|
|
'legacy_password_algo' => null,
|
|
'last_visit_at' => $lastVisitAt,
|
|
'created_at' => $createdAt,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
if (Schema::hasTable('user_profiles')) {
|
|
DB::table('user_profiles')->updateOrInsert(
|
|
['user_id' => $legacyId],
|
|
[
|
|
'bio' => $this->legacyField($legacyUser, 'about_me') ?: $this->legacyField($legacyUser, 'description'),
|
|
'country' => $this->legacyField($legacyUser, 'country'),
|
|
'country_code' => is_string($countryCode) && $countryCode !== '' ? substr($countryCode, 0, 2) : null,
|
|
'website' => $this->legacyField($legacyUser, 'web'),
|
|
'gender' => $this->normalizeLegacyGender($this->legacyField($legacyUser, 'gender')),
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
);
|
|
}
|
|
|
|
if (Schema::hasTable('user_statistics')) {
|
|
DB::table('user_statistics')->updateOrInsert(
|
|
['user_id' => $legacyId],
|
|
[
|
|
'uploads_count' => 0,
|
|
'downloads_received_count' => 0,
|
|
'artwork_views_received_count' => 0,
|
|
'awards_received_count' => 0,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function createPlaceholderUser(int $legacyId): void
|
|
{
|
|
$now = now();
|
|
$username = $this->uniquePlaceholderUsername($legacyId);
|
|
$email = $username . '@users.skinbase.org';
|
|
|
|
DB::transaction(function () use ($legacyId, $username, $email, $now): void {
|
|
if (DB::table('users')->where('id', $legacyId)->exists()) {
|
|
throw new RuntimeException(sprintf('Conflict: user id %d already exists in the new users table.', $legacyId));
|
|
}
|
|
|
|
DB::table('users')->insert([
|
|
'id' => $legacyId,
|
|
'username' => $username,
|
|
'username_changed_at' => $now,
|
|
'name' => $username,
|
|
'email' => $email,
|
|
'password' => Hash::make(Str::random(64)),
|
|
'is_active' => false,
|
|
'needs_password_reset' => true,
|
|
'role' => 'user',
|
|
'legacy_password_algo' => null,
|
|
'last_visit_at' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
if (Schema::hasTable('user_profiles')) {
|
|
DB::table('user_profiles')->updateOrInsert(
|
|
['user_id' => $legacyId],
|
|
['created_at' => $now, 'updated_at' => $now],
|
|
);
|
|
}
|
|
|
|
if (Schema::hasTable('user_statistics')) {
|
|
DB::table('user_statistics')->updateOrInsert(
|
|
['user_id' => $legacyId],
|
|
[
|
|
'uploads_count' => 0,
|
|
'downloads_received_count' => 0,
|
|
'artwork_views_received_count' => 0,
|
|
'awards_received_count' => 0,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function resolveImportUsername(object $legacyUser, int $legacyId): string
|
|
{
|
|
$rawUsername = (string) ($this->legacyField($legacyUser, 'uname') ?: ('user' . $legacyId));
|
|
$username = $this->sanitizeUsername($rawUsername);
|
|
|
|
if (! $this->usernameExists($username, $legacyId)) {
|
|
return $username;
|
|
}
|
|
|
|
return $this->uniquePlaceholderUsername($legacyId);
|
|
}
|
|
|
|
private function sanitizeUsername(string $username): string
|
|
{
|
|
return UsernamePolicy::sanitizeLegacy($username);
|
|
}
|
|
|
|
private function usernameExists(string $username, int $ignoreUserId): bool
|
|
{
|
|
return DB::table('users')
|
|
->whereRaw('LOWER(username) = ?', [strtolower($username)])
|
|
->where('id', '!=', $ignoreUserId)
|
|
->exists();
|
|
}
|
|
|
|
private function uniquePlaceholderUsername(int $legacyId): string
|
|
{
|
|
$base = 'tmpu' . $legacyId;
|
|
$candidate = $base;
|
|
$suffix = 1;
|
|
|
|
while ($this->usernameExists($candidate, $legacyId)) {
|
|
$suffixStr = (string) $suffix;
|
|
$candidate = substr($base, 0, max(1, 20 - strlen($suffixStr))) . $suffixStr;
|
|
$suffix++;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function renderAuditSummary(array $summary, array $sampleRows): void
|
|
{
|
|
$this->info(sprintf(
|
|
'Checked %d artworks: %d valid, %d missing user references, %d null user_id values.',
|
|
(int) ($summary['checked'] ?? 0),
|
|
(int) ($summary['valid'] ?? 0),
|
|
(int) ($summary['missing'] ?? 0),
|
|
(int) ($summary['null_user_ids'] ?? 0),
|
|
));
|
|
|
|
if ($sampleRows !== []) {
|
|
$this->newLine();
|
|
$this->warn('Sample missing references:');
|
|
$this->table(['Artwork ID', 'user_id', 'Title'], array_map(
|
|
static fn (array $row): array => [$row['artwork_id'], $row['user_id'], $row['title']],
|
|
$sampleRows,
|
|
));
|
|
}
|
|
|
|
if ((int) ($summary['missing'] ?? 0) === 0) {
|
|
$this->info('No missing user references found in artworks.user_id.');
|
|
} else {
|
|
$this->error('Found artworks with missing user references.');
|
|
}
|
|
}
|
|
|
|
private function renderCopySummary(array $copySummary): void
|
|
{
|
|
$this->newLine();
|
|
$this->info(sprintf(
|
|
'Legacy copy summary: requested %d users, copied %d, placeholders %d, would copy %d, conflicts %d, not found in legacy %d, errors %d.',
|
|
(int) ($copySummary['requested_users'] ?? 0),
|
|
(int) ($copySummary['copied'] ?? 0),
|
|
(int) ($copySummary['placeholders_created'] ?? 0),
|
|
(int) ($copySummary['would_copy'] ?? 0),
|
|
(int) ($copySummary['conflicts'] ?? 0),
|
|
(int) ($copySummary['not_found_in_legacy'] ?? 0),
|
|
(int) ($copySummary['errors'] ?? 0),
|
|
));
|
|
|
|
if (($copySummary['sample_copied_ids'] ?? []) !== []) {
|
|
$this->line('Copied or would-copy user ids: ' . implode(', ', $copySummary['sample_copied_ids']));
|
|
}
|
|
|
|
if (($copySummary['sample_placeholder_ids'] ?? []) !== []) {
|
|
$this->line('Placeholder users created for ids: ' . implode(', ', $copySummary['sample_placeholder_ids']));
|
|
}
|
|
|
|
if (($copySummary['sample_conflict_ids'] ?? []) !== []) {
|
|
$this->warn('Conflicts: user ids already present in new DB: ' . implode(', ', $copySummary['sample_conflict_ids']));
|
|
}
|
|
|
|
if (($copySummary['sample_not_found_ids'] ?? []) !== []) {
|
|
$this->warn('Not found in legacy: ' . implode(', ', $copySummary['sample_not_found_ids']));
|
|
}
|
|
|
|
if (($copySummary['sample_error_messages'] ?? []) !== []) {
|
|
foreach ($copySummary['sample_error_messages'] as $message) {
|
|
$this->warn($message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function ensureLegacyConnectionIsUsable(string $connection, string $table): void
|
|
{
|
|
try {
|
|
DB::connection($connection)->getPdo();
|
|
} catch (\Throwable $exception) {
|
|
throw new RuntimeException(sprintf('Legacy DB connection "%s" is not configured or reachable.', $connection), 0, $exception);
|
|
}
|
|
|
|
if (! DB::connection($connection)->getSchemaBuilder()->hasTable($table)) {
|
|
throw new RuntimeException(sprintf('Legacy users table "%s" was not found on connection "%s".', $table, $connection));
|
|
}
|
|
}
|
|
|
|
private function resolveImportEmail(object $legacyUser, int $legacyId): string
|
|
{
|
|
$rawEmail = strtolower(trim((string) ($this->legacyField($legacyUser, 'email') ?? '')));
|
|
$candidate = $rawEmail !== ''
|
|
? $rawEmail
|
|
: ($this->sanitizeEmailLocal((string) ($this->legacyField($legacyUser, 'uname') ?: ('user' . $legacyId))) . '@users.skinbase.org');
|
|
|
|
return $this->uniqueEmailCandidate($candidate, $legacyId);
|
|
}
|
|
|
|
private function uniqueEmailCandidate(string $email, int $legacyId): string
|
|
{
|
|
$candidate = strtolower(trim($email));
|
|
$suffix = 1;
|
|
|
|
while ($candidate === '' || DB::table('users')->whereRaw('LOWER(email) = ?', [$candidate])->where('id', '!=', $legacyId)->exists()) {
|
|
$parts = explode('@', $email, 2);
|
|
$local = $this->sanitizeEmailLocal($parts[0] ?? ('user' . $legacyId));
|
|
$domain = $parts[1] ?? 'users.skinbase.org';
|
|
$candidate = $local . '+' . $suffix . '@' . $domain;
|
|
$suffix++;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function sanitizeEmailLocal(string $value): string
|
|
{
|
|
$local = strtolower(trim(Str::ascii($value)));
|
|
$local = preg_replace('/[^a-z0-9._-]/', '-', $local) ?: 'user';
|
|
|
|
return trim($local, '.-') ?: 'user';
|
|
}
|
|
|
|
private function normalizeLegacyGender(mixed $value): ?string
|
|
{
|
|
$normalized = strtoupper(trim((string) ($value ?? '')));
|
|
|
|
return match ($normalized) {
|
|
'M', 'MALE', 'MAN', 'BOY' => 'M',
|
|
'F', 'FEMALE', 'WOMAN', 'GIRL' => 'F',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function isVerboseOutput(): bool
|
|
{
|
|
return $this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
|
|
}
|
|
|
|
private function describeLegacyUser(object $legacyUser, int $legacyId): string
|
|
{
|
|
$username = trim((string) ($this->legacyField($legacyUser, 'uname') ?? ''));
|
|
$name = trim((string) ($this->legacyField($legacyUser, 'real_name') ?? ''));
|
|
$email = trim((string) ($this->legacyField($legacyUser, 'email') ?? ''));
|
|
|
|
return sprintf(
|
|
'#%d username=%s name=%s email=%s',
|
|
$legacyId,
|
|
$username !== '' ? '@' . $username : '[missing]',
|
|
$name !== '' ? '"' . $name . '"' : '[missing]',
|
|
$email !== '' ? '<' . $email . '>' : '[missing]',
|
|
);
|
|
}
|
|
|
|
private function parseLegacyDate(mixed $value): ?Carbon
|
|
{
|
|
if (! is_string($value) || trim($value) === '' || str_starts_with($value, '0000-00-00')) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Carbon::parse($value);
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function legacyField(object $row, string $field): mixed
|
|
{
|
|
return property_exists($row, $field) ? $row->{$field} : null;
|
|
}
|
|
} |