PHP 8. Современная разработка: от чистого языка до production
PHP 8. Современная разработка: от чистого языка до production

Полная версия

PHP 8. Современная разработка: от чистого языка до production

Язык: Русский
Год издания: 2026
Добавлена:
Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
2 из 2

Частичное применение (вручную):

php


function discount(float $percent): \Closure

{

return fn(float $price): float => $price * (1 - $percent / 100);

}


$winterSale = discount(20);

$vipSale = discount(50);


$winterSale(1000); // 800.0

$vipSale(1000); // 500.0


3.6. Чистые функции: почему это важно


Чистая функция:


Для одних и тех же входных данных всегда возвращает один и тот же результат.


Не имеет побочных эффектов (не пишет в БД, не меняет глобальные переменные, не печатает на экран).


php


// Нечистая:

function applyDiscountUnclean(float $price): float

{

global $currentUser;

if ($currentUser->isVip()) {

return $price * 0.8;

}

return $price;

}


// Чистая:

function applyDiscountPure(float $price, User $user): float

{

if ($user->isVip()) {

return $price * 0.8;

}

return $price;

}


Разница: чистую функцию легко тестировать. Не нужно поднимать базу, мокировать глобальное состояние. Вызываем с разными User и проверяем результат.


В нашем менеджере финансов мы будем стремиться к тому, чтобы бизнес-логика была чистой, а все побочные эффекты (БД, файлы, HTTP) — изолированы в отдельных слоях. Это главная архитектурная мысль книги.

3.7. Практическое задание


Конвертер валют на замыканиях:

Напишите функцию makeConverter(float $rate): \Closure, которая возвращает функцию для конвертации суммы по курсу. Создайте конвертеры $usdToRub и $eurToRub. Сконвертируйте 100 единиц каждой валюты.


Калькулятор с match:

Функция calculate(float $a, float $b, string $operation): float|string. Операции: '+', '-', '*', '/'. При делении на ноль вернуть строку 'Ошибка: деление на ноль'. Используйте match. Проверьте строгость сравнения, передав '+' и '+' через переменную с пробелом.


Сортировка пользователей по возрасту:

Дан массив пользователей:

php


$users = [

['name' => 'Иван', 'age' => 25],

['name' => 'Мария', 'age' => 31],

['name' => 'Пётр', 'age' => 19],

];


Отсортируйте его по возрасту по возрастанию с помощью usort и стрелочной функции. Отсортируйте по имени в алфавитном порядке. Оба результата выведите через print_r.


Рефакторинг на чистую функцию:

Ниже приведён код с побочным эффектом:

php


function addTransaction(float $amount, string $category): void

{

global $transactions;

$transactions[] = ['amount' => $amount, 'category' => $category];

}


Перепишите функцию, чтобы она не использовала global, а принимала массив и возвращала новый. Убедитесь, что исходный массив не изменился.


Глава 4. ООП без боли


В которой мы перестаём писать классы ради классов, знакомимся с enum и readonly, и выясняем, почему наследование — не всегда лучший друг.

4.1. Зачем вообще классы в PHP?


PHP начинался без ООП. Потом добавили классы в PHP 4 (ужасно), переписали в PHP 5 (сносно), и с тех пор каждый релиз делает объектную модель стройнее.


Но зачем классы в мире, где есть функции и ассоциативные массивы? Ответ: инкапсуляция инвариантов.


Предположим, мы храним деньги как float $amount и string $currency. Что угодно может стать «деньгами»: ['amount' => 100, 'currency' => 'RUB']. А что, если кто-то напишет 'RUB' как 'rub' или 'RUR'? Что, если amount окажется отрицательным?


Класс — это способ сказать: «Вот структура. Вот правила, которые всегда соблюдаются. Снаружи нельзя создать невалидный объект».

php


class Money

{

public function __construct(

public readonly float $amount,

public readonly string $currency,

) {

if ($amount < 0) {

throw new \InvalidArgumentException('Сумма не может быть отрицательной');

}

if (!in_array($currency, ['RUB', 'USD', 'EUR'], true)) {

throw new \InvalidArgumentException("Неизвестная валюта: {$currency}");

}

}

}


$m = new Money(100.0, 'RUB'); // Ок.

$m = new Money(-50.0, 'RUB'); // Исключение.

$m = new Money(50.0, 'RUR'); // Исключение.


Теперь везде, где используется Money, мы знаем: сумма неотрицательна, валюта валидна. Никаких проверок на местах. Это и есть инкапсуляция — не просто «private поля с геттерами», а защита правил.

