This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!

Język PHP zawiera w sobie dużo różnych “sztuczek”, przez co nasze życie może stać się o wiele łatwiejsze, ale istnieje możliwość, że zostanie przez nas znienawidzony do końca życia. W dzisiejszym wpisie chciałbym pokazać i wyjaśnić jedną z bardzo niejasnych i bardzo brzegowych kwestii, jaką jest zachowanie zasięgu zmiennych podczas przechodzenia do innych plików [np. w przypadku ich includowania]. Zapraszam do lektury.

Fotografia: Milian Wolff, CC-BY.

TL;DR.

Nie da się zdefiniować klasy ani wewnątrz funkcji, ani wewnątrz metody. Jeśli chcesz wiedzieć dlaczego - czytaj dalej.

PHP: Definiowanie klasy wewnątrz funkcji lub metody.

Rozważmy następującą sytuację:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
class Foo
	{
	public function __construct()
		{
		}

	public function bar()
		{
		class Bar
			{
			public function __construct()
				{
				}
			}

		$bar = new Bar();
		}
	}

Co tutaj mamy? Teoretyczny programista [w tym przypadku ja] próbuje zdefiniować klasę Bar w zasięgu funkcji bar() w zakresie klasy Foo. Brzmi trochę pokrętnie, ale mam nadzieję, że wszyscy rozumiemy, jaki jest cel.

Nie ważne, czy dla podanego przypadku istnieje jakiś sensowny przypadek użycia - widziałem już tyle dziwnych pomysłów w programowaniu, że ten jeden mnie jakoś specjalnie nie dziwi. Z drugiej strony jakby się uprzeć, to na pewno coś by się znalazło [np. posiadanie prywatnych klas w zasięgu konkretnej funkcji], pomimo tego, że aktualnie ciężko byłoby mnie do tego przekonać - chyba, że ktoś wyperswadowałby mi używanie Dependency Injection Containera na rzecz czegoś JESZCZE lepszego.

Koniec rozważań - niestety tak to nie zadziała. Powód? Proszę bardzo:

Fatal error: Class declarations may not be nested in [file] on line [line]

Jedziemy dalej - spróbujmy więc tą klasę zaincludować. Nasz plik zmienia się następująco:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
class Foo
	{
	public function __construct()
		{
		}

	public function bar()
		{
		require_once('bar.php');
		$bar = new Bar();
		$bar->baz();
		}
	}

$foo = new Foo();
$foo->bar();

I plik z klasą:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
class Bar
	{
	public function __construct()
		{
		}

	public function bar()
		{
		echo __CLASS__;
		}
	}

Wynik?

Bar::baz

Działa? Działa. Tak jak chcemy? Niestety… nie. Otóż tutaj do gry wchodzi “sztuczka” interpretera PHP [manual funkcji include()]:

When a file is included, the code it contains inherits the variable scope of the line on which the include occurs. Any variables available at that line in the calling file will be available within the called file, from that point forward. However, all functions and classes defined in the included file have the global scope.
Oznacza to, że jeśli włączamy do kodu kolejny plik poprzez jedną z instrukcji (include|require)(_once)(), zasięg zmiennych zostaje przeniesiony do kolejnego pliku - np. zmiennej zadeklarowane w metodzie i potem we włączonym pliku są dostępne tylko w tej metodzie - czyli dokładnie tak, jakbyśmy wstawili dany fragment kodu w miejsce funkcji włączającej. Wyjątkiem od tej reguły są funkcje i klasy - one są deklarowane w zasięgu globalnym.

W takim przypadku następujący kod da praktycznie ten sam wynik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Foo
	{
	public function __construct()
		{
		}

	public function bar()
		{
		require_once('class_inside_bar.php');
		$bar = new Bar();
		$bar->baz();
		}
	}

$foo = new Foo();
$foo->bar();

$bar = new Bar();
$bar->baz();

Wynik:

1
2
Bar::baz
Bar::baz

Tak samo wykona się definicja klasy w funkcji:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
function bar()
	{
	class Bar
		{
		public function __construct()
			{
			}

		public function baz()
			{
			echo __METHOD__;
			}
		}
	$bar = new Bar();
	$bar->baz();
	}

bar(); // Bar::baz

$bar = new Bar();
$bar->baz(); // Bar::baz

Wariantu z includowaniem pliku klasy w funkcji chyba nie muszę już komentować. ;] Mogę więc uznać temat za wyczerpany. Jeśli coś byłoby jeszcze niejasne, zapraszam do komentowania - odpowiem na wszystkie Wasze pytania. ;]