Symfony po krůčkách – Filesystem a Finder

V dnešním článku si ukážeme, jak v Symfony za pomocí komponent Filesystem a Finder pracovat se soubory. Projdeme si jednotlivé funkce a vše si vyzkoušíme na jednoduchém projektu.
Seriál: Symfony po krůčkách (18 dílů)
- Symfony po krůčkách – Event Dispatcher 30. 11. 2015
- Symfony Console jako první rande se Symfony 7. 12. 2015
- Symfony po krůčkách – Filesystem a Finder 14. 12. 2015
- Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání 21. 12. 2015
- Symfony po krůčkách – spouštíme procesy 4. 1. 2016
- Symfony po krůčkách – Translation – překlady jednoduše 11. 1. 2016
- Symfony po krůčkách – Validator (1) 18. 1. 2016
- Symfony po krůčkách – Validator (2) 25. 1. 2016
- Symfony po krůčkách – Routing 1. 2. 2016
- Symfony po krůčkách – MicroKernel 9. 2. 2016
- Konfigurujeme Symfony pomocí YAMLu 16. 2. 2016
- Symfony po krůčkách – oblékáme MicroKernel 23. 2. 2016
- Symfony po krůčkách – ClassLoader 29. 2. 2016
- Symfony po krůčkách – Twig 8. 3. 2016
- Symfony po krůčkách – Twig II. 15. 3. 2016
- Symfony po krůčkách – DomCrawler a CssSelector 23. 3. 2016
- Symfony po krůčkách – HTTP fundamentalista 12. 4. 2016
- Symfony po krůčkách – ušli jsme pořádný kus 19. 4. 2016
Není to vynalézání kola?
PHP má pro práci se soubory už vestavěné funkce, proč tedy přidávat další vrstvu? Odpověď je jednoduchá – nativní funkce se sice snaží replikovat příkazy, které známe z konzole, ale ne už jejich funkcionalitu. A pokud narazí na chybu, jediné, čeho se zpravidla dočkáme, je warning:
Jan-MacBook-Air:~ klatys$ php -a
php > mkdir('tmp/test');
PHP Warning: mkdir(): No such file or directory in php shell code on line 1
php > copy('./foo.log', './tmp/test/');
PHP Warning: copy(): The second argument to copy() function cannot be a directory in php shell code on line 1
vs.
Jan-MacBook-Air:~ klatys$ mkdir -p tmp/test
Jan-MacBook-Air:~ klatys$ cp foo.log ./tmp/test/
Chytrý Filesystem v Symfony
Komponenta se vám nejenže snaží poskytnout stejnou funkcionalitu, ale v případě chyby vrátí výjimku Symfony\Component\Filesystem\Exception\IOException
, se kterou se lépe pracuje.
Další výhodou pak je, že některé metody (exists()
, mkdir()
, touch()
, remove()
…) dokážou na vstupu přijmout pole cest a zpracovat je naráz.
Zkusme si pro začátek vytvořit zanořenou složku a do ní zapsat soubor. Využijeme také Console komponentu z předchozího dílu.
$filesystem = new Filesystem();
//vytvoříme si soubor se kterým budeme pracovat
$filesystem->dumpFile("foo.txt", "Příšerně žluťoučký kůň úpěl ďábelské ódy");
//složku do které jej přesuneme
$filesystem->mkdir("tmp/test");
//a jdeme na to!
$filesystem->copy("foo.txt", "tmp/test/foo.txt", true);
echo "Prošlo to?\n";
echo $filesystem->exists("tmp/test/foo.txt") ? "jo!" : ":(";
a pustíme si jej:
$ ./console filesystem:1
Prošlo to?
jo!
Kompletní kód příkladu najdete na Githubu
V příkladu jsme použili mkdir()
a copy()
, které známe z unixových systémů, a jednu novou – dumpFile()
.
Kdo z vás se již někdy snažil z php atomicky měnit soubory, ví, kolik kódu kolem toho musí napsat. Metoda dumpFile()
to řeší elegantně za vás – nejprve zapíše do dočasného souboru a teprve po vydumpování celého obsahu jej přejmenuje.
Konkrétně vypadá takto:
/**
* Atomically dumps content into a file.
*
* @param string $filename The file to be written to.
* @param string $content The data to write into the file.
*
* @throws IOException If the file cannot be written to.
*/
public function dumpFile($filename, $content)
{
$dir = dirname($filename);
if (!is_dir($dir)) {
$this->mkdir($dir);
} elseif (!is_writable($dir)) {
throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir);
}
$tmpFile = $this->tempnam($dir, basename($filename));
if (false === @file_put_contents($tmpFile, $content)) {
throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename);
}
$this->rename($tmpFile, $filename, true);
}
File Locking
Každý už také někdy jistě řešil zamykání přes soubor – typicky pokud se vám mohou překrývat crony, může docházet k nemilým situacím, kdy dva procesy zpracovávají tatáž data nebo si je navzájem přepisují. Pro tyto případy je pak dobré si vytvořit souborový zámek:
$lockHandler = new LockHandler("mujcron.lock");
if (!$lockHandler->lock()) {
echo "Jiná instance commandu ještě běží!";
return false;
}
Chytáme výjimky
Zkusíme si napsat další command a zapracovat také odchytnutí výjimky:
$filesystem = new Filesystem();
$files = [
"http://placekitten.com/408/287",
"http://placekitten.com/300/128",
"http://placekitten.com/123/456",
"http://placekitten.com/54/68",
"http://foo.bar/123"
];
foreach ($files as $key => $file) {
try {
$targetDir = "tmp/".$key;
$filesystem->mkdir($targetDir);
$targetFile = $targetDir . "/" . $key . ".jpg";
$outputInterface->write("kopíruji " . $file . " do " . $targetFile." - ");
$filesystem->copy($file, $targetFile);
} catch (IOException $e) {
$outputInterface->writeln("Chyba ".$e->getMessage());
continue;
}
$outputInterface->writeln("OK!");
//Pro další příklad si ještě upravíme čas přístupu
$accessDate = new DateTime();
$accessDate->sub(new DateInterval("P".$key."D"));
$filesystem->touch($targetFile, $accessDate->format("U"), $accessDate->format("U"));
}
Celý kód příkladu najdete na Githubu
Finder
Filesystem je skvělý na práci se soubory – jejich kontrolu, úpravu atributů a zápis. Finder nám pomůže s tím, jak soubory najít. S jeho pomocí tak můžeme napsat jednoduchého správce souborů.
Soubory i složky lze filtrovat dle
- Názvu
Finder::name($pattern)
například$finder->name(“*.jpg”);
- Obsahu
Finder::contains($pattern)
aFinder::notContains($pattern)
například$finder->contains(“foo”);
- Velikosti
Finder::size($size)
například$finder->size('< 1.5K');
- Datumu modifikace
Finder::date($date)
například$finder->date(">= -3 day”)
– dají se použít výrazy zpracovatelné funkcístrtotime()
- Poslední možnost je napsat si vlastní callback, je však třeba mít na paměti, že se pak prochází všechny soubory, které odpovídají jiným filtrům
$finder->files()->filter(function (\SplFileInfo $file) { if (strlen($file) > 10) { return false; } } );
Ve výchozím režimu hledá Finder jak soubory, tak složky. Pokud si chceme vybrat jen z jednoho, stačí použít metodu Finder::directories()
nebo Finder::files()
. Například tedy:
$finder->files()->date(“since yesterday”);
V předchozích příkladech jsme si vytvořili složky se soubory a nastavili jim různá data modifikace. Pojďme si je tedy vypsat. Opět sáhneme po znalostech z minulého článku, tentokrát pro výpis použijeme Table.
$table = new Table($outputInterface);
$table->setHeaders([
"Název",
"Velikost",
"Datum modifikace",
]);
foreach ($finder->in("tmp") as $key => $item) {
/** @var SplFileInfo $item */
$table->addRow(
[
$item->getRelativePathname(),
$item->isDir() ? "---" : round(($item->getSize() / 1024), 1) . "kB",
date("Y-m-d H:i:s", $item->getMTime()),
]
);
}
$table->render();
Celý kód příkladu najdeš na Githubu
Pokud si pustíme příkaz, vypíše nám všechny soubory a složky vytvořené v předchozích příkladech
Vyzkoušet si můžeme také filtrování výsledků – do příkladu jsem zahrnul filtrování:
podle jména
$finder->name((string)$input->getOption("name"));
podle datumu modifikace
$finder->date(">= -" . (int)$input->getOption("max-age") . " day");
a podle velikosti souborů
$finder->files()->size("< " . (int)$input->getOption("max-kbytes") . "K");
Díky kterým lze výpis jednoduše filtrovat:
2 tipy, které se vám budou hodit
- Finder ve výchozím stavu ignoruje běžné názvy verzovacích složek konkrétně
private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg');
ignorování lze vypnout přes$finder->ignoreVCS(false);
- Pokud chcete procházet jen složky ke kterým máte přístup, lze použít příkaz
$finder->ignoreUnreadableDirs();
Zase o krok dále
Dnes jsme si ukázali, že práce se soubory může být díky Symfony komponentám radost.
A to hlavně díky tomu, že:
Filesystem
- se při ukládání souboru postará o složku
- umí uzavírat soubory a předejít neúplným nebo neplatným datům na disku
- převádí chyby na výjimky, které můžeme zpracovat
Finder
- nám najde soubory i složky
- v nich umí filtrovat podle názvu, typu, data vytvoření a velikosti
- má pár hezkých vychytávek pro různé edge case
Celý projekt najdete na Githubu a můžete si s ním dále hrát.
Dost čtení, přijďte si taky pokecat
Už zítra 15. 12. v 18:00 hod v Praze nebo Brně nad číší vychlazeného piva. V Ostravě až od nového roku.
Poradíme vám a podpoříme vás. Těšíme se!
Věřím, že obě komponenty jsou fajn, ale před pár dny mě celkem zklamal Finder a jeho „stavovost“. Pro každé nové hledání byste ideálně měli založit novou instanci, nelze ho tedy použít jako servisu v DI kontejneru.
Pokud víckrát za sebou zavolám a proiteruju
foreach ($finder->files()->name('*.php')->in($realpath)
na různých adresářích, tak si Finder pamatuje soubory z toho předchozího hledání a vrátí je i při procházení dalších.Zrovna tady bych to bral jako žádoucí. Pokud pracuji třeba s Guzzlem taky si pro každý request nastavuji
new Client()
Nicméně jde to – pokud si v services nastavíš
scope: prototype
bude pokaždé předána nová instanceJen pozor, od Symfony 2.8 se to definuje jen jako
shared: false
(a každé get nad DIC vrátí novou instanci), scope bylo v Symfony 3.0 tuším odstraněno.https://symfony.com/doc/current/cookbook/service_container/shared.html
Vidíš to jsem přehlídl. Díky!
Honzo, nevidím důvod, proč nemít třídu Client z Guzzle jako službu. Pokud chceme dělat v DI, tak bychom přeci „new“ měli používat jen u přepravek a entit. Nikdy u tříd, co poskytují větší funkcionalitu.
Co když budeš chtít najednou všude v aplikaci do třídy Client přidat nějaké nové nastavení/závislosti?
Jinak souhlasím s Ondrou Mirtesem, že stavovost Finderu je prasárna. Využitím shared: false v definici služby jen zachraňujeme situaci, kdy Finder vývojáři Symfony napsali blbě. Vznikne nám tak automatická továrna na objekty typu Finder.
Pokaždé když použijeme továrnu na vytvoření objektu s nějakou větší funkcionalitou (tedy ne přepravky nebo entity), tak jsme selhali v tom, abychom dělali správné DI. Vždy je z toho udělat bezstavová služba, se kterou se dlouhodobě pracuje mnohem lépe a je to mnohem více bullet-proof řešení.
Aha, rozumim tomu spravne ze podle vas a podle „spravneho DI“ nema zadna trida drzet zadny stav? Nemuzu si pomoct ale slovy klasika mi prijde Vas nazor zaostaly.
Tak si dej do DI factory.
No doprčic, v Symfony už píšu léta ale filesystem s lockama mi tak nějak uniklo. Díky!