4.2. Конструктор в стиле PHP 8


С PHP 8.0 конструктор можно писать короче:

php


// Было (PHP 7):

class User

{

private string $name;

private int $age;


public function __construct(string $name, int $age)

{

$this->name = $name;

$this->age = $age;

}

}


// Стало (PHP 8):

class User

{

public function __construct(

private string $name,

private int $age,

) {}

}


Модификатор доступа (public, private, protected) прямо в параметре — и свойство создано, значение присвоено. Никакого бойлерплейта.


С readonly (PHP 8.1) — ещё чище:

php


class User

{

public function __construct(

public readonly string $name,

public readonly int $age,

) {}

}


Теперь $name и $age доступны снаружи для чтения, но не могут быть изменены. Идеально для DTO и value objects.

4.3. readonly-классы (PHP 8.2)


Если все свойства класса должны быть readonly, можно пометить весь класс:

php


readonly class Money

{

public function __construct(

public float $amount,

public string $currency,

) {

// Валидация здесь.

}

}


Все свойства автоматически readonly. Нельзя добавить обычное свойство. Нельзя использовать static-свойства (пока что ограничение движка). Объект целиком неизменяем после создания.


Когда использовать:


Value objects (Money, Email, DateRange).


DTO для передачи данных между слоями.


Всё, что не должно меняться после создания.


4.4. Enum: больше чем константы


До PHP 8.1 мы использовали константы:

php


class TransactionType

{

public const INCOME = 'income';

public const EXPENSE = 'expense';

}


function logTransaction(float $amount, string $type): void

{

// $type может быть чем угодно. 'income'? 'Income'? 'iNcOmE'?

}


Теперь — enum:

php


enum TransactionType: string

{

case Income = 'income';

case Expense = 'expense';

}


Типизированный enum. TransactionType::Income — это объект, который при приведении к строке даст 'income'. Но в сигнатуре метода он — гарантия:

php


function logTransaction(float $amount, TransactionType $type): void

{

// $type — ТОЛЬКО Income или Expense. Никаких строк.

}


logTransaction(100.0, TransactionType::Income);


Enum'ы могут иметь методы:

php


enum TransactionType: string

{

case Income = 'income';

case Expense = 'expense';


public function label(): string

{

return match($this) {

self::Income => 'Доход',

self::Expense => 'Расход',

};

}


public function isIncome(): bool

{

return $this === self::Income;

}

}


TransactionType::Income->label(); // 'Доход'


Чистый enum (без значения):

php


enum Status

{

case Draft;

case Published;

case Archived;

}


Без : string или : int. Просто перечисление состояний. Нельзя сравнить со строкой или числом — только с другим экземпляром. Идеально для статусов, ролей, состояний конечного автомата.


Метод tryFrom и from:

php


$type = TransactionType::from('income'); // Income

$type = TransactionType::from('invalid'); // ValueError


$type = TransactionType::tryFrom('invalid'); // null (без исключения)


4.5. Интерфейсы: контракты, не наследование


Интерфейс — это обещание: «я умею делать X». Без деталей реализации.

php


interface CalculatesArea

{

public function area(): float;

}


class Rectangle implements CalculatesArea

{

public function __construct(

private float $width,

private float $height,

) {}


public function area(): float

{

return $this->width * $this->height;

}

}


class Circle implements CalculatesArea

{

public function __construct(

private float $radius,

) {}


public function area(): float

{

return pi() * $this->radius ** 2;

}

}


function printArea(CalculatesArea $shape): void

{

echo "Площадь: " . $shape->area() . "\n";

}


printArea(new Rectangle(5, 3)); // 15

printArea(new Circle(2)); // ~12.57


Функция printArea не знает ничего о прямоугольниках и кругах. Она знает только контракт: у объекта есть метод area(): float. Это полиморфизм без наследования.

4.6. Композиция вместо наследования


Наследование часто учат как основу ООП. На практике оно легко приводит к хрупким иерархиям.


Классический пример проблемы:

php


class Bird

{

public function fly(): string

{

return 'Я лечу!';

}

}


class Penguin extends Bird

{

public function fly(): string

{

throw new \Exception('Пингвины не летают!');

}

}


Пингвин — птица, но нарушает контракт родителя. Код, ожидающий Bird, упадет на Penguin. Это нарушение принципа подстановки Лисков (LSP).


Выход — композиция:

php


