Poprzedni rozdział: Instrukcja foreach
Następny rozdział: Inne elementy składni

Funkcje

edytuj

Funkcje są pojęciem znanym z matematyki. Przeniesione na grunt informatyki, zachowują się podobnie i mają podobne zastosowanie: reprezentują jakieś przekształcenie, jakie można wykonać na danych. Dane wejściowe, czyli patrząc na definicję - element zbioru X - wymieniamy jako argumenty funkcji, a po jej wykonaniu otrzymujemy wynik, czyli element zbioru Y. W matematyce jedną z najbardziej znanych funkcji jest sinus, który dla każdej wartości kąta "produkuje" stosunek długości odpowiednich boków w trójkącie prostokątnym zawierających ten kąt. Sinus może być jak najbardziej poprawną funkcją w PHP (i rzeczywiście, język ten udostępnia programiście funkcję sin()), ale ponieważ programy wykonują dużo więcej różnorodnych operacji, możemy spotkać również bardziej praktyczne funkcje z punktu widzenia generowania stron WWW, np. strip_tags(), która z podanego tekstu usuwa znaczniki HTML. Oprócz tego, programista może samodzielnie tworzyć własne funkcje i omówienie tego zagadnienia jest głównym celem rozdziału, który właśnie czytasz.

W programowaniu będziemy przede wszystkim chcieli, aby zdefiniować sobie zestaw funkcji, w którym zamkniemy często wykonywane operacje. Może to być np. obsługa błędów - zamiast kopiować i wklejać odpowiedzialny za nią kod w różne miejsca skryptu, opakowujemy go w funkcję, do której niezbędne dane podajemy jako argumenty i po prostu wywołujemy ją. Dobry podział skryptu na funkcje ma jeszcze jedną zaletę - jeśli w jakimś kawałku kodu znajdziemy błąd, wystarczy poprawić go tylko w jednej funkcji, a zmiana będzie widoczna wszędzie.

Znajomość funkcji to jeden z fundamentów programowania, dlatego w tym rozdziale niezbędna jest szczególna uwaga.

Tworzenie własnych funkcji

edytuj

Każda funkcja musi posiadać pewną unikalną nazwę, która pozwoli odróżnić ją od innych. Musimy także określić, jakie argumenty przyjmuje i co właściwie robi. Odpowiada za to następująca konstrukcja:

function nazwaFunkcji(argumenty)
{
   // kod funkcji
}

Od tego miejsca możemy wywoływać naszą funkcję w identyczny sposób, jak te dostępne w PHP.

<?php
function formatujTekst($tekst)
{
   echo '<font color="red">'.strtoupper($tekst).'</font>';	
}
 	
formatujTekst('to jest tekst 1');
formatujTekst('to jest tekst 2');

Stworzyliśmy tutaj funkcję formatujTekst(), dzięki której ustalimy jednolite formatowanie dla tekstów prezentowanych na stronie. Pobiera ona jeden argument: $tekst. Zauważmy, że nazwę tę piszemy ze znakiem dolara. Gdybyśmy chcieli podać więcej argumentów, oddzielamy je od siebie przecinkami. Jeżeli funkcja nie będzie używać żadnego argumentu, za nazwą pozostawiamy puste nawiasy. Abyśmy mogli z argumentów skorzystać, muszą one mieć swoje nazwy, gdyż wewnątrz funkcji stają się zwykłymi zmiennymi.

Kod funkcji jest dowolnym poprawnym kodem PHP i można w nim umieścić dowolną rzecz, z tworzeniem kolejnej funkcji włącznie. Jednak zauważmy, że nasza pierwsza funkcja nie zwraca wartości. Zamiast tego wynik wysyła od razu na ekran i próba wykonania

$zmienna = formatujTekst('to jest tekst 1');

nic by nam nie dała. Aby zwrócić cokolwiek jako wynik, musimy skorzystać z komendy return:

<?php
function formatujTekst($text)
{
   return '<font color="red">'.strtoupper($text).'</font>';	
}
 	
echo formatujTekst('to jest tekst 1').'<br/>';
echo formatujTekst('to jest tekst 2').'<br/>';

Po return podajemy wyrażenie generujące wartość do zwrócenia.

