Pohodlné procházení filesystémem
Vymyslet dobré API je někdy neskutečný porod. Vedle toho skutečný porod je procházka růžovou ordinací. Snad dva roky jsem neustále překopával třídu na procházení adresářů. A stále nebyl spokojen. Přitom taková blbost. Existuje totiž spousta variant toho, co a jak hledat, které soubory vracet, které adresáře vracet a které procházet rekurzivně, nebo naopak kterým se vyhnout. Také jsem potřeboval řešit specifické situace, kupříkladu když během procházení adresářovou strukturou teprve zjišťuji dodatečná pravidla. Otázkou bylo, jak to navrhnout univerzálně a pokud možno srozumitelně.
Výsledkem snažení je třída Nette\Finder, jejíž API není dokonalé, ale je zatím asi to nejpoužitelnější, k jakému jsem se dopracoval. Můžete si ji stáhnout jako součást Nette 1.0 nebo i samostatně.
Pár příkladů použití:
// nerekurzivní hledání souborů *.txt v adresáři $dir
foreach (Finder::findFiles('*.txt')->in($dir) as $key => $file) {
echo $key; // $key je řetězec s názvem souboru včetně cesty
echo $file; // $file je objektem SplFileInfo
}
// rekurzivní hledání souborů *.txt
foreach (Finder::findFiles('*.txt')->from($dir) as $file) {
echo $file;
}
// hledání podle více masek a dokonce z více adresářů v rámci jedné iterace
foreach (Finder::findFiles('*.txt', '*.php')->in($dir1, $dir2) as $file) {
}
// rekurzivní hledání souborů *.txt obsahujících číslici v názvu
foreach (Finder::findFiles('*[0-9]*.txt')
->from($dir) as $file) {
}
// rekurzivní hledání souborů *.txt kromě těch, co obsahují v názvu X
// pozn.: exclude se tu vztahuje na findFiles()
foreach (Finder::findFiles('*.txt')->exclude('*X*')
->from($dir) as $file) {
}
// rekurzivní hledání souborů *.txt umístěných v adresáři
// začínajícím na "te" ale nikoliv "temp"
foreach (Finder::findFiles('te*/*.txt')->exclude('temp*/*')
->from($dir) as $file) {
}
Omezit hloubku procházení lze metodou limitDepth().
Kromě souborů lze hledat i adresáře přes
Finder::findDirectories('subdir*') nebo obojí
Finder::find('file.txt'). V takovém případě se maska vztahuje
na soubory, nikoliv adresáře.
Adresáře, kterým se chceme zcela vyhnout, uvedeme za klauzulí „from“:
// tady se exclude vztahuje na klauzuli "from"
foreach (Finder::findFiles('*.php')
->from($dir)->exclude('temp', '.git') as $file) {
}
Nejen maskou lze výsledky filtrovat:
// prochází soubory v rozmezí 100B až 200B
foreach (Finder::findFiles('*.php')->size('>=', 100)->size('<=', 200)
->from($dir) as $file) {
}
// prochází soubory změněné v posledních dvou týdnech
foreach (Finder::findFiles('*.php')->date('>', '- 2 weeks')
->from($dir) as $file) {
}
// prochází soubory PHP s počtem řádku větším než 1000 filtrujeme callbackem
$finder = Finder::findFiles('*.php')->filter(function($file) {
return count(file($file->getPathname())) > 1000;
})->from($dir);
V Nette lze jít dál a třídu Nette\Finder skrze extension
methods dále rozšiřovat a poté můžete třeba:
// hledat obrázky s rozměry většími než 50px x 50px
foreach (Finder::findFiles('*')->dimensions('>50', '>50')
->from($dir) as $file) {
}
Třída funguje na Windows i Linuxu a je napsána co nejoptimálněji, měla by tudíž fungovat velmi rychle a neprochází zbytečně adresáře, které nemá. Enjoy!
Jsou tyto URL stejné?
Otázka, kterou si klade řada webmasterů: vnímají vyhledávače tyto URL jako stejné? Jak s nimi naložit?
http://example.com/articlehttp://example.com/article/http://example.com/Articlehttps://example.com/articlehttp://www.example.com/article
http://example.com/article?a=1&b=2http://example.com/article?b=2&a=1
Stručná odpověď by byla: „URL jsou odlišné.“ Chce to ale podrobnější rozbor.
Z pohledu uživatelů se tyto adresy liší v drobnostech, kterým nepřikládají žádnou váhu. Tedy je vnímají jako stejné, ačkoliv z technického hlediska jde o adresy různé. Říkejme jim třeba adresy podobné. V zájmu uživatelského prožitku proto dodržujte 2 zásady:
- Nedovolte, aby se na podobných adresách nacházel odlišný obsah. Jak záhy ukážu, nevedlo by to jen ke zmatení uživatelů, ale i vyhledávačů.
- Umožněte uživatelům přístup i přes podobné adresy.
Pokud se adresy liší v protokolu http / https
nebo doméně s www či bez, považují je vyhledávače za
různé. Nikoliv tak uživatelé. Bylo by tedy fatální chybou na takto
podobné adresy umístit různý obsah. Nicméně chybou by bylo
i znemožnění přístupu přes podobnou adresu. Musí tedy fungovat
adresa s www i bez www. Přičemž SEO nabádá
držet se jedné varianty a ostatní na ni přesměrovávat HTTP kódem 301. To
lze u www subdomény zajistit například souborem
.htaccess:
# presmerovani na variantu bez www
RewriteCond %{HTTP_HOST} ^www\.
RewriteRule ^.*$ http://example.com/$0 [R=301,NE,L]
# presmerovani na variantu s www
RewriteCond %{HTTP_HOST} !^www\.
RewriteRule ^.*$ http://www.example.com/$0 [R=301,NE,L]
Schválně si hned vyzkoušejte, jestli vaše servery přesměrovávají a to
včetně celé adresy a správného
předání parametrů. Nezapomeňte i na varianty
www.subdomena.example.cz. Protože některé prohlížeče umí
chybějící přesměrování obcházet, zkuste raději nízkoúrovňovou
službu jako Web-Sniffer.
Velká a malá písmena se v URL rozlišují všude kromě schéma a domény. Avšak uživatel je opět nerozlišuje a je tedy nešťastné nabízet rozdílný obsah na adresách lišících se jen velikostí písmen. Jako špatný příklad může posloužit Wikipedie:
- http://en.wikipedia.org/wiki/Acid o kyselinách
- http://en.wikipedia.org/wiki/ACID o databázových transakcích
Kuriózní chybou trpí Bing, který vrací stejné URL, ať už hledáte kyselinu nebo databázi (ačkoliv textový popis je správný). Google nebo Yahoo tímto problémem netrpí.
Bing nerozlišuje mezi kyselinou a databází
Také některé služby (webmaily, ICQ) převádí velká písmenka v URL na malá, což jsou všechno důvody, proč se rozlišování velikosti vyhnout a to nejlépe i v parametrech. Raději dodržujte konvenci, že všechna písmenka v URL budou malá.
Rozlišit některé podobné adresy je oříšek i pro vyhledávače. Udělal jsem experiment a na URL lišící se v detailech jako je přítomnost pravostranného lomítka nebo pořadí parametrů umístil odlišný obsah. Zaindexovat je byl schopen pouze Google. Ostatní vyhledávače dokázaly pojmout vždy jen jednu z variant.
Jen Google tyto stránky umí indexovat jako rozdílné
Pokud jde o pravostranná lomítka, webový server přesměrovává na kanonickou podobu obvykle za vás; přistoupíte-li k adresáři bez pravostranného lomítka, doplní je a přesměruje. Samozřejmě to neplatí, když URI spravujete ve vlastní režii (Cool URI apod.)
A nakonec: skutečně záleží na pořadí parametrů? Mezi adresou
article?a=1&b=2 a article?b=2&a=1 by neměl
být žádný rozdíl. Jsou ale situace, kdy to neplatí, zejména když
předáváme složitější struktury jako třeba pole. Například
?sort[]=name&sort[]=city může být něco jiného, než
?sort[]=city&sort[]=name. Nicméně přesměrovávat, nejsou-li
parametry ve stanoveném pořadí, bych považoval za zbytečnou
hyperkorektnost.
p. s. Nette Framework přesměrování na kanonické URL provádí zcela automaticky ve své režii
Formuláře a HTML5 - co mi ještě chybí
Pracovat s webovým formulářem na straně JavaScriptu se poměrně snadno může stát očistcem. Nebo jak se nazývá ta věc na čištění záchodové mísy. Přitom za všechno může jedno nešťastné rozhodnutí.
Mějme jednoduchý formulář
<form id=myform>
<input type=text name=query>
<input type=submit value="Vyhledat">
</form>
K jeho jednotlivým prvkům přistoupíme přes vlastnost
elements:
var form = document.getElementById('myform');
var query = form.elements.query.value;
// pochopitelně také form.elements['query'].value
A iterovat nad prvky lze cyklem for:
for (var i = 0; i < form.elements.length; i++) {
alert(form.elements[i].value);
}
Z historických důvodů lze vlastnost elements v příkladech
vynechat, neboť jednotlivé prvky se mapují přímo do objektu
form. Takže by fungovalo i form.query.value,
form.length nebo form[i].value. Což se záhy ukáže
jako nemilé. Formulářové prvky totiž přepisují nativní
metody a proměnné objektu form. Například metoda submit(),
která je klíčová pro AJAXové odeslání formuláře, se stane nedostupnou,
pokud formulář obsahuje prvek nazvaný submit. A uznejte, že to
je zrovinka název pro odesílací tlačítko jako dělaný. Pokud by tedy
formulář vypadal takto
<form id=myform>
<input type=text name=query>
<input type=submit name=submit value="Vyhledat">
</form>
nebude jej možné příkazem form.submit() odeslat, místo toho
JavaScript zařve „form.submit is not a function“ a má pravdu,
form.submit není funkce ale objekt HtmlInputElement. Dobrá, dáme
si pozor a nebudeme formulářové prvky nazývat submit.
(prozradím trik, jak by formulář šlo odeslat i v tomto případě:
document.createElement('form').submit.call(form))
Jenže název submit není tím jediným, kterému se musíme
vyhnout. Prvek pojmenovaný elements způsobí, že
form.elements nebude očekávaná kolekce a výše uvedené
příklady skončí chybou. Název length zase znemožní nad
kolekcí iterovat. A tak by se dalo pokračovat, nativních prvků třeba DOM
Firefoxu definuje hodně přes stovku. Pro zajímavost uvádím seznam jen těch
jednoslovných (tj. vynechávám nodeName nebo
innerHTML):
action, attributes, blur, children, dir, draggable, elements, encoding, enctype, focus, id, lang, length, method, name, normalize, prefix, reset, spellcheck, style, submit, target, title
Bylo by skvělé, kdyby se HTML5 dokázalo s tímto nešvarem vypořádat. Zatím jsem ve specifikaci nic takového nenašel.
Podobně mi chybí možnost, jak v obsluze události onsubmit
zjistit, kterým tlačítkem byl formulář odeslán. Triviální a užitečná
věc a jak složitě se musí řešit.
<form id=myform onsubmit="kterým tlačítkem byl odeslán?">
<input type=hidden name=id value="123">
<input type=submit name=save value="Uložit">
<input type=submit name=delete value="Smazat">
</form>
Řeším to tak, že odchytávám událost click jednotlivých
tlačítek a název prvku ukládám do vlastní proměnné formuláře. O něco
jednodušší je využít bublání a odchytávat click přímo na
formuláři. Nicméně v HTML5 tohle už nebude fungovat spolehlivě, protože
prvek může být umístěn i mimo strom formuláře a přiřazen k němu
atributem form. Bylo by tedy fajn, kdyby HTML5 zavedlo vlastnost
například form.submitter, který by vracela název
odesílajícího tlačítka.
p. s. Nette Framework s těmito situacemi počítá a snaží se je v rámci možností řešit za programátora
Programátoři chyby neignorují
Tedy alespoň by neměli. PHP je jazyk s poměrně laxním přístupem k chybám a tudíž vyžaduje od programátora vyvinout větší úsilí při jejich ošetřování. Nenechte si namluvit opak. Článek je reakcí na dobře míněnou radu Jakuba Vrány.
Existují dva tradiční způsoby, jak chyby oznamovat:
- vyhozením výjimky
- návratovou hodnotou
Takřka všechny knihovny dodávané s PHP používají druhý způsob, protože výjimky byly do jazyka zavedeny až v páté verzi. Což je v mnoha případech na škodu. Programátor totiž musí neustále otrocky kontrolovat návratové hodnoty a ošetřovat chybové stavy. A zároveň je tu riziko, že na to zapomene. Přesněji řečeno, ani ne tak „zapomene“, jako spíš se na to „vy…kašle“, protože to ve světě PHP platí za normu. Nakonec, podívejte se třeba do dokumentace fread a spočítejte, kolikrát v příkladech ošetřili návratovou hodnotu jakékoliv funkce.
Při takovém stylu práce pak není divu, že programátoři považují výjimky za něco otravného, co se (cituju Jakuba) „nedá ignorovat“, zatímco „chyby indikované návratovou hodnotou ignorovat lze a program nejspíš nějak pracovat bude.“ Notykrávo.
Co přesně znamená ono nějak pracovat? Třeba:
// přesouváme z disku na disk
copy('c:/oldfile', 'd:/newfile');
unlink('c:/oldfile');
// pokud první operace selže, soubor se nenávratně smaže
$link = new mysqli('localhost', $user, $pass, 'eshop');
...
$link->query('USE testdata'); // přepneme se z ostré do testovací databáze
$link->query('DELETE FROM orders');
// pokud předchozí operace selže, vymažou se objednávky z ostré databáze
// BTW věřili byste, že výchozí nastavení query() při chybě nevyhodí ani noticku?
// smažeme soubor z adresáře 'test'
ftp_chdir($connection, 'test');
ftp_delete($connection, 'database.sdb');
// pokud předchozí operace selže, vymaže se kupříkladu ostrá databáze
A pak jsou případy, kdy nějak pracovat znamená zařvat s neošetřitelnou fatální chybou:
// načtení konfigurace
$defaults = array('host' => 'localhost', 'charset' => 'utf8');
$config = parse_ini_file('config.ini');
$config = $config + $defaults;
// pokud předchozí operace selže a vrátí FALSE, skončí sčítání fatální chybou
function saveConfig(array $config) { ... }
$config = parse_ini_file('config.ini');
saveConfig($config);
// pokud předchozí operace selže a vrátí FALSE, skončí opět fatální chybou
mysqli_connect('localhost', $user, $pass)
->query("SET NAMES 'utf8'");
// pokud první operace selže, skončí opět fatální chybou
Bastlení zdar!
Víte, komu ublížil mod_rewrite?
Schválně, který software má v dokumentaci uvedeno, že se jedná o voodoo? No jistě, jde o mod_rewrite. Ze zkušenosti mohu říci, že programátoři se dělí do dvou skupin:
- ti, kteří mod_rewrite nerozumí
- ti, kteří si myslí, že mod_rewrite rozumí, avšak mýlí se
Do které skupiny patříte vy? Zkuste nahlédnout do svých souborů
.htaccess a podívejte se, zda vám u pravidel pro
přesměrování (příznak
R) nechybí také příznak NE (noescape)?
Vysvětlím na příkladu: do kořenového adresáře webu
www.example.cz vložím soubor .htaccess s pravidlem
pro přesměrování:
RewriteEngine On RewriteRule .* http://www.example.com/$0 [R=301] #tohle je spatne!
Server pak přesměruje
- z
http://www.example.cz/index.php?title=d%C3%ADvka(parametrtitleobsahuje slovodívka) - na
http://www.example.com/index.php?title=d%25C3%25ADvka(parametrtitleobsahuje řetězecd%C3%ADvka)
Jak vidíte, mod_rewrite ublížil dívce! Je to jeho přirozené chování,
aby to nedělal, musíte mu říct NE:
RewriteEngine On RewriteRule .* http://www.example.com/$0 [R=301,NE] #tohle uz je spravne
novější články
