PHP triky
Weblog o elegantním programování v PHP pro mírně pokročilé
Zvýrazňovače a editory kódu
Prism
Pokud bych chtěl dnes na webu jen zvýraznit zdrojový kód, tak sáhnu nejspíš po Prism: maličký, elegantní, snadný na použití:
<link rel="stylesheet" href="prism/prism.css">
<script src="prism/prism.js"></script><!-- nakonfigurované na https://prismjs.com/download -->
<code class="language-html"><a onclick="alert('Hi')">Example</a></code>
Prism neobsahuje editor kódu, ten je k dispozici zvlášť a vypadá, že je udělaný přesně v duchu Prism. Má i autocomplete, ale ne pro SQL.
CodeMirror
CodeMirror je primárně editor kódu, ale dá se použít i pro zvýrazňování syntaxe. To se hodí hlavně v situaci, kdy na stránce nějaký editor máme a ukázky kódu chceme zvýraznit stejně.
<link rel="stylesheet" href="codemirror/lib/codemirror.css"> <script src="codemirror/lib/codemirror.js"></script> <script src="codemirror/addon/runmode/runmode.js"></script> <script src="codemirror/mode/xml/xml.js"></script> <code class="cm-s-default"><a onclick="alert('Hi')">Example</a></code> <script> const el = document.querySelector('.cm-s-default'); CodeMirror.runMode(el.textContent, 'text/html', el); </script>
Ace
Editor Ace se zaměřuje hlavně na výkon. Ale způsob, jak pomocí něj jenom zvýraznit kód, jsem hledal snad hodinu:
<script src="ace/ace.js"></script> <script src="ace/ext-static_highlight.js"></script> <code class="example"><a onclick="alert('Hi')">Example</a></code> <script> const highlight = require('ace/ext/static_highlight'); const el = document.querySelector('.example'); highlight(el, {mode: 'ace/mode/sql', theme: 'ace/theme/tomorrow'}); </script>
Ace má problém s inline kódem, protože používá <div>
.
Monaco Editor
Monaco je editor používaný VS Code, ale dá se použít i samostatně pro editaci i zvýrazňování kódu.
<script src="/vs/loader.js"></script> <code data-lang="html"><a onclick="alert('Hi')">Example</a></code> <script> require.config({ paths: { vs: '/vs' } }); require(['vs/editor/editor.main'], function (monaco) { document.querySelectorAll('[data-lang]').forEach(monaco.editor.colorizeElement); }); </script>
JUSH
Přiznám se, že můj JUSH už se mi moc nechce udržovat, proto hledám náhradu. Killer-feature tohoto zvýrazňovače a editoru jsou odkazy do dokumentace, což používám zaprvé tady na blogu: strpos()
. A zadruhé taky v Admineru, kde se jednak odkazuji do dokumentace prováděných SQL příkazů, ale používám to třeba i pro nápovědu u tlačítek pro údržbu tabulek (ANALYZE TABLE
a spol.), u kterých nikdy nevím, co které znamená. Odkazy vedou do dokumentace přesně toho výrobce a verze databáze, které používáte, např. PostgreSQL 16. Podobnou feature jsem našel jen u Prism pro odkazy do neudržované Web Platform Docs. Radši bych je pro webové věci měl do MDN. Pro SQL bych si to musel udělat sám.
<script src="jush/jush.js"></script>
<code class="example"><a onclick="alert('Hi')">Example</a></code>
<script>
jush.style('jush/jush.css');
jush.highlight_tag(document.querySelectorAll('.example'));
</script>
JUSH zaostává v napovídání klíčových slov a tabulek, což ostatní editory podporují.
Použití v Admineru
V Admineru JUSH zatím nechám jako výchozí zvýrazňovač syntaxe. Ale existují pluginy pro CodeMirror, pro Ace, pro Prism i pro Monaco Editor.
JUSH včetně odkazů do dokumentace zabírá ve zkompilovaném Admineru přijatelných 42 kB. CodeMirror bez odkazů by měl asi 130 kB, Ace asi 120 kB. Prism Code Editor je dělaný tak moderně, že jeho zabalení do jednoho souboru by bylo problematické, ale nejspíš by měl jen pár kilo. Monaco Editor nejspíš několik MB. Na Ace a Prism mi vadí, že nerozlišují variantu použitého SQL, takže "text"
zvýrazňují stejně jako 'text'
, i když to první v PostgreSQL znamená identifikátor a v MySQL řetězec. CodeMirror a JUSH to umí rozlišit.
PHPStan: Kontrola sémantiky
Jazyky s explicitní kompilací můžou v tomto kroku dělat všelijaké pokročilé kontroly a kompilaci v případě něčeho podezřelého prostě zastavit. Dělá to třeba Java, která na tento krok dovoluje napojit i další kontroly. Takže existují projekty jako Error Prone od Google, který momentálně dělá 606 dalších kontrol potenciálně chybového kódu.
V PHP máme situaci jinou. Kód se kompiluje až těsně před spuštěním a asi bychom nebyli úplně rádi, když by se stránka nezobrazila třeba jen proto, že jsme napsali return;;
(druhý středník vyvolá chybu nedosažitelný kód). Předem můžeme pomocí php -l
udělat jen kontrolu syntaxe. Nicméně aby měl člověk klid v duši, tak by to chtělo udělat i nějaké další kontroly. Já jsem si před 17 lety napsal jednoduchý skript, který zkontroluje, jestli kód nepoužívá neinicializovanou proměnnou. Spouštěl jsem ho např. vždycky poté, kdy jsem část kódu přesunul do samostatné funkce. Dnes už s vyčleněním kódu do funkce pomůžou IDE, ale když uděláte pouhé copy/paste, tak se může snadno stát, že nějakou proměnnou v kódu použitou zapomenete předat funkci jako parametr.
Od té doby situace značně pokročila a dovednosti statických analyzátorů šly hodně dopředu. Etalonem schopností je PHPStan, který vzniká v Česku. Nasadil jsem ho nedávno na Adminer a i jeho netradičně strukturovaným kódem se PHPStan prokousal se ctí. Nejdůležitější je, že jeho nasazení odhalilo skutečné chyby. V několika případech mě také dokopalo k refaktoringu, díky kterému je daný kód teď lepší.
Co se mi hodně líbilo, jsou úrovně chyb. Začal jsem na úrovni 0, vyřešil všechny její chyby a pokračoval na další level. Nečekal bych, že se dá gamifikovat i tak otravná činnost jako hledání chyb. Ale skutečně jsem se několikrát dostal do situace, že jsem chtěl co nejrychleji opravit chyby jedné úrovně, abych zjistil, co mě čeká na úrovni další. Skončil jsem na úrovni 6, sedmičku jsem přeskočil a zvlášť zapnul ještě osmičku. Některé chyby jsem se rozhodl ignorovat, v kódu mám třeba fread($fp, 1e6)
, což se PHPStanu nelíbí, protože na místo celého čísla posíláme float
. Skutečný problém to ale nezpůsobí. Nic ale nebrání tomu výjimku v budoucnu smazat a kód přepsat tak, aby byl PHPStan spokojený.
PHPStan si neporadí s některými podivnostmi, např. s tímto kódem, který se dříve používal pro nahrávání pluginů:
<?php function f() { class C {} return new C; // PHPStan: Instantiated class C not found. } ?>
Také očekává, že nebudete přistupovat k neexistujícím prvkům pole:
<?php $a = array(1); if ($a[rand()]) { // PHPStan: If condition is always true. } ?>
Kód musíte přepsat, aby volal isset()
.
Nejprve jsem chtěl takovéto okrajové případy nahlásit, ale pak jsem se podíval na 1300 otevřených issues a řekl jsem si, že s takovýmito blbostmi nebudu autora PHPStanu Ondřeje Mirtese otravovat. Ondra se totiž vydal na odvážnou cestu a zkouší se tvorbou open-source živit. Muselo chtít velkou odvahu opustit práci a začít to dělat na plný úvazek. Přeji, ať to vyjde!
Adminer 5.1.0 - autoloading pluginů
Jsem velký zastánce convention over configuration, takže Adminer ani žádnou konfiguraci nemá. Jsem za to strašně rád. S konfigurací se člověk časem dostane do bodu, kdy by do ní potřeboval začít přidávat minimálně nějaké ify a pomalu se z toho stává programovací jazyk. Dále je konfigurace noční můra řešení problémů – uživatel si stěžuje na nějaké chování a teprve po hodině zjistíte, že si něco zapnul v konfiguraci. V horším případě spolu konfigurační direktivy můžou všelijak kolidovat a jejich udržování stojí hodně energie. Jenže uživatelé si chování prostě měnit chtějí, tak jsem jim dal do ruky mocnější nástroj – pluginy. Ty nejsou omezeny tupostí konfiguračního jazyka a můžou dělat vše, co jejich autor zvládne vymyslet. Adminer jen musí poskytnout dostatečné API.
Zapnutí pluginů v Admineru nebylo úplně nejjednodušší: bylo potřeba vytvořit soubor s definicí funkce adminer_object
, ručně vložit soubory s pluginy (nebo je autoloadnout) a vytvořit jejich objekty. Dále vytvořit další objekt, který to všechno pospojoval, a ten z funkce vrátit. Člověk také nesměl zapomenout vložit původní adminer.php
a také soubor plugin.php
, který pluginy vůbec dovoloval používat. Říkal jsem si, že to každý programátor zvládne – v praxi to je copy/paste příkladu a jeho upravení. Ale někdy člověk musí řešit i další věci – třeba pokud plugin dělá dodatečnou autentizaci (např. podle IP adresy nebo podle jiného hesla než do DB), tak také musíme znemožnit přístup k původnímu adminer.php
, kde tyto kontroly neproběhnou.
Tohle všechno je teď pryč. V Admineru 5.1.0 stačí pluginy prostě zkopírovat do adresáře
adminer-plugins/
a Adminer si je tam odtud sám nahraje. Pokud pluginy potřebují nějakou konfiguraci, tak je možné ji udělat v souboru adminer-plugins.php
pouhým vrácením pole s vytvořenými pluginy:
<?php // zde nemusí být žádné include_once "login-password-less.php" return array( new AdminerLoginPasswordLess('$2y$07$Czp9G/aLi3AnaUqpvkF05OHO1LMizrAgMLvnaOdvQovHaRv28XDhG'), ); ?>
Dobrá zpráva je, že předchozí způsob vytváření pluginů i všechny stávající pluginy nadále fungují – není potřeba dělat žádnou úpravu. Další možnost je všechny pluginy vytvořit z libovolného zdroje v adminer-plugins.php
a adresář adminer-plugins/
ani nemusí existovat. Tohle celé funguje i pro pluginy databázových ovladačů.
Já už tento způsob nahrávání pluginů nějakou dobu používám a je to neskutečně pohodlné. Když chci nějaký plugin, tak si na něj v adresáři adminer-plugins/
prostě jen vytvořím symlink. To jde příkazem mklink
i na Windows a ve správci souborů na to mám klávesovou zkratku. A když plugin nechci, tak symlink zase jen smažu.
Další změny
Kromě této zásadní změny jsem udělal i několik dalších oprav a úprav:
- V přehledu serveru se nyní zobrazuje i seznam nahraných pluginů.
- V přehledu tabulky se zobrazuje collation u sloupců, pokud se liší od tabulky. Existuje i plugin, který zobrazuje více informací vždy.
- Ctrl+klik ve výpisu dat vyvolá rychlou editaci dané buňky. Když jsem tuto funkci před 14 lety přidával, tak se kurzor automaticky i přesunul na místo, kam uživatel kliknul. Ale bylo to v turbulentních dobách, kdy tato API teprve vznikala, každý prohlížeč používal jiné a postupně z nich všechny odešly na nové. Na něj jsem Adminer přehodil teď, takže tato vychytávka zase funguje.
- V PostgreSQL se zobrazuje auto_increment vloženého řádku. PostgreSQL nemá funkci obdobnou
mysql_insert_id
, ale musím říct, že jeho způsob je celkem elegantní. Stačí na konec příkazuINSERT INTO
přidatRETURNING "id"
a hodnota tohoto sloupce se vrátí. - V PostgreSQL se u systémových proměnných kromě odkazu do dokumentace zobrazuje i popis.
- Přidání detekce CockroachDB zahltilo logy PostgreSQL chybami o neexistenci proměnné
crdb_version
. Nyní tuto detekci dělám pomocíversion()
(bug #924, regrese z 5.0.5). - Do PostgreSQL jsem přidal podporu
PROCEDURE
, dostupnou od PostgreSQL 11. - MS SQL mi z nějakého důvodu začalo vracet chyby s collations u defaultních sloupců, tak jsem jejich získávání změnil. Ale přiznám se, že collations v MS SQL jsou pro mě trochu španělská vesnice – jen pro češtinu jich má 70 a co znamená třeba
Czech_100_CS_AI_KS_WS
, to mi není úplně jasné. - Do vzhledu jsem dodělal změnu, kterou jsem chtěl už dávno – hlavičky sloupců zůstávají při scrollování vidět. Dřív to ve Firefoxu způsobilo zmizení rámečků v hlavičce tabulek, ale ani s moderními prohlížeči to nebylo úplně jednoduché. Nad řádkem s hlavičkou zůstávala 1px díra, kterou byl vidět obsah pod ní. Takže jsem musel konstrukci rámečků trochu změnit (bug #918).
- Kromě souboru
adminer.css
lze vzhledy konfigurovat třeba i podle názvu serveru, což využívají pluginy. Pro ně jsem upravil detekci přehazování na tmavý režim (bug #925). - Zvětšil jsem maximální šířku políčka pro editaci řetězců ze 40 na 60 (bug #930), mezeru za výsledkem SQL dotazu (bug #937) a mezeru nad odhlášením na mobilu (bug #938).
- Přidal jsem plugin pro sestavování SQL dotazů umělou inteligencí Google Gemini, který funguje naprosto neuvěřitelně. Více v samostatném článku.
- Další plugin kontroluje dostupnost nových verzí na GitHubu místo na adminer.org/version. Někdo může být háklivý na leaknutí IP adresy, kde instalace Admineru běží. Ale třeba se takoví uživatelé smíří s tím, že leakne pouze na GitHub.
Přidal jsem plugin pro ruční přepínání tmavého a světlého vzhledu.
- Přidal jsem plugin zobrazující zpětné odkazy stejně, jako to dělá Adminer Editor.
- Po editaci hodnot ve formulářích se prohlížeč nyní zeptá, jestli chcete stránku skutečně opustit. Nejdřív jsem udělal pěknou implementaci, která i vracela změněná formulářová políčka, a pak jsem si teprve všiml, že text vrácený obsluhou události
beforeunload
všechny moderní prohlížeče ignorují (asi kvůli phishingu). Nejprve jsem dal tuto funkci přímo do Admineru, ale pak mě to samotného občas trochu štvalo, tak jsem to přesunul do pluginu. - Přibyl uzbecký překlad. Uzbečtina používá docela dost apostrofy, ale Adminer překlady neescapuje, protože v některých situacích oprávněně používají HTML. Tak jsem udělal takovou fintu, že
'
měním na’
. Lepší by bylo překlady plaintextu od překladů HTML rozdělit, ale to se mi dělat nechtělo.
Ovladač pro IMAP
Jen tak pro legraci jsem vytvořil Adminer pro IMAP. Koukám do poštovního klienta a po intenzivní práci na Admineru najednou všude vidím tabulky s daty. Tak jsem si řekl, jak by asi bylo složité to udělat, a ukázalo se, že zase až tolik ne. Schránky se zprávami se zobrazují jako tabulky, hlavičky zpráv se zobrazují jako jejich obsah. Pokus o editaci zprávy o ní zobrazí nějaké další údaje. Vložení záznamu nic nedělá, ale s trochou úsilí by to asi šlo předělat na odeslání nové zprávy. Mazání označí zprávy jako smazané, ale ze schránky je fyzicky neodstraní, to se dá udělat příkazem TRUNCATE
. A to je asi tak všechno, co to umí. Funkce support()
, kterou Adminer zjišťuje, co všechno daná databáze podporuje, vrací pro všech 25 featur prostě false
.
Testy pro PDO
Přidal jsem parametr URL ext=pdo
, který vynutí použití extenze PDO místo nativních, které Adminer preferuje. Je to hlavně kvůli snadnějšímu ladění chyb, protože PDO ovladač se chová trochu jinak (např. pro boolean
sloupce v některých databázích vrací skutečný PHP bool
). Pro PDO se také nově generuje kompletní sada end-to-end testů. Díky tomu se mi podařilo ošetřit pár chyb a nekonzistencí, které Adminer při použití PDO měl.
Interní změny
Změna, která se navenek doufám nijak neprojeví (end-to-end testy nic neodhalily), je modernizace JavaScriptu:
- Změna všech
var
nalet
/const
. Využil jsem k tomueslint --fix
s pravidlyno-var
aprefer-const
.let
má logičtější chování nežvar
. - Změna anonymních funkcí, které nepoužívají
this
, na() => {}
. Adminerthis
docela hodně používá pro události, takže se to zase tolika funkcí nedotklo. - Použití
for...of
. To kód zpřehledňuje a zkracuje. - Odstranil jsem několik obstarožních kontrol, jako třeba jestli prohlížeč podporuje
JSON
. - Místo
className
se teď většinou používáclassList
.
V PHP jsem zapnul chyby E_NOTICE
a E_STRICT
kromě přístupu k neexistujícímu prvku pole. To se hádám navenek v některých okrajových případech projevit může, ale co odhalily testy a na co jsem sám narazil, to jsem opravil.
Na závěr zmíním, že jsem sepsal poznámky pro vývojáře, v podstatě takový můj braindump.
Google Gemini: API
Odborník na umělou inteligenci, vynikající PHP programátor a úžadný člověk David Grudl před časem zveřejnil sneak peek pluginu do Admineru, který dovoluje sestavovat SQL dotazy pomocí AI. Navázal tak na svůj SQL Wizard. Plugin ale nikdy nepublikoval, tak si možná část lidí myslela, že to je fake. Řekl jsem si, jak by asi bylo složité to doopravdy udělat, a ukázalo se, že úplně triviální! Použil jsem Google Gemini a z jednoduchosti jeho nasazení jsem byl velmi mile překvapen. Nemusíte chodit do žádné Google Cloud Console (která je neuvěřitelně nepřehledná), ale prostě přímo v AI Studiu vygenerujete klíč a hned vidíte URL, na kterém ho můžete použít. Implementace v PHP je pak už jednoduchá:
<?php /** Položení dotazu Google Gemini * @param string Dotaz v přirozeném jazyce * @param string Klíč získáte na https://aistudio.google.com/apikey * @param string Dostupné modely: https://ai.google.dev/gemini-api/docs/models#available-models * @return string Odpověď umělé inteligence * @copyright Jakub Vrána, https://php.vrana.cz/ */ function gemini($prompt, $apiKey, $model = "gemini-2.0-flash") { $context = stream_context_create(array("http" => array( "method" => "POST", "header" => array("User-Agent: PHP", "Content-Type: application/json"), "content" => '{"contents": [{"parts":[{"text": ' . json_encode($prompt) . '}]}]}', ))); $response = json_decode(file_get_contents("https://generativelanguage.googleapis.com/v1beta/models/$model:generateContent?key=$apiKey", false, $context)); return $response->candidates[0]->content->parts[0]->text; } ?>
V pluginu pak dotazu od uživatele předřadím schéma databáze a přidám nějakou omáčku, aby to vracelo skutečně jen SQL dotaz, který pak prostě zobrazím. Funguje to naprosto neskutečně:
Poradí si to dokonce i s češtinou, umí to vytvářet i INSERT
, přidávat indexy a já nevím, co ještě:
Plugin vyžaduje Adminer 5.1.0, kam jsem přidal hook pro zobrazení promptu na tom správném místě, jinak si to žádnou změnu nevyžádalo.
GitHub API: Získání času modifikace Gistu
Dobrovolník ke všem pluginům v jejich seznamu přidal popis a datum poslední aktualizace – díky! Já jsem všechny autory pluginů obeslal pull-requesty s úpravou na Adminer 5. A tím se většina datumů poslední aktualizace stala neplatná.
Aktualizovat to celé ručně by byl nesmysl, tak jsem využil toho, že většina pluginů je k dispozici na GitHubu, některé na Gistu. Tím pádem se dá využít GitHub API a datum aktualizace získat přes něj. Např. pro Gist se to dá udělat takhle:
<?php // zdrojem je normální URL s Gistem if (preg_match('~^https://gist.github.com.*/(.+)$~', $url, $match)) { $gist = github_api("gists/$match[1]"); echo $gist->history[0]->committed_at; // $gist->updated_at includes comments } function github_api($path) { $context = stream_context_create(array('http' => array( 'header' => array("User-Agent: PHP"), ))); return json_decode(file_get_contents("https://api.github.com/$path", false, $context)); } ?>
Při kladení požadavků musíme nastavit hlavičku User-Agent
(na libovolnou hodnotu), což je takový chyták. Pro lepší zpracování chyb je vhodné přidat do kontextu ještě ignore_errors
, díky čemuž získáme i stránky s chybami místo pouhého false
. Hlavičky zprávy potom získáme v proměnné $http_response_header
. Z nich se dozvíme třeba to, že u veřejných API (jako je tohle) má GitHub docela přísné limity – nějakých 60 požadavků za hodinu. Pokud chceme víc, dá se to nejjednodušeji udělat posláním tokenu, který si vygenerujeme v nastavení. Pak jen do pole s hlavičkami přidáme Authorization: Bearer ghp_...
(nezapomenout na Bearer
).
Starší články naleznete v archivu.

