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

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
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!

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.