Ako vytvárať a správne používať generátory v PHP

Generátory sú v PHP veľmi užitočné funkcie, ktoré umožňujú generovať a vracať veľké množstvá dát na základe požiadaviek alebo podmienok bez toho, aby zaberali príliš veľa pamäte. Mám pocit, že napriek ich praktickosti nie sú medzi PHP vývojármi veľmi obľubené. V tomto blogu som preto zhrnul, ako ich vytvárať a používať. Dúfam, že článok pomôže programátorskej komunite PHP generátory pochopiť a motivuje ju používať ich častejšie.

Na úvod niečo o poliach a iterátoroch

V každom programe sa využíva pole array – najjednoduchší zoznam hodnôt. Môžeme ho napĺňať, redukovať a prechádzať, “iterovať”, pomocou foreach. Celé pole existuje v pamäti a čím je pole väčšie, tým viac pamäte zaberá. 

Pole array ale nie je jediné, ktoré sa dá iterovať cez foreach. V PHP existuje rozhranie Iterator, ktoré vieme na iterovanie cez foreach použiť tiež. Pri iterátoch navyše vieme zapracovať už aj nejakú tú logiku. 

Napr. ak by sme chceli niekde odoslať obsah súborov v určitom priečinku, pri použití poľa array by sme načítali zoznam súborov do poľa a potom ho, cez foreach, opäť načítali a odoslali. Kód teda vyzerá takto:

