XML a JSON v PHP

PHP má tři hlavní způsoby, jak číst XML: jednoduchý SimpleXML, plnohodnotný DOMDocument a streamovací XMLReader pro obří soubory. Kapitola ukazuje, kdy který použít, jak v nich hledat přes XPath, pracovat se jmennými prostory, zachytit chyby a XML vytvořit.

Autor: Aktualizováno

Čtení přes SimpleXML

PHP
$xml = simplexml_load_file('objednavka.xml');       // nebo simplexml_load_string($text)

echo $xml['cislo'];                  // atribut: 2026-117
echo $xml->zakaznik;                 // text prvku: Jana Nováková

foreach ($xml->polozka as $polozka) {
    printf("%s × %d\n", $polozka, (int) $polozka['mnozstvi']);
}

// Pozor: hodnoty jsou objekty SimpleXMLElement, ne řetězce
$kod = (string) $xml->polozka[0]['kod'];    // bez přetypování by se porovnávaly objekty

Přístup $xml->polozka vrací první prvek a zároveň se přes něj dá iterovat. Neexistující prvek nevyhodí chybu, jen vrátí prázdný objekt — ověř ho přes isset($xml->prvek).

SimpleXML, DOMDocument, nebo XMLReader?

Všechny ukázky pracují s touto objednávkou:

objednavka.xml
<?xml version="1.0" encoding="UTF-8"?>
<objednavka cislo="2026-117">
  <zakaznik>Jana Nováková</zakaznik>
  <polozka kod="HR-01" mnozstvi="2">Hrnek</polozka>
  <polozka kod="TA-07" mnozstvi="1">Talíř</polozka>
</objednavka>
SimpleXMLDOMDocumentXMLReader
Styl práceprvky jako vlastnosti objektustandardní DOM API (jako v JavaScriptu)kurzor, čte uzel po uzlu
Paměťcelý dokumentcelý dokumentjen aktuální uzel
XPathxpath() — jen uzlyDOMXPath — uzly i hodnotyne (jen přes převod záznamu)
Úpravypřidání prvků a atributůcokoli, včetně mazání a přesunůjen čtení (zápis XMLWriter)
Kdy použítrychlé čtení menších souborů a API odpovědíúpravy, komentáře, CDATA, validacesoubory ve stovkách MB, feedy

SimpleXML i DOM stojí na stejné knihovně libxml2 a dají se kombinovat: dom_import_simplexml() a simplexml_import_dom() převedou prvek z jednoho API do druhého bez kopírování.

Chyby parsování

PHP
libxml_use_internal_errors(true);            // místo varování sbírat chyby
$xml = simplexml_load_string($text);

if ($xml === false) {
    foreach (libxml_get_errors() as $e) {
        printf("Řádek %d, sloupec %d: %s", $e->line, $e->column, trim($e->message));
    }
    libxml_clear_errors();
}

Vytvoření XML: XMLWriter

PHP
$w = new XMLWriter();
$w->openUri('export.xml');                   // nebo openMemory() a outputMemory()
$w->setIndent(true);
$w->startDocument('1.0', 'UTF-8');
$w->startElement('objednavka');
$w->writeAttribute('cislo', '2026-118');
$w->writeElement('zakaznik', 'Petr & syn');  // & se zapíše jako &amp;
$w->endElement();
$w->endDocument();
$w->flush();

Řetězce neskládej ručně ("<a>$hodnota</a>") — jeden znak & nebo < v datech udělá z výsledku nevalidní XML.

Serializace XML

Při převodu XML stromu na text (serializaci) knihovna zapíše zvláštní znaky podle pravidel XML, například ampersand jako &amp;. Zápis zpět nemusí zachovat původní prefixy, odsazení, CDATA nebo pořadí atributů; testuj obsah a požadované kódování.

$text = $doc->saveXML();
if ($text === false) { throw new RuntimeException("Serializace selhala"); }

Čtení a úpravy přes DOMDocument

PHP
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->load('objednavka.xml');                      // nebo loadXML($text)

$root = $doc->documentElement;
echo $root->getAttribute('cislo');                 // 2026-117

foreach ($doc->getElementsByTagName('polozka') as $polozka) {
    echo $polozka->textContent, ' ', $polozka->getAttribute('mnozstvi'), "\n";
}

// Úpravy: přidat prvek a uložit
$nova = $doc->createElement('polozka', 'Miska');
$nova->setAttribute('kod', 'MI-03');
$root->appendChild($nova);
$doc->formatOutput = true;
$doc->save('objednavka-nova.xml');

Velké soubory: XMLReader

PHP
// Velký soubor (feed se 100 000 produkty) po jednom záznamu, bez načtení celého do paměti
$reader = XMLReader::open('feed.xml');
while ($reader->read()) {
    if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'SHOPITEM') {
        $item = simplexml_load_string($reader->readOuterXml());   // jeden záznam pohodlně
        uloz((string) $item->PRODUCTNAME, (float) $item->PRICE_VAT);
    }
}
$reader->close();

Kombinace kurzoru a simplexml_load_string() na jeden záznam drží v paměti jen malý kus dokumentu, a přitom se se záznamem pracuje pohodlně.

Vyhledávání přes XPath

PHP
// SimpleXML: xpath() vrací pole prvků
foreach ($xml->xpath('//polozka[@mnozstvi > 1]') as $p) {
    echo $p['kod'], "\n";                        // HR-01
}

// DOM: DOMXPath umí i výrazy, které vrací číslo nebo text
$xp = new DOMXPath($doc);
$pocet = $xp->evaluate('count(//polozka)');       // 2.0 (float)
$prvni = $xp->evaluate('string(//polozka[1]/@kod)'); // "HR-01"
foreach ($xp->query('//polozka', $doc) as $uzel) { /* DOMElement */ }

PHP podporuje XPath 1.0. Funkce z novějších verzí (matches(), string-join()) v něm nejsou — výraz si proto před vložením do kódu vyzkoušej v testeru s volbou XPath 1.0.

Jmenné prostory

PHP
$text = '<f:faktura xmlns:f="http://example.cz/faktura"><f:cislo>FV2026-042</f:cislo></f:faktura>';

// SimpleXML: prefix je třeba zaregistrovat — v dotazu může být i jiný než v dokumentu
$xml = simplexml_load_string($text);
$xml->registerXPathNamespace('fa', 'http://example.cz/faktura');
echo $xml->xpath('//fa:cislo')[0];                 // FV2026-042
echo $xml->children('http://example.cz/faktura')->cislo;

// DOM
$xp = new DOMXPath($doc);
$xp->registerNamespace('fa', 'http://example.cz/faktura');
$xp->query('//fa:cislo');

Nejčastější chyba: dokument má výchozí jmenný prostor (xmlns="…" bez prefixu) a dotaz //cislo nic nenajde. V XPath 1.0 nemá výraz bez prefixu žádný jmenný prostor, takže je potřeba prefix zaregistrovat a psát //fa:cislo.

Validace proti XSD

PHP
$doc = new DOMDocument();
$doc->load('objednavka.xml');
libxml_use_internal_errors(true);

if (!$doc->schemaValidate('objednavka.xsd')) {
    foreach (libxml_get_errors() as $e) {
        echo "Řádek {$e->line}: ", trim($e->message), "\n";
    }
}

Bezpečnost: u XML z cizích zdrojů nepoužívej příznaky LIBXML_NOENT a LIBXML_DTDLOAD — otevírají útok XXE.

Bezpečné nastavení parseru

Nepovoluj LIBXML_NOENT ani načítání DTD pro cizí data. LIBXML_NONET blokuje síť, nikoli všechny soubory; podle verze PHP/libxml zvaž LIBXML_NO_XXE. Omez vstup a kontroluj chyby. Podrobné varianty jsou v XXE.

Načtení JSON

JSON parser vrací datovou strukturu, ne XML strom. Chyby syntaxe zpracuj jako chyby vstupu; data ověř také strukturálně.

$text = '{"jmeno":"Jana","vek":20}';
$data = json_decode($text, true, 512, JSON_THROW_ON_ERROR);
echo $data['jmeno'];

Vytvoření JSON

Vytvoř hodnotu v paměti a nech její textový zápis na knihovně.

$data = ['jmeno' => 'Jana', 'vek' => 20, 'aktivni' => true];

Serializace JSON

Neskládej text ručně. Ověř přesnost čísel, reprezentaci dat a chování nečíselných hodnot.

$text = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

JSON Schema

Standardní knihovna neposkytuje plnou validaci JSON Schema. Vyber knihovnu s podporou požadované verze pravidel JSON Schema z oficiální dokumentace či katalogu. PHP může použít opis/json-schema. Základy a rozdíly verzí vysvětluje kurz JSON Schema.

Dokumentace API

Příklady JSON vycházejí z oficiální dokumentace jazyka. Překlady typů, maximální hloubka a chování při chybě jsou vlastnosti knihovny, ne samotné syntaxe JSON.

JSON Schema: kompletní příklad Opis 2.x

schema.php — composer require opis/json-schema:^2
<?php
require 'vendor/autoload.php';
$schema = json_decode('{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","required":["id"],"properties":{"id":{"type":"string"}},"additionalProperties":false}', false, 512, JSON_THROW_ON_ERROR);
$validator = new Opis\JsonSchema\Validator();
foreach (['{"id":"001"}', '{"id":1}'] as $text) {
    $data = json_decode($text, false, 512, JSON_THROW_ON_ERROR);
    $result = $validator->validate($data, $schema);
    echo $result->isValid() ? "platné\n" : "neplatné\n";
}

Spusť php schema.php: první záznam projde a druhý ne. JSON objekty načítáme jako stdClass, protože validátor musí odlišit objekt od PHP pole. Asociativní pole z json_decode s true nejsou stejná reprezentace pro tento postup. Syntax chyba vyvolá JsonException ještě před schématem.

Validátor i schéma znovu používej. Podrobné chyby můžeš formátovat pomocí ErrorFormatter podle oficiální dokumentace Opis. Dále kontroluj limit těla, externí reference a příjemci vracej pouze potřebné diagnostické údaje.

Diakritika a příznaky json_encode

json_encode ve výchozím stavu zapíše každý znak mimo ASCII jako \uXXXX a lomítko jako \/. Výsledek je platný JSON a po dekódování dá stejný řetězec. Diakritika se tedy neztrácí ani nepoškozuje, jen je text delší a hůř čitelný v logu nebo v databázi.

PHP 8.4
$data = ['nazev' => 'Žluťoučký kůň', 'url' => 'https://example.cz/a/b'];

echo json_encode($data, JSON_THROW_ON_ERROR);
// {"nazev":"Žluťoučký kůň","url":"https://example.cz/a/b"}

echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// {"nazev":"Žluťoučký kůň","url":"https://example.cz/a/b"}

var_dump(json_decode('"žluťoučký"') === 'žluťoučký');   // bool(true)

json_encode("Å");                       // false, chybu zjistíš jen přes json_last_error()
json_encode("Å", JSON_THROW_ON_ERROR);  // JsonException: Malformed UTF-8 characters…
  • JSON_UNESCAPED_UNICODE zapíše znaky v UTF-8 přímo. Vstupní řetězce proto musí být platné UTF-8; data z Windows-1250 nejdřív převeď přes mb_convert_encoding.
  • JSON_UNESCAPED_SLASHES nech vypnutý, pokud JSON vkládáš do HTML elementu <script>. Escapované <\/script> značku neukončí. Pro HTML kontext přidej JSON_HEX_TAG | JSON_HEX_AMP.
  • JSON_THROW_ON_ERROR používej vždy. Bez něj vrátí json_encode při neplatném UTF-8 nebo hodnotě NAN jen false a do databáze se může uložit prázdná hodnota.