Zwróćmy uwagę na fakt, iż przy deklarowaniu argumentów nie podajemy żądanego typu danych. O to musimy zadbać sami, umieszczając na początku funkcji odpowiednie instrukcje warunkowe i w razie kłopotów zgłosić błąd. Napiszmy sobie skrypt wyświetlający zawartość katalogu. Stworzymy w nim jedną funkcję zwracającą zawartość podanego katalogu jako tablicę. Druga funkcja będzie uniwersalnym wyświetlaczem tablic. Dlaczego tak - zaraz wyjaśnimy.

<?php
function wyswietlKatalog($sciezka, $tylkoPliki = 0) // 1
{
	$dir = opendir($sciezka); // 2
	$wynik = array();
	while($f = readdir($dir)) // 3
	{
		if(is_file($sciezka.$f)) // 4
		{
			$wynik[] = $f; // 5
		}
		elseif(is_dir($sciezka.$f) && $f != '.' && $f != '..' && !$tylkoPliki) // 6
		{
			$wynik[] = $f;
		}	
	}
	closedir($dir); // 7
	
	return $wynik; // 8
} // end wyswietlKatalog();
 
function pokazListe(array $lista) // 9
{
	echo '<ul>';
	foreach($lista as $element)
	{
		echo '<li>'.$element.'</li>';		
	}
	echo '</ul>';	
} // end pokazListe();
 
pokazListe(wyswietlKatalog('./katalog1/')); // 10
echo '<br/>';
pokazListe(wyswietlKatalog('./katalog2/', true)); // 11

Opis skryptu:

  1. Oto deklaracja funkcji wyświetlania katalogów. Znak równości oraz wartość po drugim argumencie oznacza, że jest on opcjonalny. Jeżeli go nie podamy przy wywołaniu, przyjmie on wartość domyślną. Opcjonalnych argumentów może być więcej, z tym że podajemy je zawsze na końcu. W naszej funkcji opcjonalny argument określa, czy funkcja ma zwracać wszystko (domyślny stan: 0), czy jedynie pliki (stan 1).
  2. Otwieramy katalog o podanej ścieżce
  3. Pętla pobierająca kolejne elementy katalogu, dopóki istnieją.
  4. Sprawdzamy, czy zwrócony element jest plikiem. Zauważ, że do zwróconej nazwy elementu musimy dokleić ścieżkę, ponieważ funkcja is_file() jest niezależna od opendir() i nie obchodzi jej, że w takim kontekście ją wywołujemy. Jeżeli rzeczywiście mamy plik, dodajemy go do tablicy wynikowej jako kolejny element.
  5. Niepodanie indeksu oznacza: "utwórz nowy element o indeksie MAX+1".
  6. Warunek sprawdzający, czy mamy do czynienia z katalogiem, jest dość skomplikowany. Użyliśmy tu operatorów && (logiczne "oraz"), aby zagwarantować, że wszystkie muszą być spełnione, aby dodać element do listy. Mamy tu kolejno: czy element jest katalogiem, czy nie ma on nazwy "." i ".." oraz czy funkcja ma od programisty zezwolenie na pobieranie katalogów.
  7. Zamykamy katalog
  8. Zwracamy tablicę jako wynik
  9. A oto mała niespodzianka. Począwszy od PHP 5 można definiować typy argumentów obiektowych, a od PHP 5.1 - tablic, które również są typem złożonym. Robimy to właśnie w taki sposób. Jeśli próbowalibyśmy wysłać tutaj np. liczbę, PHP zgłosiłby błąd.
  10. Wywołanie funkcji z jednym argumentem i przekierowanie wyniku do funkcji wyświetlającej listę.
  11. Ponowne wywołanie, lecz tym razem żądamy wyłącznie plików.

W ten sposób poznaliśmy już niemal wszystko, co dotyczy definiowania argumentów. Pozostała jeszcze jedna rzecz, a mianowicie pobieranie ich zupełnie nieokreślonej liczby. Uruchom taki oto skrypt:

<?php 
function funkcja($a)
{
	echo $a;
}
 
funkcja(1, 2, 3, 4, 5);