<?php
$dir = '/path/to/dir/';
$listFiles = scandir($dir);
foreach ($listFiles as $fileName) {
   $file = $dir . $file;
   if (is_file(#file)) {
       $fileContent = file_get_contents($dir . $file);
       sendFileSomewhere($fileContent);
   }   
}

Pri použití rozhrania Iterator je kód o čosi jednoduchší:

<?php
 $dir = '/path/to/dir/';
 $directoryIterator = new DirectoryIterator($dir);
foreach ($directoryIterator as $fileInfo) {
   if ($fileInfo->isFile()) {
       $fileContent = file_get_contents($fileInfo->getRealPath());
       sendFileSomewhere($fileContent);
   }   
}

Ako vytvárať generátory v PHP?

Iterátory sa využívajú práve v PHP generátoroch. Sú to funkcie, ktoré umožňujú vrátenie série hodnôt z inej funkcie bez toho, aby sme museli vytvoriť celé pole hodnôt v pamäti. Na rozdiel od bežných funkcií, ktoré vracajú hodnoty pomocou return, generátory používajú kľúčové slovo yield.

Generátor môžeme vytvoriť jednoducho pomocou klauzuly function a spomínaného kľúčového slova yield. Jednoduchý príklad generátora, ktorý vráti niekoľko hodnôt:

<?php

 function generator() {
  yield "hodnota 1";
  yield "hodnota 2";
  yield "hodnota 3";
}

Tento generátor, keď je volaný, vytvorí a vráti tri rôzne hodnoty. V tomto prípade sú to „hodnota 1“, „hodnota 2“ a „hodnota 3“. Každá hodnota sa vracia pomocou kľúčového slova yield. Tento generátor môžeme použiť v cykle foreach, aby sme získali každú z hodnôt, ktoré generuje:

<?php
  foreach (generator() as $value) {
  echo $value . "<br>";
}

Tento kód vypíše „hodnota 1“, „hodnota 2“ a „hodnota 3“ na obrazovku.

Generátory v PHP však nie sú obmedzené na generovanie pevne stanovených hodnôt. Môžeme ich tiež použiť na generovanie hodnôt na základe podmienok alebo požiadaviek. 

Príklad generátora, ktorý vráti náhodné hodnoty medzi 1 a 10:

<?php
 function random_numbers() {
  while (true) {
      yield rand(1, 10);
   }
 }

Tento generátor bude vraciať náhodné čísla medzi 1 a 10, až kým sa neukončí. Pri volaní generátora budeme teda dostávať rôzne náhodné čísla.

Vnorené generátory od PHP 7

Od PHP 7 je navyše dostupné vylepšenie yield from, ktoré umožňuje ďalšiu úroveň zanorenia generátorov. Jednoducho povedané, umožňuje nám využívať vnorené generátory a zjednodušuje prácu s viacerými iterátormi.

Príklad, ako môžeme použiť yield from pre vytvorenie vnoreného generátora:

<?php
 function myGenerator() {
    yield 'foo';
    yield 'bar';
}
 function myNestedGenerator() {
    yield 'hello';
    yield from myGenerator();
    yield 'world';
}
 foreach (myNestedGenerator() as $value) {
    echo "$value ";
}

V tomto príklade sme definovali dva generátory: myGenerator() a myNestedGenerator(). Generátor myNestedGenerator() používa yield from myGenerator(), aby vrátil prvky z vnoreného generátora myGenerator() spolu s ďalšími prvkami. Využitie yield from nám umožňuje zjednodušiť kód a zjednotiť výstup z viacerých generátorov.

Použitie yield from môže byť užitočné aj pri spracovaní rekurzívnych dátových štruktúr alebo pri spracovaní stromových štruktúr, kde potrebujeme prechádzať viacero úrovní. 

Využitie yield from nám teda umožňuje pracovať s viacerými generátormi v rámci jedného generátora, čím zjednodušuje a zefektívňuje náš kód.

Získanie návratovej hodnoty

Metóda getReturn() sa používa na získanie hodnoty, ktorá bola vrátená z generátora, keď sa generátor skončí. Táto hodnota sa zvyčajne vráti pomocou return v generátore.

Použitie tejto metódy nám umožňuje získať návratovú hodnotu z generátora bez toho, aby sme museli iterovať cez všetky jeho prvky. Táto vlastnosť môže byť užitočná, keď chceme získať návratovú hodnotu generátora, ale nezaujíma nás výstupná postupnosť prvkov.

Príklad, ako môžeme použiť getReturn() pre získanie návratovej hodnoty z generátora:

<?php
function myGenerator() {
    yield 'foo';
    yield 'bar';
    return 'baz';
}
$gen = myGenerator();
foreach ($gen as $value) {
    echo "$value ";
}
echo $gen->getReturn();

V tomto príklade sme definovali generátor myGenerator(), ktorý vráti tri prvky ‚foo‘, ‚bar‘ a ‚baz‘ pomocou yield a return. Potom sme vytvorili inštanciu generátoru a pomocou foreach sme iterovali cez prvky. Nakoniec sme použili metódu getReturn() pre získanie hodnoty ‚baz‘, ktorá bola z generátora vrátená.

Metóda getReturn() by mala byť volaná až po skončení iterácie cez generátor, inak môže spôsobiť neočakávané správanie.

Použitie tejto metódy nám umožňuje získať návratovú hodnotu z generátora bez toho, aby sme museli iterovať cez všetky jeho prvky.

Ako používať generátory v PHP

Generátory v PHP môžeme použiť rôznymi spôsobmi. Ako sme už spomínali, môžeme použiť cyklus foreach na prechádzanie jednotlivých hodnôt, ktoré generátor vráti. Môžeme však tiež použiť kľúčové slovo yield v rámci funkcie, ktorá volá generátor, a týmto spôsobom generovať hodnoty na základe požiadaviek alebo podmienok.

Príklad funkcie, ktorá volá generátor na základe počtu vygenerovaných hodnôt:

<?php
function generate_numbers($count) {
  $generator = random_numbers();
  for ($i = 0; $i < $count; $i++) {
    yield $generator->current();
    $generator->next();
  }
}

Táto funkcia vytvorí nový generátor pomocou funkcie random_numbers(). Následne, pomocou cyklu for, zavolá generátor toľkokrát, koľko požadujeme, a vráti každú hodnotu, ktorú generátor vráti. Po každom zavolaní generátora voláme funkciu next(), ktorá presunie internú ukazovateľovú pozíciu generátora na ďalšiu hodnotu.

Generátor môžeme tiež použiť na generovanie veľkého množstva dát. Ak by sme napríklad chceli vygenerovať milión náhodných čísel medzi 1 a 10, mohli by sme to urobiť nasledujúcim spôsobom:

<?php
function generate_numbers($count) {
  $generator = random_numbers();
  for ($i = 0; $i < $count; $i++) {
    yield $generator->current();
    $generator->next();
  }
}
foreach (generate_numbers(1000000) as $number) {
  echo $number . "<br>";
}

Tento kód vygeneruje milión náhodných čísel medzi 1 a 10 a vypíše ich na obrazovku. Zaujímavé je, že tento kód využije len malé množstvo pamäte. Ak by sme namiesto generátora použili pole, ktoré obsahuje milión náhodných čísel, pamäťová náročnosť by bola oveľa väčšia, pretože by sme potrebovali alokovať miesto pre celé pole naraz.

Vďaka generátorom v PHP môžeme jednoducho pracovať so súbormi a efektívne načítavať veľké súbory do pamäte. Môžeme vytvoriť generátor, ktorý prechádza súbor po riadkoch a generuje každý riadok jednotlivo, a tým minimalizovať pamäťové nároky.

Napríklad môžeme vytvoriť generátor, ktorý prechádza súbor po riadkoch a vráti každý riadok ako reťazec:

<?php
function readLines($filename) {
    $file = fopen($filename, 'r');
    while (!feof($file)) {
        yield fgets($file);
    }
    fclose($file);
}

V tomto príklade sme vytvorili generátor, ktorý načíta súbor a prechádza ho po riadkoch pomocou funkcie fgets(). Každý riadok vráti ako reťazec pomocou yield. Tento prístup nám umožňuje načítať veľké súbory a pracovať s nimi jednoducho a efektívne.

Generátor tiež môžeme použiť na zápis dát do súboru. Napríklad môžeme vytvoriť generátor, ktorý prijíma pole hodnôt a zapisuje ich do súboru jeden po druhom:

<?php
function writeData($filename, $data) {
    $file = fopen($filename, 'w');
    foreach ($data as $value) {
        fwrite($file, $value . "\n");
        yield;
    }
    fclose($file);
}

V tomto príklade sme vytvorili generátor, ktorý prijíma pole hodnôt $data a postupne ich zapisuje do súboru pomocou funkcie fwrite(). Každý zápis do súboru ukončíme pomocou yield. Tento prístup nám umožňuje zapisovať veľké množstvo dát do súboru postupne a efektívne.

Využitie generátorov nám teda umožňuje jednoducho pracovať so súbormi a minimalizovať pamäťové nároky, čo je veľmi užitočné pri práci s veľkými súbormi.

Pomocou iterátorov v jazyku PHP zase môžeme prechádzať a spracovávať dáta po častiach, čím šetríme pamäťové zdroje a zvyšujeme efektivitu nášho kódu. Využitie iterátorov v kombinácii s generátormi môže byť veľmi užitočné pre spracovanie veľkých súborov alebo dátových množín.

Jeden zo spôsobov, ako vytvoriť iterátor pomocou generátoru, je vytvorenie generátora, ktorý vracia jednotlivé prvky dátovej množiny, a následné definovanie triedy iterátora, ktorá bude tieto prvky iterovať a spracovávať.

Príklad, ako vytvoriť iterátor pomocou generátora:

<?php
class MyIterator implements Iterator {
    private $data;
    public function __construct($data) {
        $this->data = $data;
    }
    public function rewind() {
        reset($this->data);
    }
    public function current() {
        return current($this->data);
    }
    public function key() {
        return key($this->data);
    }
    public function next() {
        return next($this->data);
    }
    public function valid() {
        return ($this->current() !== false);
    }
}
function myGenerator($data) {
    foreach ($data as $item) {
        yield $item;
    }
}
$data = array(1, 2, 3, 4, 5);
$generator = myGenerator($data);
$iterator = new MyIterator($generator);
foreach ($iterator as $key => $value) {
    echo "Key: $key, Value: $value\n";
}

V tomto príklade sme vytvorili triedu iterátora MyIterator, ktorá implementuje rozhranie Iterator. Táto trieda umožňuje iterovanie cez dáta a ich spracovanie. Následne sme vytvorili generátor myGenerator, ktorý vracia jednotlivé prvky z poľa. Na záver sme použili generátor na vytvorenie iterátora a iterovali sme cez všetky prvky pomocou foreach cyklu.

Využitie generátorov v kombinácii s iterátormi nám umožňuje vytvárať flexibilné a výkonné iterovateľné triedy a je veľmi užitočné pre spracovanie veľkých súborov, kde by inak mohli nastať problémy s preplnením pamäte. Navyše, využitie iterátorov nám umožňuje flexibilne a efektívne spracovať dáta po častiach, čím dosahujeme lepšie výsledky pri spracovaní veľkých dátových súborov.

Ďalšie výhody použitia generátorov

Veľmi užitočné je aj použitie metódu generátora send(), ktorá umožňuje odovzdať hodnoty späť do generátora ako yield. To zmení jednosmernú komunikáciu (z generátora na volajúceho) na obojsmerný kanál medzi nimi. 

Príklad použitia nástroja na zaznamenávanie údajov pomocou funkcie send():

<?php
 function createLogger($file)): Generator {    $f = fopen($file, 'a');
     while (true) {
         $line = yield;
         fwrite($f, $line. "\n");
     }
 }
 $log = createLogger('/path/to/log/file.log');
 $log->send("First");
 $log->send("Second");
 $log->send("Third");

