Práce se soubory

Autor: Alphard
Předchozí díly vás seznámily se základy programování nejen v PHP a některými nezbytnými principy webového vývoje. Nyní se dostáváme k práci se soubory, která umožní tvořit skutečně dynamické webové aplikace i velmi užitečné konzolové scripty. Na tvorbu anket nebo registraci uživatelů, které vás asi hned napadnou, je sice lepší databáze, ale i práce se soubory má své nezastupitelné místo. Možnost načítat data se vzdálených serverů, zpracovávat je a nějakým způsobem exportovat se vám určitě bude mnohokrát hodit.

Výpis souborů


Často potřebujeme vypsat seznam souborů z určitého adresáře. Například pro potřeby fotogalerie budeme chtít procházet adresář s fotkami, vypsat <img> tagy miniatur a vytvořit odkazy na originály. Dříve se hojně používala kombinace funkcí opendir(), readdir() a closedir() (znalí C a následovníků práci s handlery dobře znají), nyní se ale seznámíme hlavně se scandir() a glob(). Výhodou těchto funkcí je, že nám nalezené soubory vrátí v poli, se kterým lze snadno pracovat běžným způsobem. (Nevýhodou je, že pro práci s extrémně velkým množstvím souborů zaberou více paměti a budou mírně pomalejší, takový případ nyní ale nepředpokládáme.)
array scandir ( string $directory [, int $sorting_order = SCANDIR_SORT_ASCENDING [, resource $context ]] )
Funkce scandir přijme v prvním parametru adresář, který chceme procházet, a vrátí pole s názvy nalezených souborů. Druhým parametrem můžeme ovlivnit pořadí.
array glob ( string $pattern [, int $flags = 0 ] )
Funkce glob, na rozdíl od scandir, poskytuje lepší možnosti filtrování podle názvu souboru. V prvním parametru přijímá masku souboru (název souboru se zástupnými znaky), podle kterého filtruje navrácené výsledky. Druhým parametrem lze ovlivnit chování funkce, nejčastěji se může hodit GLOB_ONLYDIR pro vrácení pouze adresářů nebo GLOB_BRACE použité většinou pro filtrování různých přípon. Návratovou hodnotou je pole obsahující cesty k souborům (scandir vrací jen názvy souborů, zde jsou celé cesty).

Příklad: Výpis souborů
Předpokladem je adresář images obsahující nějaké soubory na úrovni spouštěného souboru.

$dir = 'images/';
$files = scandir($dir);
foreach ($files as $file)
{
    echo '<a href="'.$dir.$file.'">'.$file.'</a><br>'.PHP_EOL;
}

Příklad: Výpis miniatur fotek a vytvoření odkazů na originály
Předpokladem nyní je mít v adresáři fotky ve formátu *.jpg (nebo gif, png) a k nim vytvořené miniatury pojmenované small_*.jpg (název samotný je shodný jako u originálu). Jak takovou strukturu vytvořit automaticky po uploadu si ukážeme později.
$dir = 'images/';
$files = glob($dir.'small_*.{jpg,gif,png}', GLOB_BRACE);
foreach ($files as $file)
{
    echo '<a href="'.str_replace('small_', '', $file).'">';
    echo '<img src="'.$file.'">';
    echo '</a>'.PHP_EOL;
}

Čtení a zápis do souboru


Podobně jako pro procházení adresářů, i pro práci se soubory samotnými dříve byla využívána kombinace funkci fopen(), fread()/fwrite()/aj. a fclose(). Tyto funkce stále mají své využití pro práci s velkými objemy dat, které nechceme celé načítat do paměti, ale pro běžné soubory dnes ve většině případů využijeme jednodušší alternativy v podobě file_get_contents() a file_put_contents().
string file_get_contents ( string $filename [, bool $use_include_path = false
[, resource $context [, int $offset = -1 [, int $maxlen ]]]] )
File_get_contents() je funkce pro načtení obsahu souboru. První parametr obsahuje cestu k souboru, ostatní se téměř nepoužívají, $context bude zmíněn v kapitole Vzdálené soubory. Návratovou hodnotou je řetězec s obsahem souboru, nebo false při neúspěchu.
int file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] )
File_put_contents() slouží pro zápis řetězců do souboru. První parametr opět obsahuje cestu k souboru. Pokud soubor neexistuje, bude vytvořen*, pokud existuje, bude přepsán (tj. původní obsah se smaže). Druhý parametr obsahuje zapisovaná data. Jako třetí parametr se nejčastěji používá volba FILE_APPEND, díky které není existující soubor přemazán, ale nový obsah je přidán na konec.
*Chceme-li vytvořit/změnit soubor na linuxovém serveru, musí být nastavena dostatečná oprávnění. Jsou to základy linuxového souborového systému, měli byste je znát, viz www.abclinuxu.cz/ucebnice/zaklady/principy-prace-se-systemem/pristupova-prava.
bool file_exists ( string $filename )
V mnoha situacích potřebujeme nejdříve zjistit, jestli určitý soubor existuje. Ať již pro to, že nechceme číst neexistující soubor, nebo nechceme přepsat existující. Funkce file_exists() vrátí true, pokud soubor existuje, nebo false, pokud neexistuje.

