Pular para o conteúdo
murad.guide

PHP

Capítulo 15

Guardar uma visita em arquivo local

6 min de leitura 1.064 palavras 1 gravura

Neste capítulo você vai acrescentar uma visita validada sem apagar a anterior.

Seu ponto de partida

Estude depois do capítulo 14. O exemplo deste capítulo é 15-gravar-json, no pacote que acompanha o volume. Copie a pasta inicio para um diretório de prática e preserve o original. A raiz do exemplo é a referência concluída desta etapa. Sua cópia permite experimentar sem perder a referência.

Execute o PHP na raiz da sua cópia com php -S 127.0.0.1:8000 -t public. Abra http://127.0.0.1:8000/. Não abra PHP por duplo clique nem use o servidor de Python. Encerre com Ctrl+C antes de trocar de etapa.

Como praticar

Leia a explicação e digite ou confira o código indicado. Mude uma coisa. Antes de conferir, anote sua previsão. Salve, recarregue e compare. Se não acontecer o esperado, investigue o erro antes de avançar. Cada experiência parte dos valores da referência. Depois de conferir, desfaça somente a mudança desse experimento antes de começar a próxima lição. Você pode parar depois de uma lição e retomar mais tarde.

Separe página pública e dados

A pasta public contém index.php e estilos.css. Dados ficam em dados, ao lado de public. Com -t public, o servidor entrega apenas o conteúdo de public. Ele não usa arquivos .htaccess para proteger a outra pasta.

DIR indica a pasta do script. O caminho fixo sobe um nível e escolhe dados/entradas.jsonl. Ele não é montado com o nome recebido no formulário. A pasta já acompanha a referência e a base de início.

Não mude o comando para servir a raiz do projeto. O isolamento depende da pasta escolhida como raiz pública. Esta separação é fácil de conferir: a tentativa de abrir /dados/entradas.jsonl não deve entregar os registros.

Faça no seu exemplo

Confira o caminho usado no início de public/index.php. A pasta dados fica fora de public.

$arquivo = __DIR__ . "/../dados/entradas.jsonl";

Mude uma coisa e preveja

Abra http://127.0.0.1:8000/dados/entradas.jsonl antes de qualquer envio.

Anote o que você acha que vai acontecer antes de salvar e recarregar.

Recarregue e confira

Você deve receber um erro 404. Confira no disco que dados não está dentro de public. Isso não é um comando para publicar o projeto.

Se não acontecer

Se o arquivo aparecer, encerre o teste e reinicie com -t public. Mover dados para public elimina a separação ensinada.

Conserve a mensagem com JSON

O registro guarda nome, mensagem e data. O e-mail é conferido no pedido, mas não precisa ser guardado no livro de visitas. JSON representa esses campos como texto de uma maneira que preserva aspas, tabulações e quebras de linha.

Json_encode transforma o array em uma linha JSON. Uma quebra de linha da mensagem fica representada dentro desse texto, em vez de criar outra visita. O final PHP_EOL separa os registros do arquivo. Esse formato de uma linha JSON por registro é chamado JSON Lines.

Separar campos com tabulações pode confundir uma mensagem que também contém tabulação ou quebra de linha. Você vai testar esses casos antes de confiar na gravação.

Faça no seu exemplo

Confira a montagem do registro antes de gravar.

$entrada = [
    "nome" => $valores["nome"],
    "mensagem" => $valores["mensagem"],
    "criada" => date("c"),
];
$linha = json_encode($entrada, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)
    . PHP_EOL;

Mude uma coisa e preveja

Envie uma mensagem fictícia com duas linhas, aspas e a palavra ação.

Anote o que você acha que vai acontecer antes de salvar e recarregar.

Recarregue e confira

Abra dados/entradas.jsonl no editor. Deve existir um registro JSON completo. As quebras internas são representadas no JSON e não viram outra visita.

Se não acontecer

Se cada linha da mensagem virar um registro, não grave o texto cru separado por tabulações. Compare json_encode e o PHP_EOL aplicado depois do JSON.

Acrescente e confira a gravação

File_put_contents escreve um arquivo. Sem FILE_APPEND, substitui o conteúdo existente. Com FILE_APPEND, acrescenta ao final. LOCK_EX pede um bloqueio exclusivo durante essa escrita, evitando misturar escritas cooperantes.

A função devolve a quantidade de bytes escritos ou false em caso de falha. O exemplo confere o resultado antes de confirmar. O @ é usado somente nesta operação, cuja falha é tratada logo depois, para não expor avisos com caminhos internos na página. Não use @ para esconder erros sem tratamento.

