0

Объявление полей классов в PHP

Давно хотел разобраться, что будет если поле не объявлено явно в классе, но используется для чтения/записи. Периодически натыкался на такие конструкции в чужом коде, считал неверным, а потому сам никогда  не использовал и ответа на вопрос «Что же все-таки будет?» не знал. Кстати, в официальном мануале ничего об этом не нашел. Буду рад, если подскажете, где просмотрел.

Итак. Поля классов могут задаваться явно с помощью ключевых слов private, protected и public, определяющих видимость поля:


class A {

    private $foo;

    protected $bar;

    public $baz;
}

Есть еще конструкция var, но это просто устаревший аналог public.

А могут задаваться неявно, просто обращением к этому полю:


class A {

    public function getFoo() {
        return $this->foo;
    }

    public function setFoo($value) {
        $this->foo = $value;
    }
}

Второй способ никуда не годится:

  • Неявно определенное поле получает уровень доступ public. При явном определении — мы сразу указываем уровень доступа.
  • Если вызвать метод, читающий значение поля до его инициализации, то PHP сгенерирует Notice (при первом способе его нет). А поле будет инициализировано значением null.
  • То, что это поле есть в объекте, не получится узнать через Reflectionproperty_exists и get_class_vars. Только через isset и get_object_vars, и то только, если полю было присвоено значение.
  • С такими полями неудобно работать в редакторе — IDE их не подсказывает и вообще подсвечивает, как ошибочные. Можно описать эти поля с помощью PHPDoc тегами property, но непонятно, почему бы тогда не описать их явно как поля класса :).

В общем, не пользуйтесь никогда вторым способом. Он усложняет и запутывает код и является источником ошибок. Единственно, когда он имеет право на существование — это динамическое создание полей объекта. И даже тогда можно справиться геттерами и сеттерами (правда с потерями в производительности, при малом количестве вызовов потери незначительны).

5

Сокращенный тернарный оператор в PHP

Тернарный оператор — вещь известная и скучная:


$a = $expr1 ? $expr2 : $expr3;

Если $expr1 истинно, результатом $a станет $expr2, иначе $expr3.

А теперь об интересном! Начиная с версии PHP 5.3 можно опустить $expr2.


$a = $expr1 ?: $expr3;

Это равносильно записи:


$a = $expr1 ? $expr1 : $expr3;

Примеры:


$a = true ?: false; // true

$a = false ?: true; // true

$a = 1 ?: 2; // 1

$a = 0 ?: 2; // 2

В мануале об этом упоминается очень сухо и коротко. Ну а что еще добавить?

1

X-Forwarded-For и Opera

Браузеры фирмы Opera (десктопный в режиме Turbo, мобильные Mini и Mobile) используют прокси-сервера компании в Норвегии и других странах мира для сжатия трафика. Это очень удобно при плохой связи с Интернетом, особенно на мобильных телефонах. Но создает проблемы при попытке узнать IP-адрес подключившегося пользователя, например, для геолокации. Благо создатели браузера спешат нам на помощь. Специально для таких случаев они посылают нестандартизированный HTTP-загловок X-Forwarded-For, впервые внедренный для прокси-сервера Squid и впоследствии ставший стандартом де-факто для прокси. Формат заголовка:

X-Forwarded-For: {IP-адрес} [ , {IP-адрес} ]*

То есть X-Forwarded-For представляет собой строку IP-адресов, разделенных запятой, где адреса идут в порядке прокси-серверов от пользователя к серверам компании Opera (ну или любого прокси, поддерживающего этот заголовок).

Для определения географического положения пользователя нужно брать последний адрес из перечисленных в заголовке X-Forwarded-For — адрес, с которого пользователь пришел на сервера Opera. Первый может вполне быть внутренним прокси-сервером домашней сети и пользы от него нам никакой. Последний адрес может быть адресом другого прокси, но тут уж ничего не поделаешь.

Т.е. чтобы определить IP пользователя:

  1. смотрим заголовок X-Forwarded-For,
  2. если есть берем последний адрес в списке,
  3. если нет, то берем IP-адрес подключения.

Или как-то так на PHP:


function getUserIp() {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $forwarded = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        return trim(array_pop($forwarded));
    }
    return $_SERVER['REMOTE_ADDR'];
}

И да, полностью полагаться на геолокацию на основе IP неправильно и всегда нужно давать пользователю возможность свое местоположение уточнить.

Кстати, X-Forwarded-For не единственный заголовок, выставляемый прокси. Интернеты сообщают еще о существовании заголовков X-REAL-IP, VIA и других. Но Opera их не использует :)

7

Кодировки при работе с DOMDocument

DomDocument — мощный инструмент работы с XML и HTML с использованием методов DOM. Главное при его использовании не запутаться в кодировках.

Конструктор DOMDocument принимает два параметра — версию и кодировку документа

$dom = new DOMDocument([ string $version [, string $encoding ]]);

А чтобы начать работать с существующим HTML-документом его надо загрузить в объект DOMDocument:

$dom->loadHTML( string $source );

