MidТеория9 min

Особенности языка

Типы данных, ссылки, генераторы, магические методы, Fibers

Раздел посвящён вопросам, которые проверяют глубину понимания внутренних механизмов PHP: система типов, работа ссылок, генераторы, магические методы и кооперативная многозадачность.

Система типов PHP

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

Скалярные типы

<?php
declare(strict_types=1);

// bool — true/false
$isActive = true;

// int — целые числа (платформозависимый размер: 64-bit на x86_64)
$count = 42;
$hex = 0x1A;          // 26
$octal = 0o755;       // 493 (PHP 8.1+ prefix)
$binary = 0b11111111; // 255
$underscore = 1_000_000; // Underscores for readability (PHP 7.4+)

// float — числа с плавающей точкой (IEEE 754 double precision)
$price = 19.99;
$scientific = 1.2e3;  // 1200.0

// string — байтовая строка (не Unicode!)
$name = 'Alice';
$interpolated = "Hello, {$name}";
$heredoc = <<<EOT
    Multi-line
    string with $name interpolation
    EOT;
$nowdoc = <<<'EOT'
    No $interpolation here
    EOT;

Ловушка собеседования: PHP строки --- последовательности байтов, а не символов. strlen('Привет') вернёт 12 (UTF-8), а не 6. Для работы с Unicode используйте mb_string или IntlChar.

Составные типы

<?php
declare(strict_types=1);

// array — ordered map (NOT a traditional array!)
$list = [1, 2, 3];                      // Indexed array
$map = ['name' => 'Alice', 'age' => 30]; // Associative array

// object — instance of a class
$obj = new \stdClass();
$obj->name = 'Alice';

// callable — anything that can be called
$fn = function (int $x): int { return $x * 2; };
$fn = strlen(...);  // First-class callable (PHP 8.1+)

// iterable — array | Traversable (PHP 7.1+)
function processItems(iterable $items): void
{
    foreach ($items as $item) {
        // Works with arrays AND iterators
    }
}

Специальные типы

<?php
declare(strict_types=1);

// null — only one value: null
$nothing = null;

// void — function returns nothing (cannot be used as property type)
function logMessage(string $msg): void
{
    file_put_contents('/tmp/log.txt', $msg, FILE_APPEND);
    // No return, or just 'return;'
}

// never — function NEVER returns (always throws or exits)
function throwError(string $msg): never
{
    throw new \RuntimeException($msg);
    // Code after is unreachable
}

// mixed — any type (explicitly declared, PHP 8.0+)
// Equivalent to: string|int|float|bool|null|array|object|callable|resource
function debugDump(mixed $value): void
{
    var_dump($value);
}

Union и Intersection типы

<?php
declare(strict_types=1);

// Union types (PHP 8.0+) — one of listed types
function formatId(int|string $id): string
{
    return (string) $id;
}

// Intersection types (PHP 8.1+) — must satisfy ALL types
interface Countable {}
interface Serializable {}

function process(Countable&Serializable $data): void
{
    // $data must implement BOTH interfaces
}

// DNF types: Disjunctive Normal Form (PHP 8.2+)
function handle((Countable&Serializable)|null $data): void
{
    // Nullable intersection type
}

// Standalone types
function getId(): true  { return true; }   // true type (PHP 8.2+)
function check(): false { return false; }  // false type (PHP 8.2+)
function empty(): null  { return null; }   // null type (PHP 8.2+)

Ссылки в PHP

Ссылки --- один из самых непонятых механизмов PHP. Это НЕ указатели (как в C), а псевдонимы в таблице имён переменных.

Присвоение по ссылке

<?php
declare(strict_types=1);

$a = 'hello';
$b = &$a;  // $b is now an ALIAS for $a — same zval in symbol table

$b = 'world';
echo $a;   // "world" — $a changed because $b is an alias

// Unsetting a reference does NOT destroy the value
unset($b);
echo $a;   // "world" — $a still exists, only the alias $b is removed

Передача по ссылке

<?php
declare(strict_types=1);

function increment(int &$value): void
{
    $value++;
}

$counter = 0;
increment($counter);
echo $counter; // 1 — modified in-place

// CANNOT pass a literal by reference
// increment(5); // Fatal error: Only variables can be passed by reference

// Array elements by reference
$items = [1, 2, 3];
foreach ($items as &$item) {
    $item *= 2;
}
unset($item); // CRITICAL: always unset after reference foreach!

var_dump($items); // [2, 4, 6]

Возврат по ссылке

<?php
declare(strict_types=1);

class Container
{
    private array $data = ['key' => 'original'];

    // Returns a reference to internal data
    public function &get(string $key): mixed
    {
        return $this->data[$key];
    }
}

