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 na GitHubu.
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!
Komentáře
v6ak #1
Zajímavé. Měl bych k tomu dvě poznámky:
Ale jinak to API vypadá celkem použitelně.
Jan Tichý #2
Takhle na první nástřel a bez hlubšího přemýšlení bych to udělal obráceně. Nikoliv tak, že zadám masku a tu pak filtruju konkrétním adresářem. Ale že zvolím adresář a v tom pak provádím různé věci, například filtrování určitou maskou:
Navíc pokud by se našel nějaký podadresář, tak by se mohl hned vrátit nikoliv jako SplFileInfo, alébrž opět jako instance Finder (ať už by se to pak místo Finder nazvalo jakkoliv, třeba Dir ;)), což by pak opět umožňovalo nad takovým podadresářem se ptát dalšími podmínkami, třeba nějak takhle:
David Grudl #3
#1 v6aku, protože to umí hledat i adresáře a
FileFinder::findDirectories()
by bylo divné#2 Jane Tichý, váhal jsem nad pořadím adresáře a masky a nakonec si řekl, že půjdu cestou SQL, tedy aby to znělo jako věta. Takže je to podle vzoru
SELECT * FROM dir
.Vrácet podadresáře jako Finder nevidím jako dobrý nápad. Celou věc to zbytečně komplikuje a nenapadá mě, co by to přineslo navíc oproti současnému pojetí.
Roman #4
Poměrně podobné rozhraní má perlový modul File::Find::Rule (https://metacpan.org/…::Find::Rule)
Možná by se mohlo hodit pro inspiraci.
The Zero #5
#3 Davide Grudle, Já souhlasím s #2 Jan Tichý, že je to lepší. Navrch mám za to, že cestě SQL odpovídá právě jeho řešení:
SELECT * FROM dir WHERE filename LIKE '%.txt'
v6ak #6
#3 Davide Grudle, Dobře, tak FS.
Co se týče #2 Jan Tichý, jsem pro. Jinak zrovna pořadí sloupců a tabulky se mi v SQL zdá nešťastné. Ostatně, je to jeden z důvodů, proč s DibiFluent nemohu, … Mám tu větu dokončit? Obávám se OT diskuze.
blizzboz #7
#6 v6aku, hej presne napr. v LINQ je to poradie opačné (from in select) a mne to príde prirodzenejšie ako v klasickom SQL
David Grudl #8
#2 Jane Tichý, #5 The Zero #6 v6ak #7 blizzboz otevřel jsem na tohle téma vlákno na fóru.
pes #9
ahoj, můžu mít dotaz k téhle bezva věci (myšleno finder)?
Nedaří se mi seřadit výsledek, který mi finder vrátí … například něco jako : NFinder::findDirectories(‚*‘)->in($filePath)->order(„desc“)
Řazení obejdu sice při zpracování výsledku, ale mám takový pocit, že to tam určitě je někde vyřešené a já to nemůžu najít :(
díky za případné info
David Grudl #10
Řazení tam skutečně není. Dalo by se udělat třeba takto:
pes #11
#10 Davide Grudle, Super ;) dobrý „trik“ .. díky
Jan Tvrdík #12
#10 Davide Grudle, Nestálo by za to, tam přidat pro řazení nativní podporu?
David Grudl #13
#12 Jane Tvrdíku, neznám jiné řešení než #10 David Grudl, což je pomalé a tudíž bych nechtěl, aby to bylo snadno dosažitelné jedním příkazem.
Filip Procházka #14
Ahoj, při použití samotného
Vyskočí výjimka, asi by bylo dobré ty příklady opravit :) některé jedince to mate viz https://github.com/…e/issues/277#…
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.