Přejít k navigační liště

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

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

Články PHP

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.

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) a Finder::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
Screenshot 2015-12-11 16.42.20

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:
Screenshot 2015-12-11 16.49.15

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!

Komentáře

Odebírat
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře
Ondřej Mirtes

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.

Ondřej Machulda

Jen 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

jirkakoutny

Zrovna tady bych to bral jako žádoucí. Pokud pracuji třeba s Guzzlem taky si pro každý request nastavuji new Client()

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í.

Já

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.

HonzaMarek

Tak si dej do DI factory.

Já

No doprčic, v Symfony už píšu léta ale filesystem s lockama mi tak nějak uniklo. Díky!

Přístupnost není jen o splnění norem: nový pohled na inkluzivní design

Přístupnost a inkluze možná nepatří mezi nejžhavější témata digitálního světa – dokud o nich nezačne mluvit Vitaly Friedman. Na WebExpo 2024 předvedl, že inkluzivní design není jen o splněných checkboxech, ale hlavně o lidech. S energií sobě vlastní obrátil zažité přístupy naruby a ukázal, že skutečně přístupný web je nejen možný, ale i nezbytný.

Efektivnější vývoj UI nebo API: Co si odnést z WebExpo 2025?

Různé
Komentáře: 0
Jak snadno implementovat moderní uživatelské rozhraní? Které funkce brzdí rychlost vašeho webu? A kdy raději sami přibrzdit, abychom využitím AI nepřekročili etické principy? Debatu aktuálních dev témat rozdmýchá sedmnáctý ročník technologické konference WebExpo, která proběhne v Praze od 28. do 30. května. Který talk či workshop si rozhodně nenechat ujít? Toto je náš redakční výběr z vývojářských hroznů.

Zapřáhněte AI jako nikdy předtím. Květnová konference WebExpo přivítá hvězdy technologického světa

Od 28. do 30. května 2025 promění pražský Palác Lucerna na tři dny technologická konference WebExpo. Na programu je více než 80 přednášek a workshopů od expertů z celého světa. WebExpo tradičně propojuje vývojáře, designéry, marketéry i byznysové lídry a nabízí praktické dovednosti, strategické myšlení a přináší nejnovější trendy nejen v oblasti AI.