Nasza funkcja pobiera tylko jeden argument, lecz my podajemy mu pięć. Mogłoby się wydawać, że spowodujemy tym samym błąd, jednak tak się nie stanie. PHP nadmiarowych argumentów nie ignoruje. Choć nie zadeklarowaliśmy żadnego z nich podczas tworzenia funkcji, istnieje pewien sposób, aby je wydostać. Jest to funkcja func_get_args() zwracająca tablicę z wartościami wszystkich argumentów, które przekazaliśmy do funkcji.

function funkcja()
{
	$argumenty = func_get_args();
	echo '<ul>';
	foreach($argumenty as $id => $wartosc)
	{
		echo '<li>'.$id.' - '.$wartosc.'</li>';
	}
	echo '</ul>';
}
 
funkcja(1, 2, 3, 4, 5);

Istnieje także func_get_arg(numer) pobierająca wartość konkretnego argumentu. Obie te funkcje operują bezpośrednio na funkcji, dlatego PHP nakłada kilka ograniczeń na ich stosowanie. Najlepiej jest wywołać je na samym początku tworzonej funkcji, aby uniknąć kłopotów.

Widzialność zmiennych

edytuj

Napiszmy taki skrypt:

<?php
$zmienna = 'To jest zmienna';
 
function funkcja()
{
	echo $zmienna.'<br/>';
}
 
funkcja();
echo $zmienna.'<br/>';

Próbuje on wyświetlić dwa razy wartość tej samej zmiennej: z wnętrza funkcji oraz bezpośrednio w skrypcie. Po uruchomieniu okazuje się, że tylko bezpośrednie wyświetlenie podało nam prawidłowy wynik. echo wewnątrz funkcji nie pokazało żadnej wartości. Dlaczego? Zmienna przecież istnieje. I owszem, lecz tylko w tej części skryptu, w której została utworzona. PHP ma zaimplementowaną tzw. widzialność zmiennych - dla każdej funkcji tworzony jest osobny stos, niezależny od drugiego. Jeżeli więc utworzymy zmienną bezpośrednio w skrypcie, nie będzie ona istnieć w żadnej z naszych funkcji, gdyż te mają własne stosy. Ma to wyeliminować konflikty nazewnictwa.

Istnieje jednak sposób na powiedzenie PHP, że używana w funkcji zmienna jest już stworzona w stosie głównym skryptu. Po lekkiej modyfikacji skryptu otrzymujemy:

<?php
$zmienna = 'To jest zmienna';
 
function funkcja()
{
	global $zmienna;
	echo $zmienna.'<br/>';
}

funkcja();
echo $zmienna.'<br/>';

Słowo kluczowe global informuje PHP, że wymienione po nim zmienne mają zostać zaimportowane ze stosu głównego. Działa ono nawet wtedy, jeśli zmienna o danej nazwie nie istnieje, dlatego korzystanie z funkcji używających global musi być bardzo uważne.

Static

edytuj

Inną przydatną rzeczą jest przenoszenie niektórych zmiennych między wywołaniami tej samej funkcji. Dzięki temu nie musimy zapamiętywać ich wartości w globalnych tablicach, narażając się na konflikty nazewnictwa. Aby tego dokonać, wystarczy zadeklarować wybrane zmienne jako static, a ich wartość zostanie zapamiętana do następnego wywołania.

<?php
function koloruj()
{
   static $i = 0;
 		
   $i++;

   if($i % 2 == 0)
   {
      return '#ffffff';
   }
   return '#cccccc';	
} // end koloruj();
 	
echo '<table width="30%">';
for($x = 0; $x < 10; $x++)
{
   echo '<tr><td bgcolor="'.koloruj().'">'.$x.'</td></tr>';	
}
echo '</table>';

Powyższy przykład koloruje naprzemiennie wiersze w tablicy. Sztuczka ta nie wymaga finezji: po prostu zwiększamy licznik i sprawdzamy, czy dzieli się bez reszty przez dwa. Jeśli tak, wstawiamy jeden kolor, jeśli nie - drugi. Z pomocą instrukcji switch można rozszerzyć algorytm na więcej kolorów.