$container = new Container();
$ref = &$container->get('key');
$ref = 'modified';
echo $container->get('key'); // "modified" — internal state changed via reference

Критически важно для собеседования: 1) Ссылки --- НЕ указатели, а псевдонимы в таблице символов. 2) unset($ref) удаляет псевдоним, но не значение. 3) После foreach с &$item ВСЕГДА делайте unset($item), иначе последний элемент массива будет перезаписан при следующем использовании $item. 4) Нельзя передать литерал по ссылке.

Типичная ошибка с foreach по ссылке

<?php
declare(strict_types=1);

$numbers = [1, 2, 3];

foreach ($numbers as &$num) {
    $num *= 10;
}
// $num is still a reference to $numbers[2]!

foreach ($numbers as $num) {
    // Each iteration overwrites $numbers[2] because $num is still a reference!
}

var_dump($numbers); // [10, 20, 20] — NOT [10, 20, 30]!
// Fix: unset($num) after the first foreach

Генераторы (Generators)

Генераторы позволяют создавать итераторы без реализации интерфейса Iterator. Функция-генератор использует yield для поочерёдной отдачи значений, экономя память.

Базовое использование

<?php
declare(strict_types=1);

// Regular function — loads ALL into memory
function getAllNumbers(int $max): array
{
    $numbers = [];
    for ($i = 1; $i <= $max; $i++) {
        $numbers[] = $i;
    }
    return $numbers; // 10M items = ~160 MB RAM
}

// Generator — yields one at a time
function generateNumbers(int $max): \Generator
{
    for ($i = 1; $i <= $max; $i++) {
        yield $i; // Pauses here, resumes on next iteration
    }
}

// Memory-efficient iteration
foreach (generateNumbers(10_000_000) as $number) {
    // Constant memory usage — only ONE value in memory at a time
    if ($number > 5) break;
}

yield с ключами и значениями

<?php
declare(strict_types=1);

function readCsvRows(string $filePath): \Generator
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new \RuntimeException("Cannot open file: {$filePath}");
    }

    try {
        $lineNumber = 0;
        while (($row = fgetcsv($handle)) !== false) {
            // yield key => value
            yield $lineNumber => $row;
            $lineNumber++;
        }
    } finally {
        fclose($handle);
    }
}

// Usage
foreach (readCsvRows('/tmp/data.csv') as $line => $columns) {
    echo "Line {$line}: " . implode(', ', $columns) . "\n";
}

yield from и send()

<?php
declare(strict_types=1);

// Delegate to another generator with yield from
function innerGenerator(): \Generator
{
    yield 'a';
    yield 'b';
    return 'inner-result'; // Return value accessible via getReturn()
}

function outerGenerator(): \Generator
{
    yield 'start';
    $result = yield from innerGenerator(); // Delegates to innerGenerator
    echo "Inner returned: {$result}\n";    // "inner-result"
    yield 'end';
}

foreach (outerGenerator() as $value) {
    echo $value . "\n"; // start, a, b, end
}

// send() — send values INTO generator
function accumulator(): \Generator
{
    $total = 0;
    while (true) {
        $value = yield $total; // Receive value from send(), yield current total
        if ($value === null) break;
        $total += $value;
    }
}

$gen = accumulator();
$gen->current();       // Initialize: 0
$gen->send(10);        // Total: 10
$gen->send(20);        // Total: 30
echo $gen->send(5);    // 35

На собеседовании часто спрашивают: Зачем нужны генераторы? Основные сценарии: 1) Обработка больших файлов построчно. 2) Пагинация из БД. 3) Бесконечные последовательности. 4) Pipeline-обработка данных. Генератор --- это прерываемая функция, которая помнит своё состояние.

Магические методы

Магические методы --- специальные методы с двойным подчёркиванием, которые вызываются PHP автоматически при определённых действиях с объектом.

Конструктор и деструктор

<?php
declare(strict_types=1);

final class DatabaseConnection
{
    private \PDO $pdo;

    public function __construct(
        private readonly string $dsn,
        private readonly string $user,
        private readonly string $password,
    ) {
        $this->pdo = new \PDO($dsn, $user, $password);
        echo "Connection opened\n";
    }

    public function __destruct()
    {
        // Called when object is garbage collected or script ends
        echo "Connection closed\n";
        // $this->pdo is automatically released
    }
}

Перехват доступа к свойствам

<?php
declare(strict_types=1);

final class DynamicConfig
{
    private array $data = [];