Проблема в том, что документ загружается совсем не в той кодировке, что указана в конструкторе. Вообще, параметры конструктора никак не влияют на загруженный документ. А используются только при методах save():

$htmlString = $dom->saveHTML ();

Так что параметры конструктора можно совсем опустить, если вы собираетесь только работать с существующим деревом и не планируете его изменять и выгружать.

При загрузке же кодировка определяется только исходя из содержимого, загружаемого документа. При этом достаточно часто объект не может определить кодировку верно. Поэтому желательно указывать ее вручную. Возможны два способа. Добавить к импортируемому документу строку кодировки


$dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $source);

Либо сконвертировать все символы документа в HTML-entities перед импортом


$source = mb_convert_encoding($source, 'HTML-ENTITIES', 'utf-8');
$dom->loadHTML($source);

Первый вариант кажется предпочтительнее в плане производительности. В плане результатов работы различий замечено не было.

Если работать с DOM планируется много, то удобно оформить эти конструкции в отдельный класс наследника DOMDocument, добавить в методе загрузки дерева второй параметр кодировки и определить метод, например, так:


class MyDOMDocument extends DOMDocument {
    public function loadHTML($source, $encoding) {
        $source = '<meta http-equiv="Content-Type" content="text/html; charset='.
            $encoding.'">' . $source
        $dom->loadHTML('' . $source);
    }
}
0

Позднее статическое связывание в PHP

В PHP 5.3 появилась такая интересная возможность, как позднее статическое связывание (late static binding). Дальше немного вольный перевод описания из официального мануала.

Начиная с PHP 5.3.0 в языке реализована возможность, называемая поздним статическим связыванием, которая может использоваться для ссылки на вызываемый класс в контексте статического наследования.

Эту возможность назвали «позднее статическое связывание». «Позднее связывание» говорит о том, что static:: будет разрешаться не относительно класса, где определен метод, но будет вычисляться во время выполнения. «Статическое связывание» означает, что оно может быть использовано в вызовах статических методов (но не ограничивается только ими).

Ограничения self::

Статические ссылки на текущий класс в виде self:: или __CLASS__ разрешаются относительно класса, к которому относится функция, то есть относительно класса, где ссылка была определена:

Пример № 1: использование self::

<?php
class A {
  public static function who() {
    echo __CLASS__;
  }
  public static function test() {
    self::who();
  }
}

class B extends A {
  public static function who() {
    echo __CLASS__;
  }
}

B::test();
?>

Пример выведет:

A

Использование позднего статического связывания

Позднее статическое связывание пытается решить это ограничение, вводя ключевое слово, ссылающееся на класс, первоначально вызванный в процессе выполнения. То есть, ключевое слово, которое позволит сослаться на B из test() в предыдущем примере. Было решено не вводить новое слово, а использовать уже зарезервированное static.

Пример № 2: простое использование static::

<?php
class A {
  public static function who() {
    echo __CLASS__;
  }
  public static function test() {
    static::who(); // здесь происходит позднее статическое связывание
  }
}

class B extends A {
  public static function who() {
    echo __CLASS__;
  }
}

B::test();
?>

Пример выведет:

B

Замечание: static:: не работает как $this для статических методов! $this-> следует правилам наследования, а static:: нет. Это различие уточняется ниже.

Пример № 3: использование static:: в нестатическом контексте

<?php
class TestChild extends TestParent {
  public function __construct() {
    static::who();
  }

  public function test() {
    $o = new TestParent();
  }

  public static function who() {
    echo __CLASS__."\n";
  }
}

class TestParent {
  public function __construct() {
    static::who();
  }

  public static function who() {
    echo __CLASS__."\n";
  }
}
$o = new TestChild;
$o->test();
?>

Пример выведет:

TestChild
TestParent

Замечание: Позднее статическое связывание останавливает процесс разрешения вызова. Статические вызовы с использованием ключевых слов parent:: или self:: передают информацию о вызове дальше.

Пример № 4: Передача и непередача вызовов

<?php
class A {
  public static function foo() {
    static::who();
  }

  public static function who() {
    echo __CLASS__."\n";
  }
}

class B extends A {
  public static function test() {
    A::foo();
    parent::foo();
    self::foo();
  }

  public static function who() {
    echo __CLASS__."\n";
  }
}
class C extends B {
  public static function who() {
    echo __CLASS__."\n";
  }
}

C::test();
?>

Пример выведет

A
C
C

Крайние случаи

Существует множество различных способов вызвать метод в PHP, такие как коллбэки или магически методы. Поскольку позднее статическое связывание разрешается во время выполнения, это может привести к неожиданным результатам в так называемых крайних случаях.

Пример № 5 Позднее статическое связывание в магических методах

<?php
class A {
  protected static function who() {
    echo __CLASS__."\n";
  }

  public function __get($var) {
    return static::who();
  }
}

class B extends A {
  protected static function who() {
    echo __CLASS__."\n";
  }
}

$b = new B;
$b->foo;
?>

Пример выведет:

B

Пару статей по теме на хабре: Singleton и Late static binding и Позднее статическое связывание в PHP.

Copyright © 2017 — dec5e | Site design by Trevor Fitzgerald