Tutaj kolor jest zwracany przez odpowiednią funkcję. Zapamiętuje ona sobie stan wewnętrznego iteratora $i między kolejnymi wywołaniami przy pomocy słowa kluczowego static. Gdybyś usunął tę linijkę, funkcja cały czas zwracałaby ten sam kolor, gdyż zmienna tworzona byłaby w kółko od nowa z domyślną wartością 0.

Rekurencja

edytuj

Rekurencja w programowaniu oznacza odwoływanie się funkcji do samej siebie. Jest użyteczna, w niektórych sytuacjach wręcz niezbędna, lecz pochłania znacznie więcej zasobów, dlatego należy korzystać z niej ostrożnie.

Za pomocą rekurencji możemy wyświetlić w PHP drzewo katalogów:

<?php
function wyswietlKatalog($sciezka)
{
	$dir = opendir($sciezka);
	echo '<ul>';
	while($f = readdir($dir))
	{
		if(is_dir($sciezka.$f) && $f != '.' && $f != '..')
		{
			echo '<li>'.$f;
			wyswietlKatalog($sciezka.$f.'/'); // 1
			echo '</li>';
		}
	}
	echo '</ul>';
	closedir($dir);
} // end wyswietlKatalog();
 
wyswietlKatalog('../../');

Funkcja wyswietlKatalog() w przypadku napotkania katalogu w aktualnie sprawdzanej ścieżce, wywołuje samą siebie (1), z doklejoną do dotychczasowej ścieżki nazwą tego katalogu. W ten sposób możemy pobrać całe drzewo, niemniej w przypadku rozbudowanych struktur może trwać to nawet kilkanaście sekund!

Sprawdźmy następujący skrypt:

<?php
function wypisz($tekst, $ile)
{
   echo $ile.': '.$tekst.'<br/>';
   if($ile > 0)
   {
      wypisz($tekst, $ile - 1);
   }
} // end wypisz();

wypisz('Witaj', 30);

Demonstruje on pewną właściwość rekurencji - możemy nią zastąpić pętle, odpalając naszą funkcję rekurencyjnie określoną liczbę razy. Jako licznik służy nam wartość argumentu $ile. Gdy jest ona większa od zera, funkcja wywołuje samą siebie, zmniejszając go o 1, aż dojdziemy do zera. Nie ma w tym nic dziwnego. Takie rozumienie funkcji jest podstawą tzw. programowania funkcyjnego charakterystycznego dla takich języków programowania, jak Ocaml czy Erlang. Zamiast pętli, tworzymy funkcje wywoływane rekurencyjnie.

Brzmi to interesująco, lecz w PHP natrafia na bardzo ważną przeszkodę. Zmieńmy powyższy kod tak, aby wypisał nasz tekst 200 razy i wykonajmy go. Niespodzianka! Po dojściu do mniej więcej połowy otrzymaliśmy błąd:

Fatal error: Maximum function nesting level of '100' reached, aborting!

Gdy parser wywołuje nową funkcję, musi zapamiętać gdzieś wartości wszystkich zmiennych oraz ogólnie cały stan dotychczasowej. Odkłada go na tzw. stos i po zakończeniu wewnętrznej funkcji, pobiera go stamtąd z powrotem. Stos ten ma jednak ograniczoną głębokość (w PHP wynoszącą 100), dlatego nie możemy w sposób zagnieżdżony wywoływać funkcji w nieskończoność. Doprowadziłoby to bowiem do szybkiego wyczerpania się pamięci.

Wiemy, że każdą pętlę da się zapisać w postaci rekurencyjnej, ale zależność ta działa też w drugą stronę. Każdą rekurencję da się zapisać przy pomocy zwykłych pętli oraz instrukcji warunkowych, choć w pewnych przypadkach może to być zadanie bardzo trudne. Oto prosta implementacja rekurencyjna funkcji silnia, która w matematyce zdefiniowana jest następująco:  

<?php
function silnia($n)
{
  if($n > 0)
  {
     return $n * silnia($n - 1);
  }
  return 1;
} // end silnia();

echo silnia(6);

Jej wersja iteracyjna, czyli zapisana przy pomocy pętli, jest w PHP dużo wydajniejsza:

<?php
function silnia($n)
{
   $wynik = 1;
   while($n > 0)
   {
      $wynik *= $n--;
   }
   return $wynik;
} // end silnia();

echo silnia(6);

Użyteczne funkcje

edytuj

PHP dysponuje kilkoma funkcjami do zarządzania funkcjami. Brzmi to może dość śmiesznie, lecz w praktyce bywa bardzo przydatne.

Na początek zastanówmy się, kiedy PHP sprawdza, że funkcja nie istnieje. Okazuje się, że nie dzieje się to w momencie kompilacji, lecz wykonywania skryptu. Ma to swoje uzasadnienie przy konstruowaniu modułowych skryptów (zajmiemy się nimi w następnym rozdziale). Pierwszy plik PHP odwołuje się do funkcji zdefiniowanych w drugim, lecz ten z kolei ładowany jest później. Gdyby z powodu nieistnienia jednej z nich skrypt byłby przerywany w momencie kompilacji, skrypt nie miałby żadnych szans na działanie. Ponadto nie dałoby się pracować ze skryptami korzystającymi z rozszerzeń, których na serwerze nie ma. Ma to sens, przecież instrukcją warunkową możemy zdefiniować alternatywny kod dla tych uboższych serwerów.

PHP ułatwia nam zadanie jeszcze bardziej. Za pomocą function_exists() możemy sprawdzić, czy podana przez nas funkcja istnieje. Narzędziem tym można sondować zarówno nasze własne, jak i definiowane przez rozszerzenia funkcje. W poniższym przykładzie wykorzystamy to do sprawdzenia, czy serwer posiada obsługę protokołu IMAP:

<?php
if(function_exists('imap_open'))
{
   echo 'IMAP dostępny';
}
else
{
   echo 'IMAP niedostępny';
}

Innym sposobem sprawdzenia, czy rozszerzenie jest załadowane, jest skorzystanie z funkcji extension_loaded(), która ma tę przewagę, że działa także z rozszerzeniami obiektowymi, w których zwykłych funkcji nie ma:

<?php
if(extension_loaded('imap'))
{
   echo 'IMAP dostępny';
}
else
{
   echo 'IMAP niedostępny';
}

Jak dobrze korzystać z funkcji?

edytuj

Funkcje to jedno z podstawowych narzędzi programisty. Omówiliśmy techniczne aspekty ich działania w języku PHP, lecz nie poruszaliśmy dotąd tematu, jak je tworzyć, aby faktycznie były dla nas użyteczne. Istnieje kilka zasad, których przestrzeganie daje nam pewność, że nie natkniemy się gdzieś na problemy z utrzymaniem projektu.

  1. Funkcje powinny realizować jedno, konkretne zadanie. Unikamy tworzenia funkcji w stylu mydło i powidło. Jeśli nie wiemy bądź nie rozumiemy, co dana funkcja tak naprawdę pozwoli nam osiągnąć, przerywamy pisanie kodu i wracamy nad kartkę papieru. Oczywiście nic nie stoi na przeszkodzie, by zadanie było bardzo złożone (np. obsługa formatowania BBCode); dopóki jest to jedno zadanie, jesteśmy w domu.
  2. Jeśli kod danej funkcji staje się bardzo długi, możemy rozważyć jej rozbicie na podproblemy, które zapiszemy w mniejszych funkcjach. Powinniśmy to zrobić zwłaszcza wtedy, gdy dany problem pojawia się wielokrotnie w różnych postaciach. Próbujemy wtedy wyciągnąć wspólny mianownik, a różnice obsłużyć poprzez konfigurację argumentami.
  3. Funkcje nie powinny być zbyt długie.

Oczywiście zalecenia te stanowią punkt odniesienia, a nie prawo, za którego nieprzestrzeganie czeka nas lincz. Wielu początkujących programistów zadaje pytania, kiedy pisać tak, a kiedy inaczej. Odpowiedź jest bardzo prosta: nie ma jednej, uniwersalnej reguły, która mówi, że w przypadku danej funkcji mamy ją rozbić, a w przypadku innej - nie (zauważmy, że reguła taka musiałaby być albo bardzo błyskotliwa, albo obejmować nieskończoną liczbę przypadków). Kluczem jest zwyczajne myślenie. Dobry programista myśli podczas pisania kodu i każdy jego krok ma uzasadnienie. Punkt odniesienia jest pomocą, wokół którego się obraca, ale jeśli widać, że rozbijanie jakiegoś skomplikowanego i długiego algorytmu, który stanowi jedną całość, będzie niepotrzebną komplikacją, nikt poważny nie będzie tego robił. Oczywiście zdarzają się pomyłki; nie wszystkie uzasadnienia okazują się poprawne, ale duże projekty ulepszane są cały czas. Jeśli coś się nie sprawdziło, jest po prostu przerabiane. Sztuka polega na tym, że jeśli już się pomylimy, pomyłka nie powinna być poważna.

Aby zrozumieć inny aspekt tworzenia dobrej funkcji, porozmawiajmy nieco o tym, co one robią. Być może niektórzy uważni czytelnicy już zauważyli, że w dotychczasowych przykładach pojawiały się zarówno funkcje, które np. wypisywały coś na ekran, oraz takie, które wyłącznie obrabiały zewnętrzne argumenty i zwracały wynik. Okazuje się, że taki podział ma bardzo duże znaczenie praktyczne i teoretyczne, zwłaszcza przy dowodzeniu poprawności programu. Każda operacja języka programowania może mieć tzw. skutki uboczne. Jest to dowolny efekt jej działania, który zmienia stan programu. Przykładowo operacja $a + $b nie ma skutków ubocznych. Możemy ją wywołać 1000 razy, ale dopóki nie zaczniemy czegoś robić z wynikiem, nie zmieni to ani o jotę działania programu. Z drugiej strony operacja $a = 5 ma już skutek uboczny - od tego momentu zmienna $a ma już inną wartość, co potencjalnie może wpłynąć na działanie dalszej części kodu. Pomimo swojej nazwy, skutki uboczne często są właśnie spodziewanymi rezultatami jakiejś operacji. Nie należy ich rozumieć dosłownie, lecz właśnie jako taką zmianę stanu programu, która może wpłynąć na dalszy kod.

Także i funkcje możemy sklasyfikować względem tego czy mają one jakieś skutki uboczne czy nie. Spoglądając na napisany wyżej przykład funkcji silnia() łatwo stwierdzimy, że nie ma ona żadnych skutków ubocznych, ponieważ jedyne, co robi, to przetwarza podany jej argument i zwraca wynik, nie zajmując się tym, jak będzie on wykorzystany. Poza tym nie wypisujemy w niej nic na ekran, ani nie korzystamy z żadnych innych zewnętrznych źródeł danych. Przykład pobierający zawartość katalogu ma już skutek uboczny; w wyniku jej wykonania do przeglądarki wysyłany jest tekst z listą katalogów. Wywołując ją dwukrotnie, użytkownik dostanie taką listę dwa razy.

Jeśli chcemy napisać dobrą funkcję, po prostu musimy znać wszystkie jej skutki uboczne i wiedzieć, czy faktycznie są one dla nas pożądane czy nie. W tym drugim przypadku powinniśmy funkcję przepisać tak, aby ich nie zawierała. Rozpatrzmy funkcję dodającą jakiś kod HTML do podanego tekstu:

<?php
function kolorujTekst($tekst)
{
   echo '<font color="red">'.$tekst.'</font>';	
}

Jej skutkiem ubocznym jest wypisanie tekstu bezpośrednio na ekran. Oprócz tego mamy też drugą funkcję:

<?php
function pogrubTekst($tekst)
{
   echo '<strong>'.$tekst.'</strong>';	
}

Jeśli będziemy chcieli tworzyć złożenie w stylu kolorujTekst(pogrubTekst('tekst')), które wyświetli pogrubiony i pokolorowany tekst, taki skutek uboczny jest nie do przyjęcia. Zamiast echo powinniśmy użyć return tak, aby funkcja zwracała wynik, dzięki czemu jej użytkownik może zdecydować w miejscu wywołania, co tak naprawdę chce z nim zrobić. Przecież takiego kodu HTML nie musimy wcale wysyłać do przeglądarki, lecz np. zapisać do pliku. Nietrudno zauważyć, że dopiero wyeliminowanie skutku ubocznego zwiększyło nasze możliwości wykorzystania naszych funkcji do tego celu.