Introduzione
Quando si lavora con JSON, il formato più comune consiste in un singolo documento contenente un oggetto o un array completo. Questa soluzione è comoda, ma può diventare poco efficiente quando il file contiene milioni di elementi: per decodificarlo interamente con json_decode(), PHP deve caricare tutto il contenuto in memoria.
Un’alternativa pratica è il formato JSON Lines, chiamato anche NDJSON. In questo formato ogni riga del file contiene un oggetto JSON indipendente. Un file può quindi apparire così:
{"id":1,"email":"[email protected]","attivo":true}
{"id":2,"email":"[email protected]","attivo":false}
{"id":3,"email":"[email protected]","attivo":true} Questo approccio è utile per importazioni, esportazioni, log applicativi, elaborazioni batch e trasferimenti di dati. PHP può leggere una riga alla volta, decodizzarla, elaborarla e poi liberare la memoria prima di passare alla riga successiva.
In questo tutorial realizzeremo uno script che:
- legge un file JSON Lines senza caricarlo completamente in memoria;
- ignora le righe vuote;
- rileva le righe JSON non valide;
- seleziona solo gli utenti attivi;
- scrive il risultato in un nuovo file JSON Lines;
- registra gli errori in un file separato.
Codice completo
Supponiamo di avere un file denominato utenti.ndjson. Il seguente script, chiamato elabora_utenti.php, legge il file progressivamente e produce utenti_attivi.ndjson.
<?php
declare(strict_types=1);
$inputPath = __DIR__ . ´/utenti.ndjson´;
$outputPath = __DIR__ . ´/utenti_attivi.ndjson´;
$errorPath = __DIR__ . ´/errori_json.log´;
if (!is_readable($inputPath)) {
throw new RuntimeException(
"Il file di input non è leggibile: {$inputPath}"
);
}
$inputHandle = fopen($inputPath, ´rb´);
$outputHandle = fopen($outputPath, ´wb´);
$errorHandle = fopen($errorPath, ´ab´);
if ($inputHandle === false || $outputHandle === false || $errorHandle === false) {
throw new RuntimeException(´Impossibile aprire uno dei file richiesti.´);
}
$lineNumber = 0;
$processed = 0;
$written = 0;
$invalid = 0;
try {
while (($line = fgets($inputHandle)) !== false) {
$lineNumber++;
// Rimuove spazi, tab e caratteri di fine riga.
$line = trim($line);
// Le righe vuote non rappresentano dati.
if ($line === ´´) {
continue;
}
try {
// Converte ogni riga in un array associativo.
$user = json_decode(
$line,
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($user)) {
throw new UnexpectedValueException(
´La riga non contiene un oggetto JSON.´
);
}
$processed++;
// Verifica minima dei campi necessari.
$hasId = isset($user[´id´]) && is_int($user[´id´]);
$hasEmail = isset($user[´email´]) && is_string($user[´email´]);
$isActive = ($user[´attivo´] ?? false) === true;
if (!$hasId || !$hasEmail || !$isActive) {
continue;
}
$result = [
´id´ => $user[´id´],
´email´ => strtolower(trim($user[´email´])),
´attivo´ => true
];
$json = json_encode(
$result,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
);
fwrite($outputHandle, $json . PHP_EOL);
$written++;
} catch (JsonException | UnexpectedValueException $exception) {
$invalid++;
$message = sprintf(
"Riga %d non valida: %s%s",
$lineNumber,
$exception->getMessage(),
PHP_EOL
);
fwrite($errorHandle, $message);
}
}
} finally {
fclose($inputHandle);
fclose($outputHandle);
fclose($errorHandle);
}
echo "Righe elaborate: {$processed}" . PHP_EOL;
echo "Utenti scritti: {$written}" . PHP_EOL;
echo "Righe non valide: {$invalid}" . PHP_EOL; Per eseguire lo script da terminale:
php elabora_utenti.php Il file di output conterrà esclusivamente gli utenti attivi, uno per riga:
{"id":1,"email":"[email protected]","attivo":true}
{"id":3,"email":"[email protected]","attivo":true} Spiegazione
Lettura progressiva del file
La funzione fopen() apre il file in modalità binaria di sola lettura. Successivamente, fgets() restituisce una riga alla volta. In questo modo il consumo di memoria rimane generalmente stabile, anche se il file contiene molti gigabyte.
Il ciclo termina quando fgets() restituisce false. La chiamata a trim() elimina i caratteri di fine riga e gli spazi inutili. Le righe vuote vengono ignorate perché non contengono un documento JSON da elaborare.
Decodifica sicura
Usando JSON_THROW_ON_ERROR, PHP solleva una JsonException quando la riga contiene JSON non valido. È preferibile rispetto al controllo manuale di json_last_error(), soprattutto dentro cicli lunghi: l’errore viene associato direttamente all’operazione che lo ha generato.
Il secondo parametro di json_decode() è impostato a true, quindi gli oggetti JSON vengono convertiti in array associativi. In seguito lo script verifica che siano presenti un identificativo intero, un indirizzo email testuale e il flag attivo impostato a true.
Produzione di nuovo JSON Lines
Ogni record filtrato viene trasformato in un nuovo array. json_encode() lo converte in una stringa JSON, mentre PHP_EOL aggiunge il separatore necessario per il formato JSON Lines. L’output viene scritto immediatamente: non è necessario accumulare i risultati in un grande array.
Gestione degli errori
Una riga errata non interrompe l’intera importazione. L’eccezione viene intercettata, il numero della riga viene registrato in errori_json.log e il ciclo continua con il record successivo. Questa strategia è adatta ai processi batch, dove è spesso preferibile completare l’elaborazione e analizzare gli errori in un secondo momento.
Best practice
- Utilizza una riga per oggetto: non inserire un array JSON globale nel file, altrimenti perderesti il vantaggio dell’elaborazione progressiva.
- Usa
JSON_THROW_ON_ERROR: evita risultati ambigui quando il contenuto è corrotto. - Valida i dati dopo la decodifica: un JSON sintatticamente corretto può comunque contenere campi mancanti o tipi errati.
- Non registrare dati sensibili nei log: salva il numero della riga e il motivo dell’errore, ma evita password, token e informazioni personali non necessarie.
- Controlla i valori restituiti da
fopen()efwrite(): permessi insufficienti o disco pieno possono compromettere il risultato. - Normalizza i dati: nell’esempio l’email viene convertita in minuscolo e privata degli spazi iniziali e finali.
- Considera la concorrenza: se più processi scrivono sullo stesso file, utilizza un sistema di lock o file separati per evitare righe interrotte.
- Usa estensioni appropriate: puoi adottare
.ndjsono.jsonl, documentando chiaramente il formato atteso.
Riepilogo
JSON Lines permette di trattare grandi quantità di dati JSON in modo incrementale. In PHP è sufficiente combinare fopen(), fgets(), json_decode() e json_encode() per costruire processi efficienti e facili da monitorare.
Il punto fondamentale è considerare ogni riga come un documento indipendente: se una riga è invalida, può essere registrata senza bloccare necessariamente l’intero flusso. Questa caratteristica rende il formato particolarmente adatto a importazioni, log e processi ETL.