    // Called when reading an inaccessible property
    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? throw new \OutOfBoundsException(
            "Property '{$name}' does not exist"
        );
    }

    // Called when writing to an inaccessible property
    public function __set(string $name, mixed $value): void
    {
        $this->data[$name] = $value;
    }

    // Called by isset() or empty() on an inaccessible property
    public function __isset(string $name): bool
    {
        return isset($this->data[$name]);
    }

    // Called by unset() on an inaccessible property
    public function __unset(string $name): void
    {
        unset($this->data[$name]);
    }
}

$config = new DynamicConfig();
$config->database = 'mysql';    // __set()
echo $config->database;         // __get() → "mysql"
echo isset($config->database);  // __isset() → true
unset($config->database);       // __unset()

Перехват вызовов методов

<?php
declare(strict_types=1);

final class QueryBuilder
{
    private array $conditions = [];

    // Called when invoking inaccessible instance methods
    public function __call(string $name, array $arguments): self
    {
        if (str_starts_with($name, 'where')) {
            $field = lcfirst(substr($name, 5)); // whereEmail → email
            $this->conditions[$field] = $arguments[0] ?? null;
            return $this;
        }
        throw new \BadMethodCallException("Method {$name} does not exist");
    }

    // Called when invoking inaccessible static methods
    public static function __callStatic(string $name, array $arguments): self
    {
        $instance = new self();
        return $instance->__call($name, $arguments);
    }

    public function getConditions(): array
    {
        return $this->conditions;
    }
}

$query = new QueryBuilder();
$query->whereEmail('[email protected]')->whereName('Alice');
// $conditions: ['email' => '[email protected]', 'name' => 'Alice']

$query2 = QueryBuilder::whereStatus('active');
// Static call handled by __callStatic

__toString, __invoke, __clone

<?php
declare(strict_types=1);

final class Money
{
    public function __construct(
        private int $amount,
        private string $currency,
    ) {}

    // Called when object is used as string
    public function __toString(): string
    {
        return number_format($this->amount / 100, 2) . ' ' . $this->currency;
    }

    // Called when object is used as a function
    public function __invoke(float $multiplier): self
    {
        return new self(
            (int) ($this->amount * $multiplier),
            $this->currency,
        );
    }

    // Called after clone — customize cloning behavior
    public function __clone(): void
    {
        // Perform deep copy of nested objects if needed
        // In this case, only scalars — nothing to do
    }
}

$price = new Money(1999, 'USD');
echo $price;           // __toString() → "19.99 USD"
echo (string) $price;  // Same: "19.99 USD"

$doubled = $price(2.0); // __invoke() → Money(3998, 'USD')
echo $doubled;           // "39.98 USD"

$copy = clone $price;    // __clone() called after shallow copy

Сериализация: __serialize / __unserialize

<?php
declare(strict_types=1);

final class SecureSession
{
    public function __construct(
        private string $userId,
        private string $token,
        private \DateTimeImmutable $expiresAt,
    ) {}

