File manager - Edit - /var/www/html/portalHomolog/app/Http/Services/LicitacaoProcedimentoImportReport.php
Back
<?php namespace App\Http\Services; class LicitacaoProcedimentoImportReport { public const REPORT_DIR = 'docs/tests/importacoes_licitacoes_procedimentos'; /** @var array<string, array{titulo: string, explicacao: string, impacto: string, passos: string[], coluna_csv: string|null}> */ private const CLASSIFICACOES = [ 'CAMPO_OBRIGATORIO_VAZIO' => [ 'titulo' => 'Campo obrigatório vazio no CSV', 'explicacao' => 'O banco exige um valor na coluna indicada, mas o CSV veio vazio ou a normalização descartou o conteúdo.', 'impacto' => 'O registro principal (licitação/procedimento) não entra no portal. Uploads ligados a esse ID legado também serão ignorados.', 'passos' => [ 'Abra o CSV na linha indicada (número da linha = linha física do arquivo, incluindo cabeçalho).', 'Localize o registro pelo **ID legado** (`cd_licitacao` ou `cd_procedimento`).', 'Preencha a coluna do CSV indicada em **Coluna CSV** (ex.: `ds_objeto` = descrição do objeto).', 'Salve o CSV e execute novamente: `php artisan licitacoes:import` (a rotina é idempotente).', ], 'coluna_csv' => null, ], 'CAMPO_MUITO_LONGO' => [ 'titulo' => 'Texto maior que o limite do banco', 'explicacao' => 'Um valor do CSV ultrapassa o tamanho máximo da coluna na tabela `licitacoes_procedimentos` ou `uploads_licitacoes_procedimentos`.', 'impacto' => 'O registro não é gravado.', 'passos' => [ 'Abra o CSV na linha indicada e encurte o valor do campo afetado.', 'Se o truncamento não for aceitável, avalie ampliar a coluna via migration.', 'Reexecute: `php artisan licitacoes:import`.', ], 'coluna_csv' => null, ], 'UPLOAD_SEM_PROCESSO_PAI' => [ 'titulo' => 'Upload sem processo pai importado', 'explicacao' => 'O arquivo de upload referencia um `cd_licitacao` ou `cd_procedimento` que não existe no mapa — em geral porque o registro principal falhou ou foi ignorado na etapa anterior.', 'impacto' => 'O anexo/documento não aparece no portal para aquela licitação/procedimento.', 'passos' => [ 'Primeiro corrija os **erros** da importação de licitações/procedimentos (seção de erros deste relatório).', 'Confira se o ID legado do pai existe no CSV principal (`TB_PMAR_LICITACAO` ou `TB_PMAR_PROCEDIMENTOS`).', 'Reimporte: `php artisan licitacoes:import` (completo) ou `--only=licitacoes` e depois `--only=uploads`.', ], 'coluna_csv' => 'cd_licitacao / cd_procedimento', ], 'ID_LEGADO_INVALIDO' => [ 'titulo' => 'ID legado ausente ou inválido', 'explicacao' => 'A linha não possui `cd_licitacao` ou `cd_procedimento` válido — campo vazio ou com caracteres inválidos.', 'impacto' => 'A linha é ignorada; não gera registro no banco.', 'passos' => [ 'Abra o CSV na linha indicada.', 'Preencha o ID legado ou remova a linha se for lixo/dado corrompido.', 'Reexecute: `php artisan licitacoes:import`.', ], 'coluna_csv' => 'cd_licitacao / cd_procedimento', ], 'COLUNAS_CSV_INVALIDAS' => [ 'titulo' => 'Linha do CSV com colunas quebradas', 'explicacao' => 'A quantidade de colunas da linha difere do cabeçalho (aspas, ponto-e-vírgula ou quebra de linha dentro de um campo).', 'impacto' => 'A linha inteira é ignorada.', 'passos' => [ 'Abra o CSV na linha indicada em um editor que respeite aspas (LibreOffice, Excel).', 'Corrija delimitadores ou envolva o campo problemático entre aspas duplas.', 'Reexecute: `php artisan licitacoes:import`.', ], 'coluna_csv' => null, ], 'DATA_INVALIDA' => [ 'titulo' => 'Data ou hora inválida', 'explicacao' => 'O valor de data/hora não pôde ser convertido para o formato do banco.', 'impacto' => 'O registro ou upload não é gravado.', 'passos' => [ 'Na linha indicada, use formato `YYYY-MM-DD` ou `DD/MM/YYYY` (e hora `HH:MM` se aplicável).', 'Reexecute: `php artisan licitacoes:import`.', ], 'coluna_csv' => 'dt_data / dt_hora', ], 'ERRO_PERSISTENCIA' => [ 'titulo' => 'Falha ao gravar no banco', 'explicacao' => 'O registro passou na leitura do CSV, mas a inserção no MySQL falhou.', 'impacto' => 'O registro não entra no portal.', 'passos' => [ 'Veja o **Detalhe técnico** abaixo e a linha/ID legado indicados.', 'Corrija o dado no CSV conforme a mensagem do banco.', 'Reexecute: `php artisan licitacoes:import`.', ], 'coluna_csv' => null, ], 'OUTRO' => [ 'titulo' => 'Outra ocorrência', 'explicacao' => 'Evento registrado durante a importação que não se encaixa nas categorias padrão.', 'impacto' => 'Depende do tipo (erro = não importado; ignorado = linha descartada).', 'passos' => [ 'Consulte a linha e o detalhe técnico neste relatório.', 'Reexecute: `php artisan licitacoes:import` após correção.', ], 'coluna_csv' => null, ], ]; /** @var array<string, string> */ private const COLUNA_BANCO_PARA_CSV = [ 'ds_objeto' => 'ds_objeto', 'nr_licitacao' => 'nr_licitacao / nr_procedimento', 'nr_processo' => 'nr_processo', 'ds_arquivo' => 'ds_arquivo', 'ds_legenda' => 'ds_legenda', 'dt_data' => 'dt_data', 'dt_hora' => 'dt_hora', 'id_setor' => 'id_setor', ]; /** * @param array<string, array{imported: int, skipped: int, errors: int}> $results * @param array<int, array<string, mixed>> $issues * * @return array{json: string, md: string, timestamp: string, resumo_console: string[]} */ public static function persist( array $results, array $issues, string $basePath, float $duracaoSegundos, ?string $only, ): array { $timestamp = now()->format('Ymd_His'); $dir = base_path(self::REPORT_DIR); if (!is_dir($dir)) { mkdir($dir, 0755, true); } $issuesEnriquecidas = array_map( fn (array $issue): array => self::enriquecerIssue($issue), $issues, ); $grupos = self::agruparPorCodigo($issuesEnriquecidas); $totais = self::totaisPorTipo($issuesEnriquecidas); $payload = [ 'gerado_em' => now()->toIso8601String(), 'duracao_segundos' => round($duracaoSegundos, 2), 'origem_csv' => $basePath, 'only' => $only, 'contadores' => $results, 'totais' => $totais, 'resultado' => self::resultadoGeral($totais), 'acao_recomendada' => self::acaoRecomendada($grupos, $totais), 'grupos' => $grupos, 'issues' => $issuesEnriquecidas, ]; $jsonPath = $dir . DIRECTORY_SEPARATOR . "relatorio-{$timestamp}.json"; $mdPath = $dir . DIRECTORY_SEPARATOR . "relatorio-{$timestamp}.md"; file_put_contents( $jsonPath, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), ); file_put_contents($mdPath, self::renderMarkdown($payload)); return [ 'json' => $jsonPath, 'md' => $mdPath, 'timestamp' => $timestamp, 'resumo_console' => self::resumoConsole($payload), ]; } /** * @param array<string, mixed> $payload * * @return string[] */ public static function resumoConsole(array $payload): array { $linhas = []; $totais = $payload['totais'] ?? ['erros' => 0, 'ignorados' => 0]; $grupos = $payload['grupos'] ?? []; $resultado = $payload['resultado'] ?? ''; $linhas[] = $resultado; if (($totais['erros'] ?? 0) === 0 && ($totais['ignorados'] ?? 0) === 0) { $linhas[] = 'Nenhum erro ou ignorado registrado.'; return $linhas; } $linhas[] = sprintf( 'Resumo: %d erro(s), %d ignorado(s) em %d grupo(s) distinto(s).', $totais['erros'] ?? 0, $totais['ignorados'] ?? 0, count($grupos), ); foreach ($grupos as $grupo) { $linhas[] = sprintf( ' • [%s] %s — %d ocorrência(s)', $grupo['codigo'], $grupo['titulo'], $grupo['total'], ); } $acao = $payload['acao_recomendada'] ?? []; if ($acao !== []) { $linhas[] = 'Próximo passo: ' . ($acao[0] ?? ''); } return $linhas; } /** * @param array<int, array<string, mixed>> $issues * * @return array{erros: int, ignorados: int} */ private static function totaisPorTipo(array $issues): array { $erros = 0; $ignorados = 0; foreach ($issues as $issue) { if (($issue['tipo'] ?? '') === 'erro') { $erros++; } else { $ignorados++; } } return ['erros' => $erros, 'ignorados' => $ignorados]; } /** * @param array{erros: int, ignorados: int} $totais */ private static function resultadoGeral(array $totais): string { $erros = $totais['erros'] ?? 0; $ignorados = $totais['ignorados'] ?? 0; if ($erros === 0 && $ignorados === 0) { return 'Importação concluída sem erros nem linhas ignoradas.'; } if ($erros > 0 && $ignorados > 0) { return sprintf( 'Importação concluída com ressalvas: %d erro(s) bloquearam registro(s) e %d linha(s) foram ignoradas.', $erros, $ignorados, ); } if ($erros > 0) { return sprintf('Importação concluída com ressalvas: %d erro(s) bloquearam registro(s).', $erros); } return sprintf('Importação concluída com ressalvas: %d linha(s) ignorada(s).', $ignorados); } /** * @param array<int, array<string, mixed>> $grupos * @param array{erros: int, ignorados: int} $totais * * @return string[] */ private static function acaoRecomendada(array $grupos, array $totais): array { if (($totais['erros'] ?? 0) === 0 && ($totais['ignorados'] ?? 0) === 0) { return []; } $passos = []; $temErroProcesso = false; $temUploadSemPai = false; foreach ($grupos as $grupo) { if (in_array($grupo['codigo'], ['CAMPO_OBRIGATORIO_VAZIO', 'CAMPO_MUITO_LONGO', 'DATA_INVALIDA', 'ERRO_PERSISTENCIA'], true)) { $temErroProcesso = true; } if ($grupo['codigo'] === 'UPLOAD_SEM_PROCESSO_PAI') { $temUploadSemPai = true; } } if ($temErroProcesso) { $passos[] = 'Corrija no CSV os registros principais listados na seção **Erros** (comece pelos grupos com maior contagem).'; } if ($temUploadSemPai && $temErroProcesso) { $passos[] = 'Os uploads ignorados provavelmente somem após corrigir os processos pais — não é necessário editar o CSV de uploads primeiro.'; } elseif ($temUploadSemPai) { $passos[] = 'Verifique se o `cd_licitacao` / `cd_procedimento` do upload existe no CSV principal.'; } $passos[] = 'Salve os CSVs e execute novamente: `php artisan licitacoes:import` (idempotente — limpa e reimporta).'; $passos[] = 'Abra o relatório `.md` gerado nesta pasta para ver linha, ID legado e coluna exata de cada ocorrência.'; return $passos; } /** * @param array<string, mixed> $issue * * @return array<string, mixed> */ private static function enriquecerIssue(array $issue): array { $mensagem = (string) ($issue['mensagem'] ?? ''); $tipo = (string) ($issue['tipo'] ?? 'ignorado'); $etapa = (string) ($issue['etapa'] ?? ''); $contexto = is_array($issue['contexto'] ?? null) ? $issue['contexto'] : []; $classificacao = self::classificar($mensagem, $tipo, $etapa, $contexto); $campoBanco = $classificacao['campo_banco']; $colunaCsv = $campoBanco !== null ? (self::COLUNA_BANCO_PARA_CSV[$campoBanco] ?? $campoBanco) : ($classificacao['coluna_csv'] ?? null); $issue['codigo'] = $classificacao['codigo']; $issue['titulo'] = $classificacao['titulo']; $issue['explicacao'] = $classificacao['explicacao']; $issue['impacto'] = $classificacao['impacto']; $issue['passos'] = $classificacao['passos']; $issue['campo_banco'] = $campoBanco; $issue['coluna_csv'] = $colunaCsv; $issue['id_legado'] = self::extrairIdLegado($issue, $contexto); $issue['detalhe_tecnico'] = self::resumirMensagemTecnica($mensagem); $issue['tipo_registro'] = self::tipoRegistro($etapa, $contexto); $issue['referencia_curta'] = self::referenciaCurta($issue, $contexto); return $issue; } /** * @param array<string, mixed> $contexto * * @return array{codigo: string, titulo: string, explicacao: string, impacto: string, passos: string[], coluna_csv: string|null, campo_banco: string|null} */ private static function classificar(string $mensagem, string $tipo, string $etapa, array $contexto): array { $campoBanco = null; $codigo = 'OUTRO'; if (str_contains($mensagem, 'quantidade de colunas incompativel') || str_contains($mensagem, 'colunas incompativeis')) { $codigo = 'COLUNAS_CSV_INVALIDAS'; } elseif (str_contains($mensagem, 'ausente ou invalido') || str_contains($mensagem, 'ausente, ignorada')) { $codigo = 'ID_LEGADO_INVALIDO'; } elseif (str_contains($mensagem, 'sem mapeamento para')) { $codigo = 'UPLOAD_SEM_PROCESSO_PAI'; } elseif (preg_match('/Column \'([^\']+)\' cannot be null/i', $mensagem, $m)) { $codigo = 'CAMPO_OBRIGATORIO_VAZIO'; $campoBanco = $m[1]; } elseif (preg_match('/Data too long for column \'([^\']+)\'/i', $mensagem, $m)) { $codigo = 'CAMPO_MUITO_LONGO'; $campoBanco = $m[1]; } elseif (str_contains($mensagem, 'Incorrect datetime value') || str_contains($mensagem, '1292')) { $codigo = 'DATA_INVALIDA'; } elseif ($tipo === 'erro' && str_contains($mensagem, 'Falha ao inserir')) { $codigo = 'ERRO_PERSISTENCIA'; } $base = self::CLASSIFICACOES[$codigo] ?? self::CLASSIFICACOES['OUTRO']; $titulo = $base['titulo']; if ($campoBanco !== null && $codigo === 'CAMPO_OBRIGATORIO_VAZIO') { $titulo = "Campo obrigatório vazio: {$campoBanco}"; } if ($campoBanco !== null && $codigo === 'CAMPO_MUITO_LONGO') { $titulo = "Texto excede limite: {$campoBanco}"; } $passos = $base['passos']; if ($campoBanco !== null && $coluna = (self::COLUNA_BANCO_PARA_CSV[$campoBanco] ?? $campoBanco)) { $passos = array_merge( ["No CSV, preencha a coluna **{$coluna}** (campo do banco: `{$campoBanco}`)."], $passos, ); } return [ 'codigo' => $codigo, 'titulo' => $titulo, 'explicacao' => $base['explicacao'], 'impacto' => $base['impacto'], 'passos' => $passos, 'coluna_csv' => $base['coluna_csv'], 'campo_banco' => $campoBanco, ]; } /** * @param array<string, mixed> $issue * @param array<string, mixed> $contexto */ private static function extrairIdLegado(array $issue, array $contexto): ?int { if (isset($contexto['old_id'])) { return (int) $contexto['old_id']; } if (isset($contexto['old_processo_id'])) { return (int) $contexto['old_processo_id']; } $ref = (string) ($issue['referencia'] ?? ''); if (preg_match('/(?:old_id|cd_licitacao|cd_procedimento|old_processo_id)=(\d+)/', $ref, $m)) { return (int) $m[1]; } return null; } /** * @param array<string, mixed> $contexto */ private static function tipoRegistro(string $etapa, array $contexto): string { if (str_starts_with($etapa, 'uploads_')) { return str_contains($etapa, 'licitacoes') ? 'Upload de licitação' : 'Upload de procedimento'; } if ($etapa === 'licitacoes' || ($contexto['tipo_processo'] ?? '') === 'licitacao') { return 'Licitação'; } if ($etapa === 'procedimentos' || ($contexto['tipo_processo'] ?? '') === 'procedimento') { return 'Procedimento'; } return ucfirst(str_replace('_', ' ', $etapa)); } /** * @param array<string, mixed> $issue * @param array<string, mixed> $contexto */ private static function referenciaCurta(array $issue, array $contexto): string { $partes = []; if ($id = self::extrairIdLegado($issue, $contexto)) { $partes[] = "ID legado {$id}"; } if (!empty($contexto['nr_licitacao'])) { $partes[] = 'Nº ' . $contexto['nr_licitacao']; } if (!empty($contexto['nr_processo'])) { $partes[] = 'Processo ' . $contexto['nr_processo']; } if (!empty($contexto['ds_arquivo'])) { $partes[] = 'Arquivo: ' . $contexto['ds_arquivo']; } if (!empty($contexto['id_upload'])) { $partes[] = 'id_upload ' . $contexto['id_upload']; } return $partes !== [] ? implode(' · ', $partes) : ((string) ($issue['referencia'] ?? '—')); } private static function resumirMensagemTecnica(string $mensagem): string { if (preg_match('/SQLSTATE\[(\w+)\]:?\s*([^(\n]+)/i', $mensagem, $m)) { $resumo = trim($m[2]); if (preg_match('/Column \'([^\']+)\' cannot be null/i', $mensagem, $col)) { return "MySQL: coluna `{$col[1]}` não aceita NULL ({$m[1]})"; } if (preg_match('/Data too long for column \'([^\']+)\'/i', $mensagem, $col)) { return "MySQL: valor longo demais para `{$col[1]}` ({$m[1]})"; } return "MySQL ({$m[1]}): {$resumo}"; } if (strlen($mensagem) > 200) { return mb_substr($mensagem, 0, 197) . '...'; } return $mensagem; } /** * @param array<int, array<string, mixed>> $issues * * @return array<int, array<string, mixed>> */ private static function agruparPorCodigo(array $issues): array { $mapa = []; foreach ($issues as $issue) { $codigo = (string) ($issue['codigo'] ?? 'OUTRO'); if (!isset($mapa[$codigo])) { $mapa[$codigo] = [ 'codigo' => $codigo, 'titulo' => $issue['titulo'] ?? $codigo, 'tipo' => ($issue['tipo'] ?? '') === 'erro' ? 'erro' : 'ignorado', 'explicacao' => $issue['explicacao'] ?? '', 'impacto' => $issue['impacto'] ?? '', 'passos' => $issue['passos'] ?? [], 'coluna_csv' => $issue['coluna_csv'] ?? null, 'campo_banco' => $issue['campo_banco'] ?? null, 'total' => 0, 'itens' => [], ]; } if (($issue['tipo'] ?? '') === 'erro') { $mapa[$codigo]['tipo'] = 'erro'; } $mapa[$codigo]['total']++; $mapa[$codigo]['itens'][] = [ 'tipo_registro' => $issue['tipo_registro'] ?? null, 'id_legado' => $issue['id_legado'] ?? null, 'referencia_curta' => $issue['referencia_curta'] ?? null, 'arquivo' => $issue['arquivo'] ?? null, 'linha' => $issue['linha'] ?? null, 'coluna_csv' => $issue['coluna_csv'] ?? null, 'detalhe_tecnico' => $issue['detalhe_tecnico'] ?? null, 'contexto' => $issue['contexto'] ?? [], ]; } usort($mapa, fn (array $a, array $b): int => $b['total'] <=> $a['total']); return array_values($mapa); } /** * @param array<string, mixed> $payload */ private static function renderMarkdown(array $payload): string { $linhas = []; $grupos = $payload['grupos'] ?? []; $contadores = $payload['contadores'] ?? []; $totais = $payload['totais'] ?? ['erros' => 0, 'ignorados' => 0]; $acao = $payload['acao_recomendada'] ?? []; $linhas[] = '# Relatório de importação — Licitações e Procedimentos'; $linhas[] = ''; $linhas[] = '## Resultado'; $linhas[] = ''; $linhas[] = '**' . ($payload['resultado'] ?? '') . '**'; $linhas[] = ''; $linhas[] = '| | |'; $linhas[] = '| --- | --- |'; $linhas[] = '| Gerado em | ' . ($payload['gerado_em'] ?? '') . ' |'; $linhas[] = '| Duração | ' . ($payload['duracao_segundos'] ?? 0) . ' s |'; $linhas[] = '| Origem CSV | `' . ($payload['origem_csv'] ?? '') . '` |'; $linhas[] = '| Modo | `' . ($payload['only'] ?? 'completo') . '` |'; $linhas[] = '| Erros | **' . ($totais['erros'] ?? 0) . '** |'; $linhas[] = '| Ignorados | **' . ($totais['ignorados'] ?? 0) . '** |'; $linhas[] = ''; $linhas[] = '## Contadores por etapa'; $linhas[] = ''; $linhas[] = '| Etapa | Importados | Ignorados | Erros |'; $linhas[] = '| --- | ---: | ---: | ---: |'; foreach ($contadores as $etapa => $stats) { $linhas[] = sprintf( '| %s | %d | %d | %d |', $etapa, $stats['imported'] ?? 0, $stats['skipped'] ?? 0, $stats['errors'] ?? 0, ); } if ($acao !== []) { $linhas[] = ''; $linhas[] = '## O que fazer agora'; $linhas[] = ''; foreach ($acao as $i => $passo) { $linhas[] = ($i + 1) . '. ' . $passo; } } if ($grupos === []) { $linhas[] = ''; $linhas[] = '_Nenhum erro ou ignorado detalhado nesta execução._'; return implode(PHP_EOL, $linhas) . PHP_EOL; } $linhas[] = ''; $linhas[] = '## Resumo por tipo de problema'; $linhas[] = ''; $linhas[] = '| Código | Problema | Tipo | Qtd | Coluna CSV |'; $linhas[] = '| --- | --- | --- | ---: | --- |'; foreach ($grupos as $grupo) { $linhas[] = sprintf( '| `%s` | %s | %s | %d | %s |', $grupo['codigo'], self::escaparCelula($grupo['titulo']), $grupo['tipo'] === 'erro' ? 'Erro' : 'Ignorado', $grupo['total'], self::escaparCelula($grupo['coluna_csv'] ?? $grupo['campo_banco'] ?? '—'), ); } foreach ($grupos as $index => $grupo) { $num = $index + 1; $linhas[] = ''; $linhas[] = "---"; $linhas[] = ''; $linhas[] = "## {$num}. {$grupo['titulo']} (`{$grupo['codigo']}`) — {$grupo['total']} ocorrência(s)"; $linhas[] = ''; $linhas[] = '**O que aconteceu:** ' . ($grupo['explicacao'] ?? ''); $linhas[] = ''; $linhas[] = '**Impacto:** ' . ($grupo['impacto'] ?? ''); $linhas[] = ''; $linhas[] = '**Como corrigir:**'; foreach ($grupo['passos'] as $i => $passo) { $linhas[] = ($i + 1) . '. ' . $passo; } $linhas[] = ''; $linhas[] = '### Registros afetados'; $linhas[] = ''; $linhas[] = '| # | Tipo | ID legado | Referência | Arquivo | Linha CSV | Detalhe |'; $linhas[] = '| ---: | --- | ---: | --- | --- | ---: | --- |'; foreach ($grupo['itens'] as $i => $item) { $linhas[] = sprintf( '| %d | %s | %s | %s | %s | %s | %s |', $i + 1, self::escaparCelula($item['tipo_registro'] ?? null), self::escaparCelula(isset($item['id_legado']) ? (string) $item['id_legado'] : null), self::escaparCelula($item['referencia_curta'] ?? null), self::escaparCelula($item['arquivo'] ?? null), self::escaparCelula(isset($item['linha']) ? (string) $item['linha'] : null), self::escaparCelula($item['detalhe_tecnico'] ?? null), ); } if ($grupo['codigo'] === 'UPLOAD_SEM_PROCESSO_PAI') { $linhas[] = ''; $linhas[] = self::renderUploadsAgrupadosPorPai($grupo['itens']); } } $linhas[] = ''; $linhas[] = '## Comandos'; $linhas[] = ''; $linhas[] = '```bash'; $linhas[] = 'php artisan licitacoes:import'; $linhas[] = 'php artisan licitacoes:import --only=licitacoes'; $linhas[] = 'php artisan licitacoes:import --only=uploads'; $linhas[] = '```'; return implode(PHP_EOL, $linhas) . PHP_EOL; } /** * @param array<int, array<string, mixed>> $itens */ private static function renderUploadsAgrupadosPorPai(array $itens): string { $porPai = []; foreach ($itens as $item) { $pai = $item['id_legado'] ?? 0; $porPai[$pai] = ($porPai[$pai] ?? 0) + 1; } arsort($porPai); $linhas = []; $linhas[] = '**Processos pais distintos sem importação:** ' . count($porPai); $linhas[] = ''; $linhas[] = '| ID legado (pai) | Uploads ignorados |'; $linhas[] = '| ---: | ---: |'; $limite = 20; $n = 0; foreach ($porPai as $pai => $qtd) { if ($n >= $limite) { $linhas[] = '| … | … |'; break; } $linhas[] = "| {$pai} | {$qtd} |"; $n++; } return implode(PHP_EOL, $linhas); } private static function escaparCelula(mixed $valor): string { if ($valor === null || $valor === '') { return '—'; } return str_replace('|', '\\|', (string) $valor); } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings