PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

phpMyAdmin 6.0.0-dev

Po letech jsem se podíval, jak pokročil phpMyAdmin a je to snad ještě větší utrpení, než původně:

  1. Přivítá mě hláškou o chybějícím rozšíření Sodium. Když na odkaz Sodium kliknu, dostanu úplně tu stejnou chybovou hlášku, jen na jiném URL.
  2. Do SQL příkazu vložím export databáze včetně SET foreign_key_checks = 0 a skončí chybou. Musím ještě odškrtnout jejich Enable foreign key checks.
  3. Když se import konečně provede, tabulky v menu nejsou aktualizované.
  4. Kliknu na obnovit menu a celé menu poskočí tam a zpátky. To se mimochodem děje při načítání jakékoliv stránky. Vyrenderuje se o nějakých 50 pixelů vlevo a pak poskočí na správné místo.
  5. Chci vytvořit tabulku – použiji odkaz v menu, který vygeneruje 4 sloupce. Pro přidání dalšího musím kliknout na tlačítko, které skončí chybou Error code: 200. Error text: OK (rejected). Tabulku s více sloupci tedy můžu vytvořit jedině tak, že počet sloupců zadám předem v jiném formuláři.
  6. Double-clickem chci označit slovo ve výpisu, místo toho se začne editovat. Říkám si: „OK, rozbili mnou velmi často používané gesto pro označování po celých slovech, ale aspoň mají inline editaci.“ Změním hodnotu a kliknu vedle, abych změnil další. První změněná se bez jakéhokoliv mého potvrzení uloží a její editace zmizí. V Admineru můžu atomicky změnit víc hodnot najednou, klidně i v různých řádcích.
  7. Ve výpisu zaškrtnu řádek a dám ho editovat. V prohlížeči se chci vrátit o stránku zpátky a nehodí mě to zpátky na výpis, ale na stránku, kde jsem byl předtím.

Kdykoliv to po letech spustím, tak během pěti minut narazím na dlouhý seznam závažných chyb. A to se ani nesnažím chodit do nějakých hlubokých zákoutí, dělám naprosto běžné věci.

První načtení phpMyAdminu stáhne 40× víc dat než první načtení Admineru a každá další stránka taky 40× víc. Rychlost s tím jde ruku v ruce.

Další věc jsou 17 let staré bezpečnostní praktiky:

  • Skrytí celého dokumentu a jeho zobrazení až JavaScriptem, pokud nejsem uvnitř rámu. Hlavičku X-Frame-Options prohlížeče podporují od roku 2010.
  • Není důvod vnější odkazy přesměrovávat přes vlastní stránku, která referrer odstraní JavaScriptem. To je příčina té první chyby, na kterou jsem narazil. Atribut <a rel="noreferrer"> prohlížeče podporují od roku 2014.
  • Tajemství k 2FA je uložené přímo v databázi, ke které by mělo chránit přístup.

„Výhoda“ phpMyAdminu je naprosto přeplácané uživatelské rozhraní. Např. phpMyAdmin má u každého řádku odkaz Smazat, na který můžu kliknout omylem a dialog bezmyšlenkovitě potvrdit. V Admineru musím řádek vybrat a tlačítkem jinde ho smazat. To je lepší nejen UI, ale i UX. Uživatelé mají pocit, že tam je strašně moc funkcí, přitom i celkem základní chybí (např. hromadné nastavení stejné hodnoty u více řádek).

Další „výhoda“ je nabubřelá instalace – 77 MB v 4891 souborech prostě nikdo ručně kopírovat nebude. Hostingy proto dávají phpMyAdmin předinstalovaný.

Jakub Vrána, Adminer, 8.8.2026

Adminer 6.0.0

Začnu prosbou. Adminer má na GitHubu už jen o 400 ⭐ míň než phpMyAdmin, kvůli jehož těžkopádnosti jsem začal na mrštné alternativně pracovat. Pokud vy nebo vaši známí Adminer používáte, dejte mu prosím hvězdičku.

Při vývoji Admineru jsem se dostal do totálního flow. Při řešení jedné věci mě napadly dvě další a todo list byl večer často dvakrát delší než ráno. Adminer 6.0.0 obsahuje více než 130 změn a nebudu je tady všechny vypisovat. Vyberu jen to nejzajímavější:

Bezpečnost

Adminer už od verze 4.6.3 zakazuje přihlášení bez hesla. Hlavně uživatelům SQLite to komplikuje život. Pro ně jsem vytvořil stránku, kde si můžou stáhnout Adminer pro SQLite chráněný heslem dle jejich výběru.

Nově jsem zakázal i přihlášení k serverům, které se spokojí s jakýmkoliv heslem. Plugin login-password-less na tyto změny reaguje a nově v něm lze použít stejné heslo pro SQLite jako máte třeba pro MySQL.

„Operátor“ SQL v Admineru používám třeba pro práci s JSON. Je ale samozřejmě nebezpečný, do SQL příkazu lze pomocí něj doplnit jakýkoliv kód. SameSite cookies používané Adminerem riziko snižují, ale doplnil jsem dvě další ochrany:

  • Jeho použití vyžaduje poslání Sec-Fetch-Site: same-site.
  • V PostgreSQL se dotaz spustí jako READ ONLY a nelze spustit víc dotazů.

Vzhled

screenshot Chtěl jsem vypíchnout data v databázi a potlačit uživatelské rozhraní Admineru. Třeba skrytím opakujících se odkazů a jejich zobrazením až při najetí myši. Vzhled byl mnohem vzdušnější, ale zároveň vypadal děravý a na první pohled nebylo jasné, kde jakou funkci najdu. Částečná průhlednost rozhraní Admineru zase působila dojmem, že jsou odkazy disabled, tento experiment je k dispozici v pluginu menu-links (volba hover). Nakonec jsem jen checkboxy a odkazy zobrazované v řádcích tabulky vystrčil mimo tabulku, s čímž jsem spokojen. Původní chování lze vrátit pomocí vlastního vzhledu. Přemýšlel jsem, jak se zbavit opakujících se odkazů vypsat v menu. Některé vzhledy je skrývají, jiné je vymění za ikonu. Ale já zhruba stejně často používám vypsání a strukturu tabulky a jakékoliv skrytí mě zmate. U ikony musím vždycky přemýšlet, jestli znamená to nebo ono, navíc vůbec není jasné, že by měla dělat něco jiného než odkaz z názvu tabulky. U explicitního vypsat přemýšlet nikdy nemusím.

Počet ošetřovaných znaků v URL jsem snížil na minimum. Z dřívějších ošklivých URL where%5B0%5D%5Bcol%5D je pěkně čitelné where[0][col]. Týká se to i formulářů, které nepředávají nevyplněná políčka, takže jsou URL mnohem kratší. Z této pro běžného uživatele neviditelné změny mám velkou radost.

Editační funkce bez parametru (např. NOW()) nově skryjí vstupní políčko. Zvažoval jsem to udělat i při výběru hodnoty NULL, ale tam mi vyhovuje, že stačí do vstupního políčka začít psát a NULL samo zmizí.

Blbuvzdornost

Adminer měl řadu kontrol konfigurace PHP, např. proti překročení max_input_vars nebo upload_max_filesize. Ty se ale provedly, až když bylo pozdě – po odeslání formuláře. Nyní se provádí už při jeho vyplňování.

Nové verze Admineru se pomocí <noscript><iframe sandbox> nově kontrolují i s vypnutým JavaScriptem. Kontrolu verzí jsem doplnil také k pluginům, pluginům ovladačů a vzhledům, které by měly být vždy ve stejné verzi jako Adminer. Nebyly k tomu potřeba žádné nové requesty – Adminer si s sebou prostě nese verze pluginů a vzhledů, které k němu pasují. Pokud jste si nějaký plugin vědomě změnili a kontrola vás otravuje, stačí přejmenovat jeho soubor. U vzhledů pak změnit první řádek v adminer.css.

Import/export

Export obecného SQL příkazu byl v některých případech problém. Mohl obsahovat proměnné nebo stavové informace, které při exportu chyběly. Vyřešil jsem to tak, že pokud to jde, použijí se pro export přímo data z vypsané tabulky, což je i rychlejší.

Import a export si dřív každý ovladač dělal po svém a obzvlášť v pluginech ovladačů to nefungovalo moc dobře. Teď jsem to sjednotil a ovladače si jen registrují své odlišnosti – např. pokud podporují UPSERT.

V PostgreSQL jsem problém s cizími klíči vyřešil tak, že se všechny DROP TABLE a TRUNCATE provedou na začátku.

Pluginy

  • Od Davida Grudla jsem převzal plugin pro zobrazení varovného pruhu na produkčních serverech. (remote-color)
  • Vytvořil jsem plugin vyžadující přihlášení pomocí passkey. (login-passkey)
  • Další plugin dovoluje importovat CSV soubory. To už Adminer dávno umí do strutury existujících tabulek, plugin dokáže strukturu tabulek odvodit z dat. (import-csv)
  • Adminer Editor defaultně zobrazuje obrázky, teď jsem tuto možnost dal i Admineru. (select-image)
  • Osm nepoužívaných pluginů jsem smazal.

Interní změny

Z některých interních změn mám možná ještě větší radost než z toho, co je vidět navenek – hlavně z obsluhy JavaScriptových událostí.

Dávno<a onclick="return aClick(this, 1, event);">
Od 4.4.0<a><script nonce="OGY1MDBkYjNhNGRmYjAwNWU5OGJjNjMwM2Q1Nzg1MWQ=">qsl('a').onclick = partial(aClick, 1);</script>
Nově<a data-onclick='aClick(1)'>

Nový zápis je plně kompatibilní s CSP a neumožňuje přímé spuštění kódu. Je to jen přehledněji zapsané JSON pole. Zjednodušilo to i obsluhu událostí v HTML kódu poslaného přes AJAX, kterou druhý způsob přímo neumožňoval.

Pro závislosti Admineru používám Git submoduly, které jsou pro mě mnohem pohodlnější než Composer. Ale pro lidi, co instinktivně píšou git clone; composer i jsem doplnil i podporu pro composer i. Nově to nainstaluje i ESLint, který se při composer check nyní spouští spolu s PHP_CS a PHPStan.

graf Nechal jsem zjistit velikosti všech verzí Admineru. I přes velký nárůst funkčnosti nové verze se mi téměř podařilo zachovat velikost zkompilovaného souboru díky jednoduchému triku. Pro komprimaci překladů se používá komprimační tabulka angličtiny (funkce deflate_init).

  • V kořenovém adresáři už bylo moc souborů, proto jsem část z nich přesunul do podadresářů.
  • Maximální délku řádek jsem zkrátil z 250 na už celkem přijatelných 200 znaků.
  • Hlavní větev v Gitu jsem přejmenoval z master na main.
  • Sjednotil jsem úroveň požadovaného JavaScriptu. Některé obraty používaly zbytečně nové konstrukce, jiné zase zbytečně staré. Teď by měl fungovat jakýkoliv prohlížeč s podporou ES6, tedy kterýkoliv od roku 2018.
  • End-to-end testy jsem přehodil z neudržovaného Katalonu na headless Playwright.

Web

Vyměnil jsem tabulky v demu. Demo nově funguje i s PostgreSQL.

Závěr

Většina změn vznikla s asistencí Claude Opus 5. Někteří lidi se bojí, že AI asistence může kód zamořit technickým dluhem. To je jistě pravda, pokud vývojář všechny změny bezduše odbouchne Enterem. Ale pokud si pořádně projde plán, vyjedná v něm změny a pak totéž udělá i s vygenerovaným kódem, kvalita kódu stoupne a technický dluh naopak klesne. Je to jako párové programování s pozorným kolegou – něco napadne mě, něco jeho a společně vzniklý kód je lepší, než kdyby ho kdokoliv z nás psal samostatně. Sám dělám při asistovaném vývoji odhadem tak dvakrát víc důležitých rozhodnutí, než když jsem na to sám.

Jakub Vrána, Adminer, 7.8.2026

Adminer 5.5.1

Claude Fable jsem pustil i na závislosti Admineru. Opravil řadu chyb v PhpShrink a JsShrink. Admineru se netýkaly, ale i tak jsem za jejich opravu vděčný. Admineru se týká aktualizace zvýrazňování syntaxe v JUSH. Nechal jsem napsat i aktualizátory jazyků z oficiálních dokumentací.