Příklad: načtení obsahu souboru
Předpokládejme, že k naší fotogalerii z předchozích příkladů chceme vypsat název alba. Můžeme to řešit volitelným přidáním souboru description.txt do adresáře s fotkami.

$dir = 'images/';
$dFilename = 'description.txt';
if (file_exists($dir.$dFilename)
{
    echo trim(file_get_contents($dir.$dFilename));
}
Pokud soubor existuje, je načten a jeho obsah zobrazen.

Někdy může být užitečná funkce, která obsah souboru rozdělí na řádky a ty vrátí v poli. Typicky ji využijeme pro zpracování souborů, kde jsou jednotlivě zpracovávané položky na samostatných řádcích. Přesně tohle dělá funkce file().

array file ( string $filename [, int $flags = 0 [, resource $context ]] )
V prvním parametru předáme cestu k souboru, proměnnou $flags můžeme ovlivnit chování funkce. Zde se typicky hodí používat 2 konstanty najednou, spojíme je pomocí bitového operátoru |; tj. FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, ve výsledku budou ignorovány prázdné řádky a odřádkování na konci řádků. Návratovou hodnotou je pole obsahující jednotlivé řádky souboru.

Příklad: Přidání popisků k jednotlivým fotografiím
Do dříve vytvořeného souboru přidáme popisky jednotlivých (ne nutně všech) fotografií ve tvaru klíč::popisek. U jednotlivých popisků k fotkám použijeme jako klíč název fotky, u nadpisu text head.

head::Skvělý rok 2013
magistrala.jpg::Já na Sibiři, když mi ujel vlak
lipno.jpg::Když přítelkyni spadly plavky
zlodej.jpg::Kradou nám auto
/* nacteni a priprava popisku */
$description = [];
if (file_exists($dir.$dFilename))
{
    $d = file($dir.$dFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($d as $k => $line)
    {
        list($key, $value) = explode('::', $line);
        $description[$key] = $value;
    }
}

/* fotky */
$files = glob($dir.'small_*.jpg');

foreach ($files as $file)
{
    $fn = str_replace('small_', '', $file);
    echo '<div>'.PHP_EOL.'<a href="'.htmlEscape($file).'">';
    echo '<img src="'.htmlEscape($file).'">';
    echo '</a>'.PHP_EOL;
    if (isset($description[$path($fn)]))
    {
        echo '<p>'.htmlEscape($description[$path($fn)]).'</p>'.PHP_EOL;
    }
    echo '</div>'.PHP_EOL;
}
Kompletní kód obsahující funkce htmlEscape() a $path() (lambda funkce) je uveden později.

Příklad: Logování přístupu na stránky
Ukázka přidávání obsahu na konec souboru.

$log = 'Pristup na stranku '.$_SERVER[REQUEST_URI].' v '.
    date('Y-m-d H:i:s').' z IP adresy: '.$_SERVER['REMOTE_ADDR'].PHP_EOL;
file_put_contents(date('Y-m-d').'.log', $log, FILE_APPEND);

Manipulace se soubory a adresáři


Již byly detailně představeny nejčastěji používané funkce, nyní heslovitě uvedu i ty zbylé pro kompletní práci s adresářovým stromem. Detaily hledejte v manuálu.

Znáte-li detailněji linuxový souborový systém, najdete zde funkce jako is_link() a podobné, v praxi je ale v PHP zřejmě běžně nevyužijete.

Informace o souborech


Hodně funkcí s předponou file* vrací informace o souboru. Nejčastěji se používá filesize() vracející velikost a filemtime() vracející čas poslední modifikace souboru. Další funkce jako fileinode() apod. nejsou běžně moc využívané, hodí se spíš pro automatizaci administračních úkonů, najdete je v manuálu. Souhrnné informace poskytne fstat().
Nevíme-li dopředu, s jakým typem souborem pracujeme, můžeme použít některé z funkcí zjišťujících určité vlastnosti.

Některé funkce zjišťující informace o souboru načítají data z interní cache. Může-li být soubor paralelně měněn jiným programem a cache se stát neaktuální, lze ji smazat funkcí clearstatcache().

Ostatní funkce pro práci se soubory a adresáři


Z relativně často používaných funkcí jsem detailněji nerozebíral hlavně ty pro práci s adresáři a soubory pomocí ukazatelů na soubor (file pointer). Jejich použití má smysl hlavně u objemných souborů, které není rozumné načítat celé do paměti a je třeba zpracovávat je po částech.
Soubor otevřeme funkci fopen($filename, $mode). Cesta k souboru v prvním parametru je zřejmě stejná jako u předchozích funkcí, méně jasný může být druhý parametr, kterým určujeme, v jakém režimu se má soubor otevřít:

V dokumentaci je formuláváno, že se funkce pokusí vytvořit soubor. Vždy pamatujte na to, že pokus o vytvoření souboru může selhat.

Návratovou hodnotou funkce fopen je ukazatel na soubor, který můžeme použít ve funkcích jako


Čtení/zápis probíhá na pozici kurzoru, který je automaticky posouván buď dosud uvedenými funkcemi, nebo manuálně nastavován funkcemi fseek() a rewind().

Po dokončení práce, zvláště pokud jste prováděli zápis, nezapomeňte soubor zavřít pomocí fclose(). (Soubor by se po skončení scriptu zavřel sám, ale mohou nastat různé situace, kdy nezavřený soubor vyvolá problémy.)

Příklad: čtení souborů pomocí fgets

$handle = @fopen("/tmp/inputfile.txt", "r");
if ($handle)
{
    while (($buffer = fgets($handle, 4096)) !== false)
    {
        echo $buffer;
    }
    if (!feof($handle))
    {
        echo "Error: unexpected fgets() fail\n";
    }
    fclose($handle);
}
else
{
    echo "Error: file cannot be open\n";
}
Toto je mírně upravený manuálový příklad. Vidíme otevření souboru pro čtení, potlačení případné chyby a následující práci podmíněnou správným otevřením souboru. Data jsou načítaná po jednom řádku (ale maximálně 4096 bajtů, tj. znaků), dokud se nedosáhne konce souboru a funkce fgets() nevrátí false. V další části funkce feof() testuje, jestli bylo dosaženo konce souboru. Správně by mělo být; pokud ale fgets vrátila false, a přesto kurzor není na konci souboru, došlo k nějaké chybě, což vypíšeme. Na závěr nezapomeneme zavřít soubor.

Všechny souborové funkce jsou uvedeny na www.php.net/manual/en/book.filesystem.php.

Cesty


Pro práci se soubory je nutné dobře se zorientovat v pohybu mezi adresáři.
Absolutní cestu k aktuálnímu scriptu poskytne magická konstanta __DIR__. Pro pohyb do dalších adresářů slouží klasicky sestavená cesta 'dalsi/zanorene/adresare', pohyb o úroveň výš je zajištěn '..'. Když někde vidíte cestu začínající './', explicitně to značí aktuální adresář, naopak '/adresar' se odkazuje do rootu.
realpath() vyhodnotí symbolické odkazy (tj. relativní cesty) a vrátí standardizovanou absolutní cestu k souboru. Dohledávní souborů určených relativními cestami není vždy zcela intuitivní, narazíte-li na problémy, použijte absolutní cestu.

Procházení adresářového stromu


Dosavadní ukázky (fotogalerie) počítaly s plochým procházením adresářů. Nyní upravíme fotogalerii tak, aby procházela i zanořené adresáře. Uvědomme si, že budou stačit minimální změny, většinu scriptu nebude potřeba měnit, jenom dynamicky upravíme cestu k aktuálnímu adresáře a fotogalerii doplníme o navigační prvky. Vždy se snažte psát kód tak, aby byl snadno upravitelný.

Při sestavování adresy a následné práci se soubory je důležité pamatovat na to, že je třeba k souboru sestavit kompletní cestu od aktuálního umístění nebo rootu. Webový root vyhodnocovaný prohlížečem a systémový root vyhodnocovaný mj. PHP interpretem se ale liší a je třeba je rozlišovat.

Příklad: Fotogalerie s procházením zanořených adresářů

<?php

/* ************************** aplikacni vrstva ******************************* */
$defaultDir = 'images/';
$dFilename = 'description.txt';

/* jednoduchy router */
$dir = $defaultDir;
if (isset($_GET['album']))
{
    $dir = $_GET['album'].'/';
    $dD = realpath($defaultDir);
    if (strncmp(realpath($dir), $dD, strlen($dD)) !== 0)
    {
        $dir = $defaultDir;
    }
}

/* nacteni a priprava popisku */
$description = [];
if (file_exists($dir.$dFilename))
{
    $d = file($dir.$dFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($d as $k => $line)
    {
        list($key, $value) = explode('::', $line);
        $description[$key] = $value;
    }
}

/* alba v aktualnim adresari */
$folders = glob($dir.'*', GLOB_ONLYDIR);

/* fotky */
$files = glob($dir.'small_*.jpg');

$parent = dirname($dir);

$path = function ($path) { return substr($path, strrpos($path, '/')+1); };

function htmlEscape($in)
{
    return htmlspecialchars($in, ENT_QUOTES);
};


/* ************************** prezencni vrstva ******************************* */
if (!empty($description['head']))
{
    echo '<h1>'.htmlEscape($description['head']).'</h1>';
}

// o úroveň výš
echo '<ul>';
if ($dir != $defaultDir)
{
    echo '<li><a href="?album='.htmlEscape($parent).'">Nahoru – '.htmlEscape($path($parent)).'</a></li>';
}
// podadresáře
foreach ($folders as $folder)
{
    echo '<li><a href="?album='.htmlEscape($folder).'">'.htmlEscape($path($folder)).'</a></li>';
}
echo '</ul><br>'.PHP_EOL;

// fotografie
foreach ($files as $file)
{
    $fn = str_replace('small_', '', $file);
    echo '<div>'.PHP_EOL.'<a href="'.htmlEscape($fn).'">';
    echo '<img src="'.htmlEscape($file).'">';
    echo '</a>'.PHP_EOL;
    if (isset($description[$path($fn)]))
    {
        echo '<p>'.htmlEscape($description[$path($fn)]).'</p>'.PHP_EOL;
    }
    echo '</div>'.PHP_EOL;
}
?>

<style>
div { width: 300px; height: 220px; display: inline-block; text-align: center; vertical-align: middle; }
img { max-width: 300px; max-height: 200px; }
p { margin: 0; padding: 0; }
</style>
Jednoduchý router v první části zajišťuje dynamickou cestu k adresářům a zároveň kontroluje, aby nebylo možné dostat se mimo vymezený prostor. V další části jsou načteny všechny potřebné údaje. Tím končí první, aplikační vrstva. V následující prezenční vrstvě jsou již pouze vypisovány připravené proměnné, nezapomínáme na správné escapování.

Vzdálené soubory


Dosud byla představena většina možností, jak pracovat se soubory v rámci lokálního souborového systému. Je-li povolena direktiva allow_url_fopen, což je defaultní stav a obvykle nebývá zakazován, můžeme některé souborové funkce použít pro čtení vzdálených souborů, tj. mj. běžných webových stránek. Stačí místo lokální cesty k souboru zadat absolutní cestu ke vzdálenému souboru. PHP umí takto pracovat s http i ftp protokolem.

K sestavení cesty ke vzdálené stránce s dynamickým dosazením parametrů můžeme využít http_build_query().

Příklad: načtení webové stránky

$content = file_get_contents('http://www.php.net/');
V proměnné $content je html kód stránky, ze kterého můžeme kombinací funkcí pro práci s textem a regulárních výrazů získat informace, které nás zajímají.

Tip: Stránky načítané přes http protokol projdou nejdříve zpracováním PHP. Když načtete soubor 'page.php' přes souborový systém, získáte zdrojový php kód. Když načtete 'http://vas-server.cz/page.php', získáte zdrojový html kód.

Zpracování surového html bývá nouzovou záležitostí, kdy nemáme jinou možnost. Mnohem lepší je, když jsou požadované informace nabídnuty v podobě přívětivější pro strojové zpracování, kterou může být např. XML, JSON, CSV, vhodně formátovaný textový soubor a další.

Příklad: načtení XML souboru

$xml = simplexml_load_file('http://technet.idnes.feedsportal.com/c/33324/f/565937/index.rss');
// todo: doplnit ukázku zpracování
$xml je instance třídy SimpleXMLElement. Vstupní XML dokument byl rozparsován do stromové struktury, kterou nyní můžeme snadno procházet.

Chybové stavy
Při práci se vzdálenými soubory musíme být velmi důslední při ošetřování chybových stavů. Soubor nemusí existovat (jeho umístění se mohlo bez našeho vědomí změnit), nebo třeba vzdálený server momentálně nestíhá odpovídat. Zvláště pokud pro nás nejsou získávaná data kritická, neměli bychom dopustit zamrznutí i našich stránek.
Abychom se vyrovnali s dlouhou odezvou vzdáleného serveru, musíme nastavit timeout pro vytvářené spojení. Pole s nastavením předáme funkci stream_context_create(), která vytvoří tzv. context. Ten můžeme následně přidat např. do funkce file_get_contents do parametru $context.
Tím zabráníme zamrznutí, ale musíme ještě ošetřit chybu vzniklou nedostupností stránky. file_get_contents při neúspěchu vrátí false a generuje warning. Generování chyby potlačíme @, ale nezapomeneme následně otestovat, jestli chyba nenastala.

Příklad: načtení webové stránky s ošetřením nedostupnosti

$url = 'http://www.php.net/file_get_contents';
$options = [
    'http' => [
        'timeout' => 1
    ]
];
$context = stream_context_create($options);
$content = @file_get_contents($url, false, $context);
if ($content === false)
{
    echo "Soubor neni momentálně dostupný.";
}
else
{
    echo htmlspecialchars(substr($content, 0, 5000), ENT_QUOTES);
}
Pokud chcete zobrazovat informace, které se často nemění, je vhodné stažená data cachovat (ukládat k sobě na server) a nová stahovat pouze v určitých intervalech. Zrychlí to naše vlastní stránky a je to ohleduplné k poskytovateli informací.

Příklad: načtení webové stránky s cachováním

function loadPage($url, $time = 3600, $timeout = 3, $tempDir = '')
{
    $cacheName = $tempDir.md5($url).'.html';
    $t = file_exists($cacheName) ? file_get_contents($cacheName) : '';
    $cachedTime = (int) substr($t, 0, strpos($t, PHP_EOL)+1);

    if (time()-$time > $cachedTime)
    {
        $options = [
            'http' => [
                'timeout' => $timeout
            ]
        ];
        $context = stream_context_create($options);
        $content = @file_get_contents($url, false, $context);
        if ($content !== false)
        {
            $r = file_put_contents($cacheName, time().PHP_EOL.$content);
            if ($r === false) { /* chybovy stav, nelze zapisovat */ }
            return $content;
        }
    }
    return substr($t, strpos($t, PHP_EOL)+2);
}
Takto můžeme načítat soubory pro libovolné využití. Oprávnění ale musí být nastavena tak, aby script mohl zapisovat cachované soubory. file_put_contents není nijak ošetřeno, pokud selže a bude potlačen výpis warning, script bude fungovat dál, ale bez cachování.

Příklad: načtení vzdáleného JSON souboru

$json = json_decode(loadPage($url, 20, 1));
Využíváme dříve vytvořenou funkci pro načtení libovolného souboru. Tímto způsobem můžeme zpracovávat i XML funkcí simplexml_load_string() (místo přímého načítání souboru).

Další možnosti contextu
Pomocí uvedeného contextu je možné nejenom měnit timeout, ale i nastavit mnoho dalších parametrů, např. cookies dle příkladu www.php.net/file_get_contents#example-2368 a přistupovat tak na stránky jako přihlášený uživatel.

Příklad: předání cookie

// bude doplněno

Příklad: odeslání dat metodou POST přes PHP
// bude doplněno
Detailní soupis parametrů pro http protokol je na www.php.net/manual/en/context.http.php a na www.php.net/manual/en/context.php naleznete i ostatní dostupné protokoly.

cURL knihovna
Prakticky totožných věcí, kterých lze dosáhnout pomocí nastavování contextu, lze dosáhnout použitím knihovny cURL.

Příklad: načtení vzdáleného souboru pomocí cURL

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);

$content = curl_exec($ch);
curl_close($ch);

if ($content !== false)
{
    echo $content;
}
Cílovou adresu můžeme předávat přímo v curl_init(). Nastavením CURLOPT_RETURNTRANSFER zajistíme, že curl_exec() vrátí obsah pro další práci, bez této volby by ho vypsal na výstup.

cURL má principem práce blíže k socketům, context přidávaný do klasických souborových funkcí může být snadnější na naučení. S oběma přístupy lze dosáhnout prakticky stejných výsledků.
Obě knihovny však nemusí být vždy k dispozici, funkčnost file_get_contents() závisí na nastavení allow_url_fopen, pro cURL musí být povolená cURL knihovna. Obvykle je ale dostupné obojí.

Ověření existence souboru
Pokud potřebujeme jenom ověřit existenci vzdáleného souboru, není třeba ho celý načítat (výhodné především u velkých souborů). Můžeme využít funkci get_headers() vracející jen hlavičky, cURL knihovnu s parametrem CURLOPT_NOBODY nebo zkusit soubor otevřít pomocí pomocí fopen() v režimu čtení (r) a případně získat hlavičky funkcí stream_get_meta_data($hn).

Příklad: ověření existence vzdáleného souboru

// bude doplněno

Upload a download


Http upload souborů je jedna z metod, jak posílat soubory na server. Většinou bývá využíván pro nahrávání uživatelského obsahu, zpravidla fotografií, které můžeme ihned po uploadu zpracovat.

Atomicita


Většina dosud uvedených příkladů soubory pouze četla, ale nezapisovala. Jakmile začneme do souborů i zapisovat, narazíme na problémy s atomicitou operací. Při vývoji obvykle pracujeme pouze s jednou stránkou najednou, takže si ničeho nevšimneme. O to nepříjemnější může být překvapení, až se při ostrém provozu začnou z návštěvním knihy ztrácet příspěvky.

Příklad: Chybně implementované počitadlo

$filename = 'counter.txt';
$pocet = (int) trim(file_get_contents($filename)); // krok 1
$pocet += 1; // krok 2
file_put_contents($filename, $pocet); // krok 3
Představte si, že v kroku A1 načtete obsah souboru, v kroku A2 ho nějak upravíte a v A3 zapíšete zpátky. Zamýšlená posloupnost je A1, A2 a A3, vše se zdá v pořádku. Ale na stránky může přijít zároveň druhý návštěvník, který chce provést tytéž B1, B2 a B3. Protože ale zpracování požadavků probíhá paralelně a oba uživatelé pracují se stejným souborem, může se stát, že se provede například sekvence A1, B1, A2, A3, B2 a B3. Co to znamená? Změny provedené uživatelem A se ztratily. Uživatel B načetl původní neupravenou verzi souboru a následně svými změnami přepsal změny provedené uživatelem A. Podle charakteru aplikace mohou tyto konflikty vyústit až smazáním celého obsahu souboru.

Základní možnosti zamykání souborů poskytuje funkce flock(), sama o sobě ale k řešení všech problémů nestačí, například nemůžeme zamčít dosud neexistující soubor. V tomto díle se vlastním řešením tohoto problému nebudeme detailněji zabývat. Jsou dostupné knihovny, které bezproblémovou práci se soubory umožňují, ty budeme využívat.

Jedno z velmi dobrých řešení atomických operací je součástí Nette frameworku, podrobněji popsané na Atomické operace ještě jednou. Princip řešení spočívá ve využití umělého protokolu safe vytvořeného pomocí uživatelského wraperu (můžete si vytvořit vlastní wrapper funkcí stream_wrapper_register()). Detailnější informace lze najít ve zdrojových kódech.

Příklad: Atomické operace s protokolem safe

include 'nette.php';
$handle = fopen('safe://test.txt', 'x');
// ve skutečnosti se vytvořil dočasný soubor
fwrite($handle, 'La Trine');
fclose($handle);
// a teprve teď se přejmenoval na test.txt

// můžeme soubor smazat
unlink('safe://test.txt');

// a vůbec používat všechny známé funkce
file_put_contents('safe://test.txt', $content);

$ini = parse_ini_file('safe://autoload.ini');
Takto můžeme bezpečně pracovat se soubory paralelně.

Iterátory pro procházení adresářů


Tato podkapitola předpokládá znalost OOP, čímž předbíhá další díly této učebnice. Jestli zatím OOP neznáte, nebojte se ji přeskočit. Ale pokud OOP znáte, nevyhýbejte se ji. PHP nabízí celou sadu iterátorů, které v sobě zapouzdřuji funkčnost pro procházení nejen adresářových struktur. Jejich rozšiřováním a kombinováním lze snadno dosáhnout i poměrně složitého prohledávání adresářového stromu.

Příklad: Základní použití DirectoryIteratoru

foreach (new DirectoryIterator('./images') as $fileInfo)
{
     if($fileInfo->isDot()) continue;
     echo $fileInfo->getFilename() . PHP_EOL;
 }
Tento příklad je funkčně ekvivalentní dříve uvedeným funkcím. Projde zadaný adresář a vypíše soubory (a adresáře).
Lepší možnosti poskytuje RecursiveDirectoryIterator, který prochází celý strom. Jeho použití si ukážeme v následujícím příkladu.

Příklad: Spojování iterátorů, RecursiveIteratorIterator

$dir = './images';
$pattern = '~^.+leto.+\.jpg$~i';

$directoryTree = new RecursiveDirectoryIterator($dir);
$iterator = new RecursiveIteratorIterator($directoryTree);
$files = new RegexIterator($iterator, $pattern, RecursiveRegexIterator::GET_MATCH);

foreach ($files as $item)
{
    var_dump($item);
}
Všimněte si hlavně iterátoru RecursiveIteratorIterator. Jeho účelem je umožnit snadné procházení stromové struktury jako lineárního seznamu. Budeme ho často využívat při práci se stromovými daty. Podobná implementace IteratorIterator slouží pro procházení pouze lineárních seznamů. Dokáže jakýkoliv objekt implementující Traversable rozhraní převést na iterátor.

Pro ještě složitější filtrování souborů můžeme využít RecursiveFilterIterator. Jeho použití je mírně složitější, protože musíme implementovat jeho metodou accept().

Příklad: RecursiveFilterIterator s metodou accept()

// obecny vyhledavaci iterator
class RecursiveSearchIterator extends RecursiveFilterIterator
{
     protected $filters = [];

     public function __construct(RecursiveIterator $recursiveIter)
     {
         parent::__construct($recursiveIter);
     }

     public function addFilter(callable $filter)
     {
        $this->filters[] = $filter;
     }


     public function accept()
     {
        if ($this->hasChildren())
        {
            return true;
        }

        foreach ($this->filters as $filter)
        {
            if (!$filter($this->current()))
            {
                return false; // mezi pravidly je AND
            }
        }
        return true;
     }

     public function getChildren()
     {
         $iterator = new self($this->getInnerIterator()->getChildren());
         $iterator->filters = $this->filters;
         return $iterator;
     }

 }
// filtrovaci funkce, v praxi by byly umisteny v nejake tovarnicce
// predpoklada se, ze budou doplneny dle potreby
$filterFilename = function($pattern) { return function($file) use ($pattern)
        { return (bool) preg_match($pattern, $file->getFilename()); }; };

$filterContent = function($pattern) { return function($file) use ($pattern)
        { return (bool) is_readable($file->getPathname())
            && preg_match($pattern, file_get_contents($file->getPathname())); }; };
// pouziti navrzenych struktur
$treeIterator = new RecursiveDirectoryIterator('images/');
$searchIterator = new RecursiveSearchIterator($treeIterator);

$searchIterator->addFilter($filterFilename('~\.txt|\.htm~i'));
$searchIterator->addFilter($filterContent('~best~i')); // vsimnete si poradi

foreach (new RecursiveIteratorIterator($searchIterator) as $file)
{
    echo $file->getPathname() . PHP_EOL;
}
Na závěr se ještě podívejme, jak by mohla vypadat třída předpřipravených filtrovacích funkcí.
// bude doplněno

Příklad: Řazení souborů
Iterátory samy o sobě nejsou určeny k řazení položek, můžeme je k tomu však přizpůsobit, když vnitřně implementujeme průchod strukturou, uložení do datového objektu, seřazení a vrácení iterátoru. V tomto případě sice neušetříme paměť, ale zjednodušíme si následnou práci se soubory. Berte v úvahu, že to opravdu není typické využití iterátorů a řadit velké soubory položek bude pomalé (možnost zrychlení poskytuje např. index v databázi, běžnými PHP prostředky řazení zrychlit nejde).
class SortableDirectoryIterator implements IteratorAggregate
{
    private $storage;

    public function __construct($path, $order = 1)
    {
        $this->storage = new ArrayObject();

        $files = new DirectoryIterator($path);
        foreach ($files as $file)
        {
            $this->storage->append($file->getFileInfo());
        }

        $this->storage->uasort(
            function ($a, $b) use ($order) {
                return $a->getSize() < $b->getSize() ? $order : -$order;
            }
        );
        // podobně jako u SearchIteratoru lze třídu napsat obecně
        // a řadíci funkci doplnit externě
        // $this->storage->uasort($this->sort);
    }

    public function getIterator()
    {
        return $this->storage->getIterator();
    }
}

foreach ((new SortableDirectoryIterator('./images')) as $fileInfo)
{
     if($fileInfo->isDir()) continue;

     echo $fileInfo->getFilename() . sprintf(' (size: %1.2f) kiB', $fileInfo->getSize()/1024) .
         '<br>' . PHP_EOL;
}

Iterátory jsou velmi mocný nástroj pro procházení obecných struktur, kromě adresářů, XML stromů a dalších v základu stromových struktur můžeme vytvářet iterátory i z obyčejných polí nebo traversable objektů. Získáme tak možnost používat metody jako isLast(), isOdd() apod., které se velmi hodí u mnoha činností, velmi užitečné jsou například při výpisech v šablonách.
Všem, kteří vládnou angličtinou, rozhodně doporučuji přečíst stackoverflow.com/a/12236744/2375157.

Úkoly


  1. Připravte si ve vhodném formátu textový soubor obsahující několik termínů a jejich vysvětlení. Jednotlivé položky načtěte, seřaďte podle abecedy a vypište na obrazovku.
  2. Načtěte libovolný textový soubor, spočítejte, kolikrát se v něm vyskytují jednotlivé znaky, seřaďte znaky podle četnosti a včetně četností je zapište do nového souboru. Na konec souboru doplňte datum a čas zpracování. Pokud se script spustí vícekrát, budou se nové údaje dopisovat vždy na konec souboru.
  3. Implementujte vyhledávací funkci, která prohledá obsah všech souborů v zadaném adresáři (i rekurzivně) a na obrazovku zobrazí výsledky.
  4. Vypište obsah adresáře seřazený podle data poslední změny (typicky datum uploadu) sestupně.
  5. Implementujte funkci tree(string $dir) podobnou Linuxovému příkazu tree. Tj. od zadaného adresáře projděte adresářovou strukturu a vypište ji do stromu. Výsledek zobrazte na obrazovku a zároveň uložte do souboru.*obtížné
  6. Napište funkci copyDir(string $source , string $dest), která zkopíruje neprázdný adresář. (Uvažujte jen adresáře a soubory, nemusíte řešit linky nebo zachování stejných přístupových práv.)
  7. Vyberte si nějaký váš oblíbený RSS feed nebo jiný vzdálený XML zdroj. S cachováním ho načtěte a zobrazte z něj libovolnou informaci na obrazovku (ale ne celý soubor).
  8. Znáte-li OOP a četli jste kapitolu o iterátorech, přepište příklad fotogalerie s použitím iterátorů.

Otázky


  1. Dokážete vysvětlit příčinu problémů při paralelní manipulaci se soubory?
  2. Jak se liší relativní a absolutní adresa? Je root pro PHP interpret a prohlížeč totožný? Jak se v cestě k souboru odkáže o 3 adresáře výše?


Správcem webu Péhápko.cz je Joker, mail zavináč it-joker tečka cz. Informace o autorských právech a možnostech použití obsahu viz Autorská práva
Přihlášení