deftcode
English
← Powrót do bloga

Podstawy PHP w pigułce: Zmienne oraz stałe

Przeczytaj po angielsku
phpkurs phppodstawy

Zanim zaczniemy

Zakładam, że programujesz już w czymś innym — w JavaScripcie, Pythonie, C#, Javie, czy też może Go. Wiesz czym jest zmienna, funkcja, pętla czy też klasa i nie zamierzam w mojej serii tłumaczyć takich zagadnień. Pokażę Ci natomiast, jak elementy powszechnie występujące w innych językach programowania wyglądają i działają w PHP — oraz, przede wszystkim, gdzie PHP zachowuje się inaczej, niż podpowiada intuicja czy też doświadczenie z innego języka programowania.

Cały kurs dotyczy PHP w wersjach 8.0–8.3. W każdym przykładzie zakładam, że korzystamy z declare(strict_types=1); na początku pliku, nawet jeśli jawnie tego nie napisałem. Za chwilę wyjaśnię, dlaczego ta deklaracja nie powinna być opcjonalna w żadnym Twoim pliku.

W tej części omówimy sobie budowę pliku PHP, tryb ścisłego typowania, trzy funkcje przydatne do „podglądania” wartości zmiennych, zmienne, referencje oraz stałe. Czyli wszystko, co potrzebne, żeby ruszyć dalej z nawet najprostszym kodem PHP.


Plik PHP

Każdy plik PHP zaczyna się od znacznika otwierającego. Bez niego interpreter potraktuje zawartość jako zwykły tekst i w takiej też postaci wyśle do przeglądarki nic w niej nie zmieniając.

index.php
<?php // znacznik otwierający
declare(strict_types=1);
echo 'Hello';
/*
Dzięki znacznikowi w przeglądarce zobaczymy "Hello" a bez niego zobaczylibyśmy
declare(strict_types=1);
echo 'Hello';
*/

Znacznik zamykający: warty pominięcia

Znacznik ?> jest opcjonalny i w plikach zawierających wyłącznie PHP należy go pominąć. To nie jest kwestia gustu — to zabezpieczenie przed konkretnym, wyjątkowo irytującym błędem.

config.php
<?php
declare(strict_types=1);
return ['db' => 'mysql'];
?>
← ta pusta linia zostanie wysłana do przeglądarki

Wszystko, co znajdzie się po ?> — spacja, tabulator, znak nowej linii — PHP wysyła do przeglądarki jako treść strony. A razem z pierwszym znakiem treści leci komplet nagłówków HTTP, bo PHP dopisuje je automatycznie. Gdy potem spróbujesz którykolwiek z nich ustawić — header(), setcookie(), session_start() — dostaniesz błąd headers already sent.

Pomijanie ?> to jeden z tych nawyków, które warto wyrobić sobie na początku swojej przygody z PHP. Standard PSR-12 wprost tego wymaga.

Rodzaje znaczników

Znacznik Status
<?php ... ?> Standardowy, zawsze dostępny
<?= $x ?> Skrócony echo — dostępny zawsze od 5.4, bezpieczny w szablonach HTML
<? ... ?> Krótki znacznik — zależny od dyrektywy short_open_tag w php.ini, unikaj
<% %>, <script language="php"> Usunięte w PHP 7

Znacznika <?= używaj tylko w szablonach, tam gdzie mieszasz HTML z PHP:

views/user.php
<h1><?= htmlspecialchars($user->name) ?></h1>

Komentarze i średniki

// jednolinijkowy
# też jednolinijkowy (styl shellowy — działa, ale w PHP prawie nikt go nie używa)
/* wielolinijkowy
komentarz */
/** DocBlock — czyta go IDE, generatory dokumentacji i refleksja (o niej w dalszych częściach) */

Każda instrukcja w kodzie PHP kończy się średnikiem. Jedyny wyjątek to ostatnia instrukcja przed ?> — ale skoro ?> pomijamy, wyjątek ten co do zasady nie powinien występować.


declare(strict_types=1) — najważniejsza linijka w pliku

Jeśli przychodzisz z języka typowanego statycznie, to jest miejsce, w którym PHP najbardziej Cię zaskoczy. Deklaracje typów w PHP wyglądają znajomo:

function addItems(int $count): int
{
return $count;
}

Wygląda jak kontrakt. Domyślnie nim nie jest. Bez trybu ścisłego PHP nie odrzuca złego typu — po cichu go konwertuje.

// bez declare(strict_types=1)
addItems('5'); // int(5) — string zamieniony na liczbę
addItems(5.9); // int(5) — UCIĘTE, bez ostrzeżenia
addItems(true); // int(1)
addItems('abc'); // TypeError — tego już nie da się skonwertować

Zwróć uwagę na drugą linijkę. 5.9 staje się 5. Bez ostrzeżenia, bez notice’a, bez śladu w logach. Jeżeli tą liczbą była kwota, rabat albo waga przesyłki — właśnie straciłeś dane i dowiesz się o tym za trzy miesiące z reklamacji.

Jedna linijka na początku pliku zamienia te konwersje w błędy:

src/Cart.php
<?php
declare(strict_types=1);
// teraz każde z tych wywołań rzuca TypeError
addItems('5');
addItems(5.9);
addItems(true);
Konwersja Tryb domyślny strict_types=1
'5' → int Rzutowane na 5 TypeError
5.9 → int Rzutowane na 5 — cicha utrata danych TypeError
true → int Rzutowane na 1 TypeError
5 → float Poszerzone do 5.0 Poszerzone do 5.0 — zawsze dozwolone
Obejmuje parametry, typy zwracane, przypisania do typowanych właściwości to samo

Poszerzenie int → float jest jedynym wyjątkiem i działa również w trybie ścisłym. Jest bezstratne, więc nie ma powodu, żeby go blokować.

Trzy rzeczy, o których musisz pamiętać

Po pierwsze: to ustawienie dotyczy pliku, nie projektu. Nie ma globalnego przełącznika. Nie dziedziczy się przez require. Każdy nowy plik zaczynasz od tej linijki — bez wyjątków.

Po drugie: musi być pierwszą instrukcją w pliku. Przed nią może stać tylko <?php i ewentualnie deklaracja namespace w formie blokowej. Cokolwiek innego to błąd krytyczny.

Po trzecie — i to jest naprawdę nieoczywiste — liczy się plik, w którym napisano wywołanie, nie ten, w którym zadeklarowano funkcję.

vendor/legacy/Calculator.php
<?php
// biblioteka bez trybu ścisłego
function calculate(int $value): int
{
return $value * 2;
}

A teraz plik, który tę funkcję wywołuje:

src/Order.php
<?php
declare(strict_types=1);
require 'vendor/legacy/Calculator.php';
calculate('10'); // TypeError — mimo że biblioteka trybu ścisłego nie ma

To działa na Twoją korzyść: włączając tryb ścisły u siebie, chronisz się także przed własnymi błędami w wywołaniach kodu zewnętrznego. Odwrotnie to nie działa — biblioteka z trybem ścisłym nie wymusi niczego na Twoim pliku bez niego.

Tryb ścisły obowiązuje również funkcje wbudowane. strlen(123) przejdzie domyślnie, a pod strict_types=1 rzuci TypeError.


Wypisywanie i podglądanie wartości

echo i print

echo 'a', 'b'; // przyjmuje WIELE argumentów, nic nie zwraca
print 'a'; // zwraca int(1), więc da się jej użyć w wyrażeniu

W praktyce używaj echo. print ma sens tylko wtedy, gdy potrzebujesz wartości zwracanej — na przykład w wyrażeniu warunkowym — co jest niezwykle rzadkim scenariuszem.

Żadne z nich nie nadaje się do sprawdzania zawartości zmiennych ani typu przypisanych do nich danych:

$arr = ['a' => 1];
echo $arr; // "Array" + warning "Array to string conversion"

Trzy funkcje do podglądania wartości

$v = ['a' => 1, 'b' => [2]];
print_r($v); // czytelnie dla człowieka, BEZ typów
// Array
// (
// [a] => 1
// [b] => Array
// (
// [0] => 2
// )
// )
var_dump($v); // typ + wartość + długość stringa + liczność tablicy
// array(2) {
// ["a"]=>
// int(1)
// ["b"]=>
// array(1) {
// [0]=>
// int(2)
// }
// }
var_export($v); // poprawny KOD PHP, który można wkleić z powrotem do pliku
// array (
// 'a' => 1,
// 'b' =>
// array (
// 0 => 2,
// ),
// )

Na tablicach różnica wygląda kosmetycznie. Na wartościach skalarnych robi się dramatyczna — tylko var_dump rozróżnia typy:

Wartość print_r var_dump var_export
'hi' hi string(2) "hi" 'hi'
1.0 1 float(1) 1.0
null (nic) NULL NULL
true 1 bool(true) true
false (nic) bool(false) false

print_r(false) i print_r(null) nie wypisują niczego. W tym przypadku pusty ekran nie oznacza, że coś nie zadziałało — oznacza, że wartość zmiennej jest fałszywa. Fakt ten potrafi niejednego początkującego przysporzyć o ból głowy.

Kiedy czego używać:

Pokazuje typy Zwraca kod PHP Potrafi zwrócić wynik zamiast go wypisać
print_r ❌ ❌ print_r($v, true)
var_dump ✅ ❌ ❌ (potrzebne buforowanie wyjścia)
var_export częściowo ✅ var_export($v, true)

Ostatnia kolumna mówi, czy funkcja potrafi zwrócić podgląd jako zwykły string, zamiast od razu wypisać go na ekran. Przydaje się, gdy chcesz zapisać go do logu, a nie pokazać użytkownikowi.

Praktyczna reguła: var_dump() gdy debugujesz wartość, której nie ufasz, var_export() gdy chcesz zamienić wartość w fixture do testu, print_r() gdy chcesz tylko rzucić okiem na kształt tablicy.


Zmienne

$name = 'Paweł'; // nazwy zaczynają się od $, są WRAŻLIWE na wielkość liter
$Name = 'ktoś inny'; // to inna zmienna

Zmiennych nie deklaruje się osobno — deklaracja jest w PHP nierozłączna z przypisaniem wartości. Pierwsze przypisanie tworzy zmienną i nie ma tutaj odpowiednika let foo; z JavaScriptu, które tworzy niezdefiniowaną zmienna do której potem możemy przypisać wartość.

Nazwy zmiennych

Po znaku $ nazwa musi zaczynać się od litery albo podkreślenia, a dalej mogą iść litery, cyfry i podkreślenia. Cyfra na pierwszym miejscu to błąd składni.

$name; $_internal; $user2; // ✅
$2fast; // ❌ błąd składni

Zarezerwowana jest dokładnie jedna nazwa: $this. Wewnątrz klasy wskazuje bieżący obiekt, a próba przypisania do niej czegokolwiek to błąd krytyczny.

Zmienne zmiennych

PHP pozwala użyć wartości jednej zmiennej jako nazwy drugiej:

$name = 'Paweł';
$varName = 'name';
echo $$varName; // 'Paweł' — to samo co $name
$$varName = 'nowe'; // ustawia $name = 'nowe'

W interpolacji stringów trzeba to jednoznacznie ograniczyć klamrami:

echo "{$$varName}"; // poprawnie
echo "${$varName}"; // przestarzałe od 8.2, usunięte w PHP 9

Wspominam o tym, bo oba te zapisy spotkasz w starym kodzie — a nie dlatego, że masz ich używać. Zmienne zmiennych łamią analizę statyczną, autouzupełnianie i refaktoryzację: ani IDE, ani żadne narzędzie nie ma jak zgadnąć, jaka nazwa powstanie dopiero w czasie działania programu.

Jeśli kusi Cię ta konstrukcja, prawie zawsze chodzi o dane, które lepiej trzymać w tablicy:

$values = ['name' => 'Paweł'];
echo $values['name']; // zamiast echo $$varName
$values['name'] = 'nowe'; // zamiast $$varName = 'nowe'

Zysk jest konkretny: nazwy kluczy widać w kodzie, więc da się je wyszukać, podmienić i sprawdzić.


Referencje

Domyślnie PHP przypisuje przez wartość. Wyjątkiem są obiekty, ale to temat na część ósmą — na razie zostańmy przy skalarach (czyli pojedynczych wartościach: liczbach, tekstach, wartościach logicznych).

$a = 1;
$b = $a; // kopia WARTOŚCI — $b dostaje własne miejsce w pamięci
$c = &$a; // przypisanie REFERENCJI — $c wskazuje na ten sam adres w pamięci co $a
$a = 2;
var_dump($b); // int(1) — kopia żyje własnym życiem
var_dump($c); // int(2) — ten sam adres co $a
Diagram: $b ma własne miejsce w pamięci z kopią wartości, a $c wskazuje na ten sam adres co $a
Przypisanie kopiuje wartość. Operator & sprawia, że dwie nazwy wskazują na ten sam adres w pamięci.

unset() na takiej zmiennej odłącza ją od tego adresu, ale nie usuwa samej wartości z pamięci — dopóki wskazuje na nią jakakolwiek inna zmienna, wartość żyje dalej:

unset($c); // $c przestaje istnieć, ale $a nadal wskazuje na ten adres i ma wartość 2

Referencja w PHP to nie to samo co wskaźnik z C. Nie jest wartością — nie policzysz na niej niczego, nie zapiszesz jej do tablicy „na potem” i nie zrobisz referencji do referencji. To po prostu druga nazwa dla tego samego adresu w pamięci i na tym koniec. W praktyce potrzebujesz jej rzadziej, niż się wydaje, a najłatwiej potknąć się o nią w pętli foreach — wrócimy do tego w części czwartej.


Stałe

PHP ma dwa sposoby definiowania stałych i nie są one wymienne.

define('APP_NAME', 'deftcode'); // wywołanie funkcji — działa w czasie wykonania
const APP_VERSION = '1.0'; // rozstrzygane w czasie kompilacji

Różnica sprowadza się do momentu, w którym stała powstaje. const przyjmuje wyrażenie stałe i nie może być warunkowa ani wyliczona w locie. define() to zwykłe wywołanie funkcji, więc można je opakować w if albo oprzeć na zmiennej:

if ($env === 'dev') {
define('DEBUG', true); // ✅ działa
}
if ($env === 'dev') {
const DEBUG = true; // ❌ błąd — const nie może być warunkowa
}
define() const
Przestrzeń nazw ignoruje bieżącą — stała należy do globalnej przestrzeni nazw trafia do bieżącej przestrzeni nazw
Definicja warunkowa ✅ ❌
Szybkość wolniejsza (czas wykonania) szybsza (czas kompilacji)
Wewnątrz klasy ❌ ✅
Opcja ignorowania wielkości liter usunięta w PHP 8 nie dotyczy

Pierwszy wiersz wymaga słowa wyjaśnienia. Przestrzeń nazw to przedrostek doklejany do nazw z danego pliku — mniej więcej to, czym dla plików jest katalog. Pozwala mieć w projekcie dwie klasy czy stałe o tej samej nazwie. Deklaruje się ją na górze pliku i od tej chwili obowiązuje wszystko, co niżej:

src/config.php
namespace App;
const VERSION = '1.0'; // powstaje App\VERSION
define('NAME', 'deftcode'); // powstaje NAME — w globalnej przestrzeni nazw
echo VERSION; // '1.0' — PHP znajduje App\VERSION
echo NAME; // 'deftcode' — App\NAME nie istnieje, więc PHP sięga do globalnej

Różnica robi się widoczna dopiero w innym pliku, bo decyduje o tym, jak stałą trzeba zaimportować:

src/Order.php
namespace App\Model;
use const App\VERSION;
echo VERSION; // '1.0' — działa dzięki importowi powyżej
echo \App\VERSION; // '1.0' — to samo, tylko pełną ścieżką
echo NAME; // 'deftcode' — globalna, więc żaden import nie jest potrzebny