Obojsmernú komunikáciu medzi volajúcim a generátorom pomocou metódy send() môžeme použiť na implementáciu kooperatívneho multitaskingu. To v podstate znamená prepínanie medzi viacerými úlohami – spustením jednej úlohy naraz a jej prepnutím s inou úlohou. Odporúčam skvelý blog o tom, ako implementovať kooperatívny multitasking pomocou co-routines pomocou generátorov PHP.

Dôležité je zdôrazniť aj to, že generátory nie sú vhodné pre každú situáciu – mali by sme používať správne nástroje pre správne úlohy. Napríklad, keď potrebujeme uložiť väčšie množstvo dát do pamäte, môže byť vhodnejšie použiť namiesto generátora pole alebo zoznam.

Takisto by som rád spomenul, že použitie generátorov môže mať vplyv na výkonnosť kódu, pretože generátor musí byť vytvorený a riadený počas behu programu. Preto je dôležité mať na pamäti, že generátory by sme mali používať s rozumom.

Zhrnutie na záver

Generátory v PHP sú veľmi užitočné pre generovanie veľkého množstva dát, ktoré nemusíme ukladať do pamäte naraz. Majú veľmi nízke pamäťové nároky a môžeme ich použiť na generovanie hodnôt na základe požiadaviek alebo podmienok. Využitie generátorov v PHP nám teda umožňuje generovať veľké množstvo dát efektívne a s nízkou pamäťovou náročnosťou.

Máš toto všetko v malíčku? Potom sa pridaj do nášho tímu!