Sloupce typu jsonb v PostgreSQL a JSON v MySQL ukládají data v rozložené podobě, takže uložená hodnota je s příznakem i bez něj stejná. Rozdíl uvidíš v textových sloupcích, v souborech a v HTTP odpovědích. Viz také escapování řetězců v JSON.

JSON sloupce v Laravelu a Filamentu

Eloquent převádí mezi JSON sloupcem a PHP hodnotou pomocí castů. array a json vrátí pole, AsArrayObject a AsCollection objekt, ve kterém lze měnit jednotlivé klíče. json:unicode (od Laravelu 12.3) zapisuje jako JSON_UNESCAPED_UNICODE; lomítka dál escapuje.

Model — Laravel 13
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Model;

class Produkt extends Model
{
    protected $fillable = ['popis', 'vlastnosti', 'vlastnosti->rozmery->v'];

    protected function casts(): array
    {
        return [
            'popis'      => 'json:unicode',        // pole ↔ JSON bez \uXXXX (Laravel 12.3+)
            'vlastnosti' => AsArrayObject::class,  // jednotlivé klíče lze měnit přímo
        ];
    }
}
Použití; SQL v komentářích vypsal toSql()
$p = Produkt::create([
    'popis'      => ['nazev' => 'Žluťoučký kůň'],
    'vlastnosti' => ['barva' => 'zelená', 'rozmery' => ['v' => 10]],
]);

$p->vlastnosti['barva'] = 'modrá';     // s castem 'array' jen Notice: Indirect modification … has no effect
$p->save();

$p->update(['vlastnosti->rozmery->v' => 12]);   // klíč musí být ve $fillable

Produkt::where('vlastnosti->barva', 'modrá')->count();
// PostgreSQL: select … where "vlastnosti"->>'barva' = ?
Produkt::whereJsonContains('vlastnosti->rozmery', ['v' => 12])->count();
// PostgreSQL: select … where ("vlastnosti"->'rozmery')::jsonb @> ?
Co je skutečně ve sloupci (PostgreSQL 18)
popis       text   {"nazev":"Žluťoučký kůň"}                               json:unicode
popis       text   {"nazev":"\u017dlu\u0165ou\u010dk\u00fd k\u016f\u0148"}   array
vlastnosti  jsonb  {"barva": "modrá", "rozmery": {"v": 12}}                 libovolný cast
  • Zápis přes -> a $fillable. update(['vlastnosti->rozmery->v' => 12]) změní jeden klíč, ale jen když je přesně tento zápis uvedený ve $fillable. Jinak ho Eloquent tiše zahodí a nic neuloží. Model::preventSilentlyDiscardingAttributes() z toho udělá výjimku MassAssignmentException; zapni ji alespoň ve vývoji.
  • Dotazy. where('vlastnosti->barva', …) a whereJsonContains Laravel přeloží na operátory PostgreSQL ->> a @>, v MySQL na json_extract a json_contains. Index nad JSON sloupcem musíš vytvořit v migraci sám.
  • Cast nekontroluje strukturu. Do sloupce se uloží libovolné pole. Pravidla zajistí validace požadavku ('vlastnosti.barva' => 'required|string') nebo JSON Schema.
  • Filament. Pole formuláře KeyValue a Repeater ukládají pole hodnot. Dokumentace Filamentu doporučuje JSON sloupec a cast array na modelu. AsArrayObject nebo json:unicode fungují také, protože vrací a přijímají stejná data.

Ověřeno s illuminate/database 13.34 proti PostgreSQL 18. Zdroje: Laravel: Array and JSON Casting, Mass assignment a JSON sloupce, Filament KeyValue a konstanty JSON v PHP.