interface FlyBehavior

{

public function fly(): string;

}


class FlyWithWings implements FlyBehavior

{

public function fly(): string

{

return 'Я лечу!';

}

}


class CantFly implements FlyBehavior

{

public function fly(): string

{

return 'Я не летаю.';

}

}


class Bird

{

public function __construct(

private readonly string $name,

private readonly FlyBehavior $flyBehavior,

) {}


public function tryToFly(): string

{

return "{$this->name}: " . $this->flyBehavior->fly();

}

}


$eagle = new Bird('Орёл', new FlyWithWings());

$penguin = new Bird('Пингвин', new CantFly());


$eagle->tryToFly(); // Орёл: Я лечу!

$penguin->tryToFly(); // Пингвин: Я не летаю.


Поведение вынесено в отдельный объект и внедряется через конструктор. Хотите добавить реактивный полёт? Новый класс JetFly. Не трогаем ни Bird, ни существующие поведения.


Это же — суть Dependency Injection (глава 9), но на уровне дизайна.

4.7. Трейты: осторожно, хрупкость


Трейты — способ подмешать поведение в класс, минуя наследование. Полезны, но опасны.


Хороший пример: Timestampable — добавляет поля createdAt и updatedAt с логикой.

php


trait Timestampable

{

private \DateTimeImmutable $createdAt;

private ?\DateTimeImmutable $updatedAt = null;


public function initializeTimestamps(): void

{

$this->createdAt = new \DateTimeImmutable();

}


public function touch(): void

{

$this->updatedAt = new \DateTimeImmutable();

}


public function getCreatedAt(): \DateTimeImmutable

{

return $this->createdAt;

}


public function getUpdatedAt(): ?\DateTimeImmutable

{

return $this->updatedAt;

}

}


Плохой пример: трейт, который неявно зависит от свойств или методов использующего класса:

php


trait BadTrait

{

public function doStuff(): void

{

$this->logger->log('Что-то делаю...'); // Откуда logger? Магия.

}

}


Такой трейт предполагает, что класс предоставит свойство $logger. Забудете — фатальная ошибка. Статический анализ не поможет, пока не запустите.


Правило: трейт должен быть самодостаточным или явно объявлять абстрактные методы, которые класс обязан реализовать. Лучше — композиция.

4.8. Собираем модель для менеджера финансов


Используем всё, что изучили:

php


enum TransactionType: string

{

case Income = 'income';

case Expense = 'expense';


public function sign(): int

{

return match($this) {

self::Income => 1,

self::Expense => -1,

};

}

}


readonly class Money

{

public function __construct(

public float $amount,

public string $currency,

) {

if ($amount < 0) {

throw new \InvalidArgumentException('Сумма не может быть отрицательной');

}

}


public function add(self $other): self

{

if ($this->currency !== $other->currency) {

throw new \InvalidArgumentException('Валюты не совпадают');

}

return new self($this->amount + $other->amount, $this->currency);

}

}


readonly class Transaction

{

public function __construct(

public Money $money,

public TransactionType $type,

public string $category,

public \DateTimeImmutable $date,

) {}


public function signedAmount(): float

{

return $this->money->amount * $this->type->sign();

}

}


Заметили?


Нельзя создать Money с отрицательной суммой.


Нельзя создать Transaction с невалидным типом.


signedAmount() зависит только от собственных свойств — чистая функция внутри объекта.


Никаких сеттеров. Объекты создаются через конструктор и не меняются.


Это — доменная модель в миниатюре. В следующих главах мы добавим репозитории, сервисы и контроллеры, но сердце приложения останется здесь — в строгих, неизменяемых объектах.

4.9. Практическое задание


Деньги не горят:

Добавьте в класс Money метод subtract(Money $other): Money. Проверьте, что нельзя вычесть больше, чем есть (выбросить исключение). Проверьте совпадение валют.


Enum для категорий:

Создайте enum Category: string с кейсами: Salary, Food, Transport, Entertainment, Other. Добавьте метод isEssential(): bool — обязательные траты (еда, транспорт) возвращают true.


Фигуры без наследования:

Реализуйте интерфейс Shape с методом area(): float. Создайте классы Square и Triangle, реализующие интерфейс. Напишите функцию totalArea(Shape ...$shapes): float, которая суммирует площади. Проверьте с разными фигурами.


Композиция в деле:

Создайте интерфейс NotificationChannel с методом send(string $message): void. Реализуйте EmailChannel и SmsChannel. Создайте класс Notifier, который принимает массив каналов в конструкторе и в методе notify(string $message) отправляет сообщение через все каналы. Проверьте с двумя разными наборами каналов.


Глава 5. База данных без магии


В которой мы подключаемся к SQLite одной строкой, навсегда забываем про SQL-инъекции и пишем Repository, который не стыдно показать коллегам.

5.1. Почему не ORM?


ORM (Doctrine, Eloquent) — отличные инструменты. Но прежде чем прятать SQL за объектами, нужно понять, что происходит на самом деле. Когда вы знаете PDO и чистый SQL, ORM становится удобной абстракцией, а не магическим чёрным ящиком, который иногда делает 500 запросов там, где нужен один.


В этой главе мы используем SQLite — базу данных, которая хранится в одном файле и не требует сервера. Для продакшена замените на PostgreSQL или MySQL, но PDO-интерфейс останется тем же.

5.2. Подключение через PDO


PDO (PHP Data Objects) — единый интерфейс для работы с разными базами данных. Подключаемся:

php


// Где хранить базу? В файле рядом с проектом.

$dsn = 'sqlite:' . __DIR__ . '/database.sqlite';


$pdo = new PDO($dsn);

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);


Три важные строчки:


ERRMODE_EXCEPTION — любая ошибка SQL превращается в исключение. Без этого PDO молча возвращает false, и мы гадаем, почему данных нет.


FETCH_ASSOC — результаты запросов возвращаем как ассоциативные массивы. Никаких дублирующихся числовых ключей.


DSN начинается с sqlite: — для MySQL было бы mysql:host=...;dbname=....


Проверка:

php


$pdo->exec('SELECT 1'); // Если не упало — подключение работает.


5.3. Создание таблиц: миграции на чистом SQL


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

php


$pdo->exec('

CREATE TABLE IF NOT EXISTS transactions (

id INTEGER PRIMARY KEY AUTOINCREMENT,

amount REAL NOT NULL,

currency TEXT NOT NULL,

type TEXT NOT NULL,

category TEXT NOT NULL,

date TEXT NOT NULL,

created_at TEXT NOT NULL DEFAULT (datetime(\'now\'))

)

');


Пояснения:


REAL для суммы (SQLite не различает float и double).


TEXT для даты — храним в ISO 8601 (2026-03-15), PHP легко парсит.


type и currency — текстовые коды. В реальном проекте можно сделать отдельные таблицы-справочники с внешними ключами, но для нашего менеджера финансов enum-подход из PHP надёжнее (контроль на уровне кода, а не БД).


created_at с дефолтным значением — метка времени создания записи.


Это простейшая миграция. В production-проектах используют инструменты вроде Phinx или Doctrine Migrations, но сейчас нам важна суть.

5.4. Подготовленные выражения: щит от инъекций


SQL-инъекция — до сих пор одна из главных уязвимостей веб-приложений. Злоумышленник передаёт '; DROP TABLE users; -- в поле ввода, и...

php


// НИКОГДА ТАК НЕ ДЕЛАЙТЕ:

$name = $_GET['name'];

$sql = "SELECT * FROM users WHERE name = '{$name}'";

// Если $name = "'; DROP TABLE users; --", будет больно.


Правильный способ — подготовленные выражения:

php


$name = $_GET['name'];

$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');

$stmt->execute(['name' => $name]);

$user = $stmt->fetch();


PDO отправляет SQL и данные отдельно. Движок базы данных компилирует запрос, а потом подставляет параметры как данные — без интерпретации как SQL-кода. Инъекция невозможна физически.


Именованные параметры vs позиционные:

php


// Именованные (рекомендую):

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');

$stmt->execute(['id' => $userId]);


// Позиционные (?):

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');

$stmt->execute([$userId]);


Именованные читаемее и не ломаются при добавлении новых параметров.


Вставка данных:

php


$stmt = $pdo->prepare('

INSERT INTO transactions (amount, currency, type, category, date)

VALUES (:amount, :currency, :type, :category, :date)

');


$stmt->execute([

'amount' => 1500.00,

'currency' => 'RUB',

'type' => 'income',

'category' => 'Зарплата',

'date' => '2026-03-15',

]);


$newId = (int) $pdo->lastInsertId(); // ID новой записи.

Конец ознакомительного фрагмента.

Текст предоставлен ООО «Литрес».

Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.

Конец ознакомительного фрагмента
Купить и скачать всю книгу
На страницу:
2 из 2