PHP triky
Weblog o elegantním programování v PHP pro mírně pokročilé
Adminer 5.2.0
Uživatelsky nejviditelnější změna v Admineru 5.2.0 je našeptávač pro SQL. Dále jsem udělal několik drobnějších změn:
- Při hromadné editaci záznamů přes odkaz Změnit se nevytvoří políčka pro hodnotu
NULL
. Tento jednoduchý formulář totiž nerozlišuje prázdný řetězec aNULL
, takže pouhé uložení nezměněného formuláře přepsaloNULL
na prázdný řetězec. Políčka s hodnotouNULL
se v tomto formuláři dají změnit pomocí Ctrl+klik, což používám primárně já (bug #967). - Předchozí verze rozbila akce cizích klíčů.
- V MySQL se v
GROUP BY
dotazech zobrazuje počet nalezených řádek, což rozbila předchozí verze. - SQL příkaz pro PostgreSQL podporuje příkaz
COPY FROM stdin
, který generujepgdump
(bug #942). - V ne-MySQL se
--
bere jako začátek komentáře, v MySQL za tím musí být ještě mezera (bug SF-842). - V exportu MS SQL je počet řádků v jednom
INSERT
omezen na 1000 (bug #983). - Udělal jsem maličký facelift – přidal logo, zaoblil nějaké rohy, přidal nějaké stíny. Alternativní designy jsem upravil, aby fungovaly na mobilu.
- Možnost rozesílat hromadné e-maily z Adminer Editoru jsem přesunul do pluginu. Jednak to asi není moc používané a jednak to jde teď naopak použít i v Admineru.
- Do pluginů jsem přidal možnost překladů. Ty nově můžou dědit ze třídy
Adminer\Plugin
, která jim dá metodulang()
. Pokud pluginy používají nějaké fráze z Admineru, tak můžou nadále volatAdminer\lang()
. - Pro Adminer Editor jsem přidal plugin pro jednodušší nastavení ovladače, serveru a databáze.
Konfigurace
Před představením dalších dvou nových pluginů udělám trochu odbočku. Čas od času po mně někdo chce, abych třeba změnil odkazy na tabulky z menu. Že používá jen odkaz pro výpis dat, druhý odkaz je zbytečný a způsobuje menší přehlednost menu. Někdo naopak usoudil, že je zbytečný odkaz pro výpis dat a nechal jen odkaz na strukturu tabulky. Další lidi po mně chtějí, ať pro to přidám možnost konfigurace. Nebo ať o podobných věcech nechám hlasovat uživatele. Většinou to rychle smetu ze stolu pomocí „použij plugin nebo vlastní vzhled“, ale zamyslel jsem se nad tím trochu víc.
Chování Admineru ovlivňují tři různé strany:
- Vývojář: Rozhoduje o výchozím chování a poskytuje hooky pro pluginy
- Admin: Instaluje Adminer, vybírá pluginy a výchozí vzhled
- Uživatel: Používá Adminer
Díky tomu, že je Adminer k dispozici v jednom souboru a že jeho cílová skupina jsou především programátoři, tak Admin je často ta stejná osoba jako Uživatel. Ale zdaleka to neplatí vždy. Pokud si ve WordPressu nainstaluji plugin pro Adminer, tak jsem jeho Uživatel, ale ne jeho Admin. Nebo pokud mi hosting dá Adminer předinstalovaný.
Adminer na Uživatele myslí jen trochu. Třeba kromě vzhledu v souboru adminer.css
(o tom rozhoduje Admin) existuje i plugin designs
, který vzhledy dovoluje přepínat uživateli.
Nově přidávám plugin config
, který dovoluje chování Admineru změnit Uživateli. Nastavení ukládá do cookie. Pokud chci stejné nastavení použít na více zařízeních, tak se dá URL s konfigurací uložit do záložky a tu pak otevřít jinde. Samotný plugin config
momentálně dovoluje vypnout načítání vzhledu adminer.css
a použít vestavěný vzhled Admineru. Dají se na něj ale napojit další pluginy.
Druhý plugin s nastavením pro uživatele je menu-links
, který dovoluje změnit ono chování odkazů na tabulky v menu. Dá se použít jako normální plugin, kde může Admin přepnout chování natvrdo všem uživatelům. Ale v kombinaci s config
si Uživatel toto chování může změnit sám pro sebe.
Možností konfigurace uživateli možná časem přidám víc. Zvažoval jsem i dát uživatelům možnost vypnout některé pluginy, ale třeba možnost vypnout bezpečnostní pluginy by mít neměli. Časem možná plugin config
integruji i přímo do Admineru, stejně jako se to stalo s pluginem plugin
, který dovoloval pluginy používat.
Adminer 5.2.1 opravuje především chyby odhalené díky typové kontrole.
Autocomplete pro SQL
Našeptávače ve webových editorech kódu jsou převážně tupé. Mají seznam klíčových slov, která jednoduše doplňují podle toho, co začnete psát. V SQL jim můžete dát ještě seznam tabulek a jejich sloupců a doplňují pak i ty. Ale když napíšete třeba SELECT * FROM tabulka WHERE |
tak netuší, že mají doplňovat sloupce jen z tabulky tabulka
. Třeba v CodeMirror musíte napsat WHERE tabulka.|
, aby se zobrazily sloupce jen z dané tabulky.
Rozhodl jsem se to vzít za jiný konec. Místo, aby se za všech okolností napovídalo všechno, tak se napovídá jen to, co v daném kontextu dává smysl. Takže třeba:
- Za
SELECT
se napovídají sloupce ze všech tabulek. Pokud jich je moc, tak se napovídátabulka.
pro všechny tabulky. Když to vyberete, ukážou se sloupce jen z dané tabulky. - Pokud ale někde vpravo už máte
FROM tab1 JOIN tab2
, tak se napovídají jen sloupce z tabulektab1
atab2
. - U dotazu
SELECT * FROM tab ORDER BY id
se nenapovídáWHERE
, protože to patří předORDER BY
. Napovídá se aleLIMIT
. - U
CREATE TABLE
se nenapovídá nic, název tabulky bude určitě jiný. - Když necháte doplnit
INSERT
, tak se rovnou doplníINSERT INTO
, protože to je nejčastější varianta. Můžete se ale vrátit a nechat si dopnitIGNORE
. - Když máte někde v dotazu identifikátor uzavřený do
`idf`
(v MySQL), tak se tabulky a sloupce defaultně nabízí taky takto uzavřené.
phpMyAdmin má plnohodnotný parser SQL, ale pro autocomplete ho nepoužívá. Navíc parser běží na serveru, takže se s každým napsaným písmenkem musí odesílat požadavek a čekat na odpověď. Já jsem si napsal pár regulárních výrazu v JavaScriptu, které nakonec udělají mnohem víc muziky. Když jsem to dokončil, tak jsem nemohl uvěřit vlastním očím: Je to v podstatě SQL wizard, ve kterém celý dotaz sestavíte skoro jen pomocí šipky dolů a Enteru. Ale když chcete něco speciálního, tak se snaží nepřekážet.
Živá ukázka
Jistě sami přijdete na to, jaké tabulky a sloupce jsou definované. Začněte psát SQL dotaz:
Adminer 5.1.1 - refaktoring
Adminer prodělal asi největší refaktoring od přidání podpory pro ne-MySQL databáze.
PHP
Začalo to celkem nevinným komentářem, který mě vybídl k lepšímu popisu tvarů polí v dokumentačních komentářích. To bylo většinou jakž-takž popsané v textu komentáře, ale ne zcela přesně. Např. informace o sloupci v databázi mají trochu jiný tvar než informace o parametru uložené procedury, některé funkce ale dokáží pracovat s oběma formáty. K popisu jsem použil syntaxi PHPStanu a pro časté typy jsem vytvořil aliasy. Vyzkoušel jsem i náhradu polí za value objekty, ale nepracuje se s tím tak pohodlně, tak jsem zůstal u polí.
Původní kód:/** @return array ["Timing" => [], "Event" => [], "Type" => []] */ |
Nový kód:/** @return array{Timing: list<string>, Event: list<string>, Type: list<string>} */ |
Když už jsem měl typy popsané, tak jsem si řekl, že by bylo škoda je nevyužít pro kontrolu Admineru PHPStanem. Jenže PHPStan vyžaduje opakování názvu parametru ve značce @param
a Adminer uváděl jen typ podle pořadí parametru. Nesnáším opakování, tak jsem se rozhodl, že typ parametrů deklaruji přímo v kódu a značku @param
nechám jen v případě, že parametr bude mít nějaký popis nebo typ specifičtější než PHP (např. literal-string
nebo list<int>
). Totéž jsem udělal i pro návratový typ a vlastnosti objektů. Použil jsem syntaxi PHP 7.4, kterou nyní spuštění Admineru ze zdrojových kódů vyžaduje. Při kompilaci do jednoho souboru se typy zase odstraní, čímž jednosouborová verze zůstala kompatibilní s PHP 5.3. Vedlejší efekt je i ten, že v některých zákoutích kódu může být typ špatně, takže nová verze tyto případy hned nerozbije. Byl jsem opravdu hodně vděčný za bohaté end-to-end testy, díky kterým jsem většinu typových chyb snad našel.
Původní kód:/** Get INI boolean value * @param string * @return bool */ function ini_bool($ini) |
Nový kód:/** Get INI boolean value */ function ini_bool(string $ini): bool |
PHPStan našel i řadu dalších, většinou drobných chyb. Kód Admineru teď s několika výjimkami prochází PHPStan levelem 6.
Adminer pro každou z 12 podporovaných PHP extenzi (+ další v pluginech) definuje třídu Db
. Těm jsem nyní dal společného předka, což kód v některých případech i zjednodušilo. Zvažoval jsem použití rozhraní, protože Db
pro MySQLi je potomek třídy mysqli
, takže jiného předka mít nemůže. Ale kód by pak musel mít stejné typy jako mysqli
, takže by nebyl kompatibilní zároveň se starými a novými verzemi PHP. Rozhraní navíc nedovoluje deklarovat vlastnosti. Lepší by bylo dědění z mysqli
zahodit a předělat to na proxy.
Změnil jsem implementaci třídy Plugins
pro nahrávání pluginů. Tu jsem tak nesnášel, že před verzí 5.1 jsem jejích zkompilovaných 7 kB odmítl dát přímo do Admineru a uživatele pluginů ji nutil nahrát zvlášť. Původně byla tato třída potomek třídy Adminer
a po zavolání pluginů zavolala výchozí implementaci ve třídě Adminer
. Teď jsem to změnil tak, že třída Plugins
prostě zaregistruje Adminer
jako nejspodnější plugin a volá ji stejně jako všechny ostatní pluginy. Kód třídy Adminer
jsem musel změnit tak, aby metody místo na $this
volala na adminer()
, což je svým způsobem i přesnější. Je z toho totiž jasné, že se zavolají nejprve pluginy a ne nutně implementace ve třídě Adminer
.
Původní kód:function name() { $args = func_get_args(); return $this->applyPlugin(__FUNCTION__, $args); } function credentials() { $args = func_get_args(); return $this->applyPlugin(__FUNCTION__, $args); } // kilometr dalšího kódu na stejné brdo |
Nový kód:function __call(string $name, array $args) { foreach ($this->hooks[$name] as $plugin) { $value = call_user_func_array(array($plugin, $name), $args); if ($value !== null) { return $value; } } } |
Smazal jsem všechny globální proměnné. V minulosti jich měl Adminer maximálně 23.
Kód ve verzi 4.8.1:global $adminer, $connection, $driver, $drivers, $edit_functions, $enum_length, $error, $functions, $grouping, $HTTPS, $inout, $jush, $LANG, $langs, $on_actions, $permanent, $structured_types, $has_token, $token, $translations, $types, $unsigned, $VERSION; |
Nový kód:// nic |
Přestal jsem ignorovat chybu Trying to access array offset on null, kterou PHP vyvolá třeba s kódem $_GET["where"]["_id"]
. Našlo to jeden okrajový problém.
JavaScript
Do JavaScriptu jsem přidal 'use strict'
, což si v kódu nevyžádalo žádnou změnu.
CSS
Adminer používá obrázky prakticky jen pro tlačítka ve změně struktury tabulky. Byly vložené pomocí <input type="image">
, což komplikovalo jejich stylování. Autoři designů si s tím i tak poradili, ale v nové verzi vkládám obrázky přes CSS background-image
, což stylování zjednodušuje. Stávající designy jsem na to přehodil. Při kompilaci jsem url(plus.gif)
změnil na url(data:image/gif;base64,...)
, což dovolilo odstranit kód pro servírování obrázků Adminerem. Pak jsem to zjedodušil ještě víc, obrázky inlinoval ručně a z repozitáře je smazal. Stejně se prakticky nemění a v případě potřeby je jde z CSS zase vytáhnout, upravit a vložit zpátky. Při vývoji Admineru mám vypnuté kešování a tato změna odstranila kratičké probliknutí před tím, než se obrázky stáhly.
Do CSS jsem doplnil alespoň dvě proměnné pro výchozí barvu pozadí a popředí a ve stávajících barevných designech ji nadefinoval. Důvodem je, že někdy ve výchozím vzhledu potřebuji tyto barvy použít na dalších místech a pokud s tím designy nepočítají, tak se rozbijí. Do budoucna to také umožní jednodušší vytváření nových designů.
Původní kód:/* default.css */ body { background: #fff; } .footer > div { background: #fff; } #tables a { background: #fff; } .footer { border-top-color: rgba(255, 255, 255, .7); } /* dark.css */ body { background: #002240; } .footer > div { background: #002240; } #tables a { background: #002240; } .footer { border-top-color: rgba(0, 34, 64, .7); } |
Nový kód:/* default.css */ html { --bg: #fff; } body { background: var(--bg); } .footer > div { background: var(--bg); } #tables a { background: var(--bg); } .footer { border-top-color: rgb(from var(--bg) r g b / .7); } /* dark.css */ html { --bg: #002240; } |
Za všimnutí stojí hlavně použití funkce rgb(from)
.
Testy
Přidal jsem si skript na vytvoření stejného testu pro všechny databáze a přidal s ním několik dalších testů. Doplnil jsem také alespoň základní test pro Elasticsearch.
Rozčilovalo mě, že screenshoty na homepage nejsou pro aktuální verzi, ale samozřejmě se mi je nechce dělat s každou novou verzí znovu. Tak jsem si napsal test, který mi je udělá.
Funkční změny
Žádná z těchto změn by se neměla navenek nijak projevit, proto nedošlo k žádnému bombastickému zvýšení čísla verze. Viditelných změn je jen pár:
- Opravil jsem TAR export rozbitý přidáním viditelnosti vlastností ve verzi 5.0.3. To odhalil PHPStan.
- Opravil jsem řazení podle sloupce
COUNT(*)
rozbité verzí 5.0.2 (bug #966). - Pro získání sloupců grafického schématu databáze se nově klade jen jeden dotaz. Metodu jsem zpřístupnil i pluginům, využívají to editory kódu s podporou doplňování názvů sloupců.
- Plugin pro Elasticsearch jsem opravil, aby fungoval s verzí 8 – snad jsem ho tím nerozbil pro starší verze.
- Na mobilu (nebo obecně při malém rozlišení) se menu nyní úplně schová a zobrazí se až pomocí tlačítka. Ikonu tlačítka jsem mimochodem vzal ze 12 let staré nedokončené větve.
- V tmavém vzhledu jsem invertoval ikony. Díky jejich stylovatelnosti by se daly i vyměnit, ale inverze stačila.
- Metodě
csp()
posílám aktuální CSP, což dovoluje nastavení CSP více pluginům najednou. - Přidal jsem pluginy pro editory a zvýrazňovače kódu Prism a Monaco Editor. S žádným z pěti nyní podporovaných zvýrazňovačů kódu ale nejsem úplně spokojen, takže vylepším interní JUSH. Více v samostatném článku.
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="/https/php.vrana.cz/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!
Starší články naleznete v archivu.