Změny přímo v Admineru:

  • Hlavička X-Forwarded-Prefix se použila jen pro prefix /[^/]. Tomu bohužel vyhovuje i /\evil.com, což prohlížeče pořád vyhodnotí jako https://evil.com. (GHSA-fr74-9mf9-gf44)
  • V přihlašovacím formuláři lze opět používat znaky :/_, které jsem v minulé verzi zakázal. (bug #1305)
  • Kvůli minimalizaci velikosti byl zkompilovaný adminer.php binární soubor, s čímž uživatelé měli problém při všelijakém ručním kopírování (bug #1113, #1213). Jednoduchým řešením by bylo binární řetězce prohnat base64_encode, jenže to je zvětší o 33 %. Hospodárnější řešení je každých 13 bitů binárního výstupu zakódovat jako dva znaky z rozsahu [!-~], což přidá jen 23 %. Tady jsem si ověřil, jak moc modely pokročily – stejnou úlohu jsem zadal AI asi před půl rokem, dost jsem se s tím natrápil a výsledku jsem se stejně nedobral. Teď to bylo na první dobrou.
  • Toto navýšení jsem vyvážil změnou kompresního algoritmu z LZW na účinnější gzdeflate. Pro dekompresi se používá gzinflate. Jen pokud není k dispozici, tak se použije vlastní implementace – to by se mi ručně fakt psát nechtělo.
  • Claude Fable jsem použil i na doplnění chybějících překladů, ale jen v případech, kdy si byla překladem jistá. Model k tomu využil i obraty ze stávajících překladů. Přeskočil jazyky, kterými si nebyl jist. Našel u toho i několik chyb v původních překladech. Nové překlady jsou označené komentářem // Claud Fable 5, který můžou rodilí mluvčí po kontrole smazat. Doplnění překladů paradoxně snížilo velikost zkompilovaného souboru – každý jazyk se komprimuje zvlášť a místo chybějících překladů se používá angličtina. Míchání jazyků komprimaci zhoršuje.
  • Adminer v PHP 8 opět funguje i se zakázanou funkcí ini_set. Starší verze PHP dokázaly funkce zakázané pomocí disable_functions pořád zavolat, jen vyvolaly chybu, kterou Adminer potlačil. Od PHP 8 už takto zakázaná funkce nejde zavolat vůbec, proto jsem pro ni do Admineru musel přidat wrapper. Přidal jsem i kontrolu dostupnosti funkce set_time_limit. (bug #1288)
  • V PostgreSQL se jde opět přihlásit i s prázdným serverem. (bug #1304, regrese z 5.5.0)

Také jsem aktualizoval PHPCS a PHPStan.

Jakub Vrána, Adminer, 21.7.2026

Adminer 5.5.0

Z nástupu AI jsem byl trochu rozčarovaný. Model udělal třeba 80 % požadované kvality práce, ale donutit ho dodělat ten zbytek nebo to dodělávat sám nakonec často zabralo víc času, než když bych si to udělal sám celé od začátku. Často mi to připadalo jako hádat se s blbcem. Když přišel model Fable 5 a David Grudl kvůli němu zrušil návštěvu festivalů, tak jsem se rozhodl tomu dát šanci znovu.

A jsem nadšen! Model kódu rozumí kolikrát líp než já. Zeptal jsem se ho třeba „Jak to, že se v Admineru pro SQLite zobrazuje struktura tabulky sqlite_schema?“ A on mi to správně vysvětlil – sám bych to musel v kódu hledat a vymýšlet to. Spoustu práce mi ušetřil hlavně dotaz „Podívej se na poslední verze MySQL/PostgreSQL/SQLite a navrhni novinky, které by šly do Admineru zahrnout.“ Pak už jen stačilo zaškrtnout, které z těch novinek chci, a bylo hotovo. Tohle procházení novinek se mi nikdy dělat nechce, protože většina z nich se Admineru nijak netýká. Při práci se model jen tak mimochodem zmínil, že v process listu došlo v PostgreSQL 9.2 k přejmenování jednoho sloupce, a opravil to taky.

Nebo jsem chtěl v SQLite podporovat tabulky STRICT bez toho, aniž bych tím zaplevelil uživatelské rozhraní. Navrhl použít mechanizmus engine, který se v MySQL používá pro výběr typu tabulky. Jde skutečně o elegantní řešení, ke kterému je UI už navíc hotové. Časem bych k němu nejspíš dospěl i sám.

Pomohl mi i se zvýrazňovačem syntaxe. Třeba při přidávání podpory pro VECTOR do Admineru nejprve do submodulu jush commitnul zvýraznění syntaxe a pak do Admineru přidal samotnou podporu. Ještě mi připomněl, ať submodul pushnu jako první.

S tímhle už mě baví pracovat, je to rovnocenný parťák. Část novinek v Admineru je tedy ve spolupráci s Fable.

  • Zakázal jsem neplatné porty a sockety. (GHSA-58cq-mgw2-38m5)
  • Ignoruje se neplatný X-Forwarded-Prefix. (GHSA-8478-xrj3-h9c2)
  • Do process listu jsem přidal odkaz pro zkopírování do schránky. Dávno existuje možnost ho i naklonovat, ale pokud je moc dlouhý, tak se nevejde do URL.
  • Odkazy pro zkopírování dotazů do schránky nefungují v nebezpečných kotextech (např. na HTTP), takže se nově ani nezobrazují.
  • V editaci se zalamují zobrazené hodnoty. To se hodí hlavně u needitovatelných položek na více řádků.
  • Zrychlil jsem zvýrazňování syntaxe, hlavně při editaci dlouhých textů a při velkém množství zvýrazňovaných položek (typicky JSON v datech tabulky).
  • Dá se připojit k socketu bez serveru pomocí :/tmp/mysql.sock. (bug #1199, regrese z 5.4.0)
  • MySQL: V parametrech funkcí se neexportuje příznak IN.
  • MySQL: Sloupce typu vector se ve výpisu a editaci zobrazují jako text.
  • MySQL: Odstranil jsem podporu MySQL 5.0 (z roku 2005) a starší.
  • MySQL, SQLite: Ve změně tabulky jde sloupce přesouvat pomocí drag and drop.
  • MariaDB: Podpora typů inet4 a inet6.
  • MariaDB: Podpora password validation pluginů při správě uživatele – Adminer volal odděleně PASSWORD(), aby se nezahešované heslo neukládalo do historie. To se těmto pluginům nelíbilo. (bug SF-814)
  • MariaDB 11.7+: Podpora typu a indexů vector.
  • PostgreSQL 9.2+: Zvýraznění dotazu v process listu.
  • SQLite: Podpora typu ANY.
  • SQLite: Podpora možností STRICT a WITHOUT ROWID.
  • SQLite: Samotné integer PRIMARY KEY už se nebere jako AUTOINCREMENT.
  • SQLite: Zakázání editace sqlite_schema.
  • MS SQL: Oprava dotazů s více výsledky. (bug #1180)

Přidal jsem taky jednoduchý ovladač pro Redis a plugin pro přihlášení za reverzní proxy. Samotný Adminer totiž nevěří hlavičce X-Forwarded-For, takže neplatné pokusy o přihlášení za reverzní proxy znemožní přihlášení z jakékoliv IP adresy.

Jakub Vrána, Dobře míněné rady, 17.7.2026

Zastaralé $http_response_header

PHP 8.5 zastaralo proměnnou $http_response_header, která obsahuje hlavičky posledního HTTP požadavku. Místo ní je k dispozici funkce http_get_last_response_headers(). Co když potřebujeme zpětně kompatibilní kód? Tímhle si nepomůžeme, protože zastaralost $http_response_header se zjišťuje už při kompilaci:

<?php
$headers = (function_exists('http_get_last_response_headers') ? http_get_last_response_headers() : $http_response_header);
?>

Tohle varování o zastaralosti vyřeší, ale kód ve starších verzích nefunguje, proměnná se nenaplní:

<?php
$var = 'http_response_header';
$headers = (function_exists('http_get_last_response_headers') ? http_get_last_response_headers() : $$var);
?>

Jedině tohle je řešení, které ve starších verzích funguje a v nové nezpůsobí varování:

<?php
if (function_exists('http_get_last_response_headers')) {
    $http_response_header = http_get_last_response_headers();
}
?>
Jakub Vrána, Řešení problému, 12.7.2026

Starší články naleznete v archivu.

avatar © 2005-2026 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.