Analiza isset() w PHP
isset()
jest jednym z najważniejszych narzędzi do walidacji danych w PHP. Jak sama nazwa wskazuje, jest on zaprojektowany w celu sprawdzenia, czy zmienna, która została mu podana, jest ustawiona (isset), zwracając wartość logiczną na podstawie wyniku.
Ma jednak swoje dziwactwa i pewne zachowania, które naprawde warto znać, gdyż mogą łatwo zaskoczyć nawet doświadczonych programistów.
Przyjrzyjmy się zatem temu, jak się zachowuje i co w nim takiego specjalnego. Nawet jeśli jesteś weteranem PHP, mam nadzieję, że znajdziesz tu coś nowego.
To konstrukcja języka, a nie funkcja
Pomimo tego, że wygląda tak samo, isset()
nie jest typową funkcją, i nie zachowuje się tak jak można by się było spodziewać.
Podobnie jak die()
, array()
, print()
i inne, jest to "konstrukt języka", co jest fantazyjnym określeniem, które w terminologii laików oznacza, że jest wbudowany bezpośrednio w silnik PHP i może posiadać pewne unikalne zachowania, które różnią się od typowych wbudowanych lub zdefiniowanych przez użytkownika funkcji, co omówimy niedługo.
Nie można go używać jako callable
Dowolna wbudowana lub zdefiniowana przez użytkownika funkcja może być używana jako "wywoływalny" wskaźnik do funkcji, który może być wywoływany dynamicznie i używany do wzorców takich jak currying.
is_callable('strtoupper');
// true
array_map('strtoupper', ['a', 'b', null, 'd']);
// ['A', 'B', '', 'D']
Ponieważ jest to konstrukcja języka, a nie funkcja, nie można jej wywoływać ani używać w taki sposób.
is_callable('isset');
// false
array_map('isset', ['a', 'b', null, 'd']);
// PHP Warning: array_map() expects parameter 1 to be a valid callback, function 'isset' not found or invalid function name…
Nie akceptuje wyrażenia
Podczas gdy regularne funkcje i inne konstrukcje języka mogą zaakceptować wynik dowolnego wyrażenia, ze względu na jego unikalną naturę, isset()
może zaakceptować jedynie zmienną, klucz tablicy lub właściwość obiektu jako argument.
Próba użycia go w inny sposób spowoduje błąd krytyczny.
if (isset('Hello world')) {
// Fatal error: Cannot use isset() on the result of an expression (you can use "null !== expression" instead)
}
if (isset($a->b())) {
// Fatal error: Cannot use isset() on the result of an expression (you can use "null !== expression" instead)
}
if (isset(! $a)) {
// Fatal error: Cannot use isset() on the result of an expression (you can use "null !== expression" instead)
}
if (isset(CONSTANT)) {
// Fatal error: Cannot use isset() on the result of an expression (you can use "null !== expression" instead)
}
Sprawdza również, czy wartość jest pusta
isset()
zwróci false
jeśli zmienna jest niezdefiniowana LUB jeśli jej wartością jest null
. Może Cię to zmylić, jeśli null jest właściwą wartością, którą ustawiłeś i na którą chcesz zezwolić.
$value = null;
if (isset($value)) {
// ...
}
Jednym ze sposobów na sprawdzenie tego, w zależności od wymagań, to potwierdzenie, czy zmienna jest zdefiniowana w bieżącym zasięgu poprzez get_defined_vars()
i zbadanie kluczy w wynikowych tablicach.
$value = null;
if (array_key_exists('value', get_defined_vars())) {
// ...
}
Może zaakceptować wiele argumentów
Bardzo często zdarza się, że ludzie łączą wywołania, aby sprawdzić, czy wiele wartości jest ustawianych jedna po drugiej.
if (isset($a) && isset($b) && isset($c)) {
// ...
}
Jednakże isset()
jest funkcją ze zmienną liczbę argumentów, która może zaakceptować dowolną liczbę parametrów jednocześnie, aby osiągnąć ten sam efekt, potwierdzając, czy wszystkie przekazywane zmienne są ustawione.
Może to być świetny sposób na skrócenie długich instrukcji warunkowych.
if (isset($a, $b, $c)) {
// ...
}
Nie wywołuje komunikatów "undefined variable/index/property"
Jeśli pobierasz wartość zagnieżdżoną na wielu poziomach, prawdopodobnie chcesz się upewnić, że każdy etap łańcucha istnieje.
if (isset($response, $response->list, $response->list['results'], $response->list['results'][0])) {
// ...
}
if (isset($arr[$key], $otherArr[$arr[$key]], $otherArr[$arr[$key]][$otherKey])) {
// ...
}
Jednakże isset()
nie spowoduje wystąpienia "undefined variable", "undefined index" czy "undefined property", bez względu na ilość warstw przez które przejdziesz.
Oznacza to, że zamiast potwierdzać wartość na każdym kroku, można je wszystkie wykonać za jednym zamachem:
if (isset($response->list['results'][0])) {
// ...
}
if (isset($otherArr[$arr[$key]][$otherKey])) {
// ...
}
Błędy "Undefined method" są wywoływane
Jeśli sprawdzany łańcuch zawiera w połowie drogi wywołanie metody, PHP spróbuje ją wywołać.
Oznacza to, że jeśli wcześniejsza część łańcucha nie istnieje lub ostatnia wartość w łańcuchu jest obiektem, który po prostu nie ma tej metody, pojawi się błąd.
$a = new stdClass();
if (isset($a->b()->c)) {
// Fatal error: Uncaught Error: Call to undefined method A::b()…
}
if (isset($a->b->c()->d)) {
// Fatal error: Uncaught Error: Call to a member function c() on null…
}
Jednym ze sposobów radzenia sobie z tym problemem jest jawne sprawdzenie w instrukcjach warunkowych, zatrzymanie łańcucha i wywołanie method_exists()
w celu sprawdzenia, czy metoda istnieje za każdym razem, gdy jest potrzebna.
if (isset($a) && method_exists($a, 'b') && isset($a->b()->c)) {
// ...
}
Jednym ze sposobów skrócenia takiego wyrażenia jest użycie operatora kontroli błędów, który eliminuje wszelkie błędy dla pojedynczego wyrażenia. Jeśli wyrażenie spowoduje błąd, operator zwróci zamiast tego null
i będzie kontynuował wykonywanie.
if (@$a->b()->c !== null) {
// ...
}
Tym niemniej, choć może to być wygodne, należy pamiętać, że operator kontroli błędów jest bardzo nieefektywny i może również zatuszować błędy, tam gdzie wcale tego nie chciałeś. Nie jest to bezpośredni zamiennik dla isset()
.
!empty() jest nieco inne
empty()
jest również konstrukcją języka o podobnym zachowaniu jak isset()
, ponieważ nie powoduje powiadomień związanych z "undefined".
$a = [];
if (empty($a['b']->c)) {
// ...
}
Wygląda na to, że służy jako bezpośrednia odwrotność isset()
, ale tak nie jest. empty()
może również przyjmować wyrażenia jako swoje argumenty, ale, co ważniejsze, wspiera typ juggling, więc obsługuje wartości falsey.
$a = '0';
if (isset($a)) {
// It IS set
}
if (empty($a)) {
// It IS empty
}
Null coalesce operator (Operator ??)
Bardzo często zdarza się, że chcemy podać wartość awaryjną w przypadku, gdy zmienna nie jest ustawiona. Zazwyczaj odbywa się to za pomocą krótkiego warunkowego if
lub operatora warunkowego.
$result = isset($value) ? $value : 'fallback';
Jednak od PHP 7.0, można to skrócić za pomocą Null coalesce operator (??
), który zwróci pierwszą wartość, jeśli jest ustalona, lub drugą wartość, jeśli nie.
$result = $value ?? 'fallback';
Jeśli w trakcie zwracania nowej wartości, nie chciałeś ustawiać nowej zmiennej, taki krok również jest tu uwzględniony. W PHP 7.4, Null Coalescing Assignment (??=
) pozwala na jeszcze krótszy sposób ustawienia zmiennej na wartość domyślną, jeśli nie jest jeszcze ustawiona.
$value ??= 'fallback';
Nie wykonuje magicznej metody __get()
Załóżmy, że mamy dość typową klasę, która może dynamicznie pobierać właściwości przy użyciu magicznej metody __get()
uzyskać ich wartość.
class Person
{
protected $attributes = [];
public function __get($name)
{
return $this->attributes[$name] ?? null;
}
public function __set($name, $value)
{
$this->attributes[$name] = $value;
}
}
Jeśli używamy tej klasy do ustawienia właściwości, możemy z niej korzystać w sposób w jaki można by się od niej spodziewać. Jeśli jednak sprawdzimy, czy wartość jest ustawiona, zwróci ona wartość false
.
$person = new Person();
$person->name = 'Liam';
echo $person->name; // 'Liam'
isset($person->name); // false
Czekaj, co tu się dzieje?! Ponieważ isset()
jest konstrukcją języka, a nie regularną funkcją, wyrażenie nie jest wykonywane przed jego przekazaniem. Ponieważ name
nie jest prawdziwą właściwością zdefiniowaną na obiekcie, tak naprawdę nie istnieje.
Jednakże, gdy isset()
zostanie wywołany na właściwości, która nie istnieje lub jest niedostępna dla bieżącego zasięgu (np. będzie chroniona lub prywatna), wywoła magiczną metodę __isset()
, jeśli klasa ma ją zdefiniowaną. Pozwala to na określenie, czy właściwość, którą sprawdzamy, jest ustawiona zgodnie z naszymi własnymi regułami.
class Person
{
// ...
public function __isset($name)
{
return isset($this->attributes[$name]);
}
}
Dzięki temu wszystko działa zgodnie z oczekiwaniami.
$person = new Person();
$person->name = 'Liam';
isset($person->name); // true
isset($person->somethingElse); // false
Należy pamiętać, że jeśli sprawdzasz właściwości zagnieżdżone, __isset()
zostanie wywołany jako właściwy dla każdej właściwości w łańcuchu.
Nieistniejące zmienne można przekazywać do funkcji użytkownika
Jak już wspomniałem, ponieważ isset()
jest konstrukcją językową, ma specjalne zachowanie wynikające z rdzenia PHP, a więc nie zachowuje się jak funkcje, które sami definiujemy.
Podobny efekt możemy jednak osiągnąć w funkcjach użytkowanych przez użytkowników, dzięki wykorzystaniu referencji. W ten sposób otwieramy możliwość wyeksponowania dodatkowych funkcji, wybranych przez nas samych, oprócz zwykłej konstrukcji języka.
Jednym z praktycznych przykładów może być traktowanie wszelkich obiektów implementujących Pusty Obiekt (wzorzec projektowy) jako fałszywe wartości.
interface NullObject {}
class Logger {
// ...
}
class NullLogger extends Logger implements NullObject {
// ...
}
function is_truthy(&$value)
{
if ($value instanceof NullObject) {
return false;
}
return (bool) $value;
}
is_truthy($a);
// false
$b = '';
is_truthy($b);
// false
$c = '1';
is_truthy($c);
// true
$logger = new Logger();
is_truthy($logger);
// true
$nullLogger = new NullLogger();
is_truthy($nullLogger);
// false
Jednak referencje nie zawsze są tak bezpieczne w użyciu, ponieważ samo ich użycie może mieć wpływ na pierwotną wartość, nawet jeśli funkcja nie robi tego wprost.
Na przykład, dowolne niezdefiniowane klucze w tablicy lub właściwości zostaną automatycznie przypisane, a ich wartość zostanie ustawiona na null
.
$a = [];
is_truthy($a['b']['c']);
// false
var_dump($a);
// [
// 'b' => [
// 'c' => null,
// ],
// ]
Wniosek
Miejmy nadzieję, że w całej tej analizie isset()
, jego zachowania i innych powiązanych rzeczy, znalazłeś coś, co pomoże Ci uczynić Twój kod czystszym, bardziej wyraźnym, oraz że oszczędzi Ci problemów w mniej popularnych przypadkach, kiedy musisz sprawdzić, czy zmienna została ustawiona.
Oryginał tekstu w języku angielskim przeczytasz tutaj.