    // Modern approach (PHP 7.4+) — replaces __sleep/__wakeup
    public function __serialize(): array
    {
        return [
            'userId' => $this->userId,
            'token' => $this->token,
            'expiresAt' => $this->expiresAt->format('c'),
            // Sensitive data can be excluded
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->userId = $data['userId'];
        $this->token = $data['token'];
        $this->expiresAt = new \DateTimeImmutable($data['expiresAt']);
    }

    // __debugInfo — controls var_dump() output
    public function __debugInfo(): array
    {
        return [
            'userId' => $this->userId,
            'token' => '***HIDDEN***',
            'expiresAt' => $this->expiresAt->format('Y-m-d H:i:s'),
        ];
    }
}

$session = new SecureSession('user-1', 'secret-token', new \DateTimeImmutable('+1 hour'));

$serialized = serialize($session);   // Calls __serialize()
$restored = unserialize($serialized); // Calls __unserialize()

var_dump($session); // __debugInfo() — token is hidden

Для собеседования: __serialize() / __unserialize() (PHP 7.4+) --- современная замена __sleep() / __wakeup(). Они имеют приоритет: если определены обе пары, используются __serialize() / __unserialize(). __debugInfo() контролирует вывод var_dump().

Приведение типов и строгое сравнение

== vs === (жонглирование типами)

<?php
declare(strict_types=1);

// Loose comparison (==) — type juggling
var_dump(0 == 'foo');     // false (PHP 8.0+), was true in PHP 7!
var_dump(0 == '');        // false (PHP 8.0+), was true in PHP 7!
var_dump('' == null);     // true (!)
var_dump('0' == false);   // true (!)
var_dump('' == false);    // true (!)
var_dump(null == false);  // true (!)
var_dump(0 == null);      // true (!)
var_dump('1' == true);    // true

// Strict comparison (===) — no type conversion
var_dump(0 === false);    // false — different types
var_dump('' === null);    // false — different types
var_dump(1 === true);     // false — different types
var_dump('1' === '1');    // true  — same type, same value

// Spaceship operator <=> for sorting
var_dump(1 <=> 2);   // -1
var_dump(2 <=> 2);   // 0
var_dump(3 <=> 2);   // 1

declare(strict_types=1)

<?php
declare(strict_types=1);

function addInts(int $a, int $b): int
{
    return $a + $b;
}

// With strict_types=1:
// addInts(1.5, 2);  // TypeError: must be int, float given
// addInts('1', 2);  // TypeError: must be int, string given
addInts(1, 2);       // OK: 3

// IMPORTANT: strict_types affects the FILE where the CALL is made
// Not the file where the function is defined!

Ключевой момент: strict_types влияет на файл, в котором происходит ВЫЗОВ функции, а не на файл с определением. В режиме strict_types=1 автоматическое приведение скалярных типов отключается, и передача неверного типа вызывает TypeError.

Fibers (Кооперативная многозадачность)

Fibers (PHP 8.1+) --- механизм кооперативной многозадачности. Fiber --- это блок кода, который может быть приостановлен и возобновлён, сохраняя свой стек вызовов.

Базовое использование

<?php
declare(strict_types=1);

$fiber = new \Fiber(function (): void {
    $value = \Fiber::suspend('first');  // Pause and return 'first'
    echo "Received: {$value}\n";        // "Received: hello"

    $value2 = \Fiber::suspend('second'); // Pause and return 'second'
    echo "Received: {$value2}\n";        // "Received: world"
});

// Start the fiber
$result1 = $fiber->start();
echo "Fiber yielded: {$result1}\n";  // "Fiber yielded: first"

// Resume with a value
$result2 = $fiber->resume('hello');
echo "Fiber yielded: {$result2}\n";  // "Fiber yielded: second"

// Resume again
$fiber->resume('world');
// Fiber completes

echo $fiber->isTerminated() ? 'Done' : 'Running'; // "Done"

Практический пример: асинхронные задачи

<?php
declare(strict_types=1);

final class Scheduler
{
    /** @var \SplQueue<\Fiber> */
    private \SplQueue $queue;

    public function __construct()
    {
        $this->queue = new \SplQueue();
    }

    public function schedule(\Fiber $fiber): void
    {
        $this->queue->enqueue($fiber);
    }

    public function run(): void
    {
        while (!$this->queue->isEmpty()) {
            $fiber = $this->queue->dequeue();

            if (!$fiber->isStarted()) {
                $fiber->start();
            } elseif ($fiber->isSuspended()) {
                $fiber->resume();
            }

            // If fiber is not terminated, reschedule
            if (!$fiber->isTerminated()) {
                $this->queue->enqueue($fiber);
            }
        }
    }
}

// Create tasks
$task1 = new \Fiber(function (): void {
    echo "Task 1: Step 1\n";
    \Fiber::suspend();
    echo "Task 1: Step 2\n";
});

$task2 = new \Fiber(function (): void {
    echo "Task 2: Step 1\n";
    \Fiber::suspend();
    echo "Task 2: Step 2\n";
});

$scheduler = new Scheduler();
$scheduler->schedule($task1);
$scheduler->schedule($task2);
$scheduler->run();
// Output:
// Task 1: Step 1
// Task 2: Step 1
// Task 1: Step 2
// Task 2: Step 2

Fiber API

<?php
declare(strict_types=1);

$fiber = new \Fiber(function (): string {
    \Fiber::suspend();
    return 'completed';
});

// Fiber lifecycle methods
$fiber->start();           // Start execution
$fiber->resume('data');    // Resume suspended fiber with data
$fiber->getReturn();       // Get return value (only after termination)

// Fiber state inspection
$fiber->isStarted();       // Has start() been called?
$fiber->isRunning();       // Is currently executing?
$fiber->isSuspended();     // Is paused via suspend()?
$fiber->isTerminated();    // Has finished execution?

// Static method — can only be called INSIDE a fiber
\Fiber::suspend($value);  // Pause current fiber, optionally pass value back

Для собеседования: Fibers --- НЕ потоки и НЕ параллельное выполнение. Это кооперативная многозадачность: fiber сам решает, когда отдать управление через suspend(). Fibers используются библиотеками (ReactPHP, Revolt) для построения async API. В пользовательском коде они редко используются напрямую.


Проверь себя

Являются ли Fibers параллельным выполнением кода?

Что произойдёт после foreach ($arr as &$item) без последующего unset($item)?

На какой файл влияет declare(strict_types=1)?

Чем __serialize() отличается от __sleep()?

Что вернёт strlen('Привет') в PHP?