Este livro local não é um banco de dados e não oferece transações para um mural público. A prática testa um envio por vez, no servidor embutido de desenvolvimento.

Faça no seu exemplo

Confira a escrita em public/index.php. Use a referência completa para ver o tratamento de falha.

$bytes = @file_put_contents($arquivo, $linha, FILE_APPEND | LOCK_EX);
if ($bytes !== strlen($linha)) {
    throw new RuntimeException("Não foi possível guardar a visita.");
}

Mude uma coisa e preveja

Envie duas visitas fictícias com mensagens diferentes.

Anote o que você acha que vai acontecer antes de salvar e recarregar.

Recarregue e confira

O arquivo deve conservar dois registros. Ao abrir por GET, o formulário não acrescenta nada. Nesta etapa a confirmação aparece após o POST; evite recarregar esse POST, pois o redirecionamento virá no capítulo 18.

Se não acontecer

Se somente a segunda visita existir, confira FILE_APPEND. Se houver falha, confirme que a pasta dados existe e é gravável; não libere toda a máquina nem invente sucesso.

Trecho do resultado de referência do capítulo 15

A captura mostra um trecho da referência concluída. Abra e percorra o exemplo para conferir o restante. Experimentos temporários podem produzir outra aparência; restaure os valores indicados antes de comparar.

Atividades para fazer antes das respostas

Use uma nova cópia do exemplo. Tente aplicar o que aprendeu antes de consultar os comentários.

  1. Confira a posição de dados em relação a public.

  2. Envie duas visitas e conte os registros JSON.

  3. Envie mensagem com quebra de linha e tabulação.

Respostas comentadas

Compare sua explicação e o que realmente aconteceu. Outra solução pode funcionar se cumprir o objetivo e preservar o acesso ao conteúdo.

  1. Dados é uma pasta irmã de public e não pertence à raiz servida com -t public.

  2. Cada envio válido acrescenta uma linha JSON, sem substituir a anterior.

  3. JSON conserva esses caracteres dentro do campo. A etapa de leitura deve recuperar a mensagem completa.

Quando seguir

Você conserva visitas anteriores e distingue registro no disco de página mostrada no navegador. Se ainda precisa de ajuda, repita a experiência pequena antes de avançar.

Consulta opcional

Não é preciso ler os links externos para executar os arquivos fornecidos. Eles servem para consultar detalhes quando você quiser.

O arquivo completo

Referência do exemplo 15-gravar-json, reproduzida aqui para que você possa conferir sem sair da página.

apoio.php

<?php
declare(strict_types=1);

function escapar(string $texto): string
{
    return htmlspecialchars($texto, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}

function campo(array $dados, string $chave): string
{
    $valor = $dados[$chave] ?? "";
    if (!is_string($valor) || preg_match('//u', $valor) !== 1) {
        return "";
    }
    return trim($valor);
}

function tamanho(string $texto): int
{
    if (function_exists("mb_strlen")) {
        return mb_strlen($texto, "UTF-8");
    }
    return preg_match_all('/./us', $texto);
}

public/estilos.css

* { box-sizing: border-box; }
body { margin: 0; background: #f4f7fb; color: #203246;
  font: 1.05rem/1.65 Arial, sans-serif; }
header, main, footer { max-width: 48rem; margin-inline: auto; padding: 1.5rem; }
header { padding-top: 2.5rem; }
h1 { font-size: clamp(1.8rem, 5vw, 2.7rem); line-height: 1.2; }
main { background: #fff; border-radius: .8rem; }
article { border-top: 1px solid #cad4df; padding-block: 1rem; }
a { color: #124e77; text-underline-offset: .2em; }
nav { display: flex; flex-wrap: wrap; gap: 1rem; }
label { display: block; font-weight: bold; }
input:not([type=hidden]), textarea { width: 100%; max-width: 34rem;
  border: 2px solid #718095; border-radius: .35rem; padding: .7rem;
  font: inherit; color: inherit; background: #fff; }
button { font: inherit; background: #124e77; color: white;
  border: 0; border-radius: .35rem; padding: .75rem 1.2rem; cursor: pointer; }
button:hover { background: #103b59; }
.mensagem { white-space: pre-wrap; overflow-wrap: anywhere; }
.erros { color: #8b142c; }
.ok { color: #175c30; font-weight: bold; }
:focus-visible { outline: 3px solid #b04d00; outline-offset: 3px; }
.pular { position: absolute; left: -10000px; }
.pular:focus { position: static; display: block; padding: 1rem; background: white; }
@media (max-width: 35rem) { header,main,footer { padding: 1rem; } }