Odczyt stałej PHP zaczyna od bieżącej przestrzeni nazw, a gdy jej tam nie znajdzie, sprawdza globalną — dlatego NAME działa wszędzie bez importu. Wygoda ma jednak cenę: define() tworzy nazwę widoczną w całym projekcie, więc dwie biblioteki chcące tej samej nazwy wejdą sobie w drogę. const zostaje w swojej przestrzeni i wędruje razem z resztą kodu.

Tablice jako stałe są dozwolone w obu formach:

const ROLES = ['admin', 'editor']; // od PHP 5.6
define('LIMITS', ['api' => 100]); // od PHP 7.0

I dwie funkcje pomocnicze, które przydają się przy konfiguracji:

defined('DEBUG'); // bool — czy stała istnieje
constant('APP_NAME'); // odczyt po nazwie podanej jako string

Praktycznie: w kodzie aplikacyjnym używaj const (wewnątrz klas i przestrzeni nazw), a define() zostaw dla plików bootstrapowych, gdzie coś naprawdę zależy od środowiska.

Stałe magiczne

To nie są prawdziwe stałe — interpreter podmienia je na wartość w czasie kompilacji, więc ta sama nazwa daje inny wynik w zależności od miejsca użycia. Wszystkie zwracają string, poza __LINE__, które zwraca liczbę.

Stała Co zwraca Przykładowa wartość
__LINE__ numer linii, w której stoi 42
__FILE__ pełną ścieżkę do pliku wraz z nazwą /app/src/Order.php
__DIR__ katalog pliku, bez ukośnika na końcu /app/src
__FUNCTION__ nazwę funkcji albo metody, w której stoi getName
__CLASS__ nazwę klasy razem z przestrzenią nazw App\Model\User
__METHOD__ nazwę metody poprzedzoną nazwą klasy App\Model\User::getName
__NAMESPACE__ bieżącą przestrzeń nazw App\Model

__DIR__ to ta, której będziesz używał najczęściej — do budowania ścieżek niezależnych od katalogu roboczego:

require __DIR__ . '/../vendor/autoload.php';

::class zamiast stringów z nazwami klas

Osobno warto zapamiętać ::class. To nie jest stała magiczna z powyższej tabeli, ale rozwiązuje pokrewny problem — zwraca pełną nazwę klasy jako string:

use App\Model\User;
echo User::class; // 'App\Model\User'
echo $user::class; // to samo, na instancji (od PHP 8.0)

Dlaczego to ważne: 'App\Model\User' wpisane ręcznie jest zwykłym tekstem. IDE go nie zrefaktoryzuje, analiza statyczna nie sprawdzi, a literówka wyjdzie dopiero w czasie wykonania.

User::class działa inaczej, bo korzysta z importu use na górze pliku. Gdy przeniesiesz klasę do innej przestrzeni nazw albo zmienisz jej nazwę, IDE poprawi ten jeden import, a User::class samo zacznie zwracać nową nazwę. Ręcznie wpisany string zostanie taki, jaki był — i wskaże na klasę, której już nie ma.

Zostaje pytanie, po co w ogóle podawać nazwę klasy, skoro zwykle pracuje się na gotowych obiektach. Przydaje się wtedy, gdy przekazujesz dalej nie sam obiekt, tylko informację, jaką klasę ma coś utworzyć albo z czym ją porównać:

$class = User::class;
$user = new $class(); // obiekt powstaje dopiero tutaj
if ($someObject::class === User::class) {
// ten obiekt jest dokładnie klasy User
}

Co dalej

Masz już plik, tryb ścisły, narzędzia do podglądania zawartości zmiennych i sposoby na przechowywanie danych w zmiennych i stałych. Brakuje najważniejszego: co tak naprawdę siedzi w tych zmiennych.

W następnej części zajmiemy się systemem typów PHP — dlaczego 'abc' == 0 przestało być prawdą dopiero w PHP 8, dlaczego (bool) '0' to false, a (bool) '0.0' to true, i której z tych rzeczy naprawdę trzeba się nauczyć na pamięć.

Komentarze