🐘 PHP

PHP Orientado a Objetos

ООП в PHP: классы, трейты и интерфейсы для новичков

Объектно-ориентированное программирование — способ организации кода вокруг объектов. Объект — как ученик: есть характеристики (имя, возраст) и он умеет делать вещи (учиться, сдавать тесты).

Класс и объект

<?php
class Student {
    public string $name;
    public int    $age;
    public string $grade;
    private float $gpa;

    public function __construct(string $name, int $age, string $grade, float $gpa) {
        $this->name  = $name;
        $this->age   = $age;
        $this->grade = $grade;
        $this->gpa   = $gpa;
    }

    public function introduce(): string {
        return "Привет, я {$this->name} из класса {$this->grade}.";
    }

    public function getGpa(): float { return $this->gpa; }

    public function setGpa(float $gpa): void {
        if ($gpa < 0 || $gpa > 5) {
            throw new InvalidArgumentException("GPA должен быть 0-5");
        }
        $this->gpa = $gpa;
    }
}

$alice = new Student("Алиса", 12, "6А", 4.8);
echo $alice->introduce() . "\n";
echo "GPA: " . $alice->getGpa() . "\n";
$alice->setGpa(5.0);
echo "Новый GPA: " . $alice->getGpa() . "\n";
echo get_class($alice) . "\n";         // Student
echo ($alice instanceof Student) ? "Да\n" : "Нет\n";
?>

Visibility: public, protected, private

<?php
class BankAccount {
    public string  $owner;      // везде
    protected float $balance;   // класс + подклассы
    private string  $pin;       // только в этом классе

    public function __construct(string $owner, float $initial, string $pin) {
        $this->owner   = $owner;
        $this->balance = $initial;
        $this->pin     = $pin;
    }

    public function deposit(float $amount): void {
        if ($amount <= 0) throw new InvalidArgumentException("Сумма > 0");
        $this->balance += $amount;
        echo "Пополнено {$amount}. Баланс: {$this->balance}\n";
    }

    public function withdraw(float $amount, string $pin): bool {
        if ($pin !== $this->pin)    { echo "Неверный PIN\n"; return false; }
        if ($amount > $this->balance) { echo "Недостаточно средств\n"; return false; }
        $this->balance -= $amount;
        echo "Снято {$amount}. Баланс: {$this->balance}\n";
        return true;
    }

    public function getBalance(): float { return $this->balance; }
}

$acc = new BankAccount("Алиса", 1000.0, "1234");
$acc->deposit(500);
$acc->withdraw(200, "1234");  // Успех
$acc->withdraw(200, "0000");  // Неверный PIN
?>

Наследование

<?php
class Animal {
    protected string $name;
    public function __construct(string $name) { $this->name = $name; }
    public function speak(): string { return "..."; }
    public function describe(): string { return $this->name; }
}

class Dog extends Animal {
    private string $breed;
    public function __construct(string $name, string $breed) {
        parent::__construct($name);
        $this->breed = $breed;
    }
    public function speak(): string { return "Гав!"; }
    public function fetch(): string { return "{$this->name} принёс мячик!"; }
    public function describe(): string {
        return parent::describe() . " ({$this->breed})";
    }
}

class Cat extends Animal {
    public function speak(): string { return "Мяу!"; }
    public function purr(): string { return "{$this->name} мурлычет..."; }
}

$dog = new Dog("Шарик", "Лабрадор");
$cat = new Cat("Мурка");

echo $dog->describe() . ": " . $dog->speak() . "\n";
echo $cat->describe() . ": " . $cat->speak() . "\n";
echo $dog->fetch() . "\n";
echo $cat->purr() . "\n";

// Полиморфизм
$animals = [$dog, $cat, new Dog("Бобик", "Дворняга")];
foreach ($animals as $a) {
    echo "{$a->describe()}: " . $a->speak() . "\n";
}
?>

Static, Abstract, Interfaces

<?php
// Static — принадлежит классу, не объекту
class Counter {
    private static int $count = 0;
    public function __construct() { self::$count++; }
    public static function getCount(): int { return self::$count; }
}
new Counter(); new Counter(); new Counter();
echo Counter::getCount() . "\n";  // 3

// Abstract — нельзя создать экземпляр
abstract class Shape {
    abstract public function area(): float;
    public function describe(): string {
        return get_class($this) . ": S=" . number_format($this->area(), 2);
    }
}

class Circle extends Shape {
    public function __construct(private float $r) {}
    public function area(): float { return M_PI * $this->r ** 2; }
}

class Rectangle extends Shape {
    public function __construct(private float $w, private float $h) {}
    public function area(): float { return $this->w * $this->h; }
}

$shapes = [new Circle(5.0), new Rectangle(4.0, 6.0)];
foreach ($shapes as $s) echo $s->describe() . "\n";

// Interface — контракт без реализации
interface Serializable2 {
    public function serialize2(): string;
}

interface Cacheable {
    public function getCacheKey(): string;
    public function getTtl(): int;
}

// Класс реализует несколько интерфейсов!
class UserCache implements Serializable2, Cacheable {
    public function __construct(private string $name) {}
    public function serialize2(): string { return json_encode(["name" => $this->name]); }
    public function getCacheKey(): string { return "user_{$this->name}"; }
    public function getTtl(): int { return 3600; }
}

$uc = new UserCache("Алиса");
echo $uc->serialize2() . "\n";
echo $uc->getCacheKey() . "\n";
?>

Traits — множественное наследование

<?php
trait Timestampable {
    private ?DateTime $created_at = null;
    public function setCreatedAt(): void { $this->created_at = new DateTime(); }
    public function getCreatedAt(): ?string {
        return $this->created_at?->format("Y-m-d H:i:s");
    }
}

trait SoftDeletable {
    private bool $deleted = false;
    public function softDelete(): void { $this->deleted = true; }
    public function restore(): void   { $this->deleted = false; }
    public function isDeleted(): bool { return $this->deleted; }
}

trait Validatable {
    private array $errors = [];
    public function addError(string $f, string $m): void { $this->errors[$f][] = $m; }
    public function isValid(): bool  { return empty($this->errors); }
    public function getErrors(): array { return $this->errors; }
}

class Post {
    use Timestampable, SoftDeletable, Validatable;  // несколько трейтов!

    public function __construct(public string $title, public string $content) {
        $this->setCreatedAt();
        if (empty($title)) $this->addError("title", "Заголовок обязателен");
        if (strlen($content) < 10) $this->addError("content", "Слишком короткий");
    }
}

$post = new Post("Привет PHP!", "Это мой первый пост о PHP!");
echo "Создан: " . $post->getCreatedAt() . "\n";
echo "Валидный: " . ($post->isValid() ? "Да" : "Нет") . "\n";

$post->softDelete();
echo "Удалён: " . ($post->isDeleted() ? "Да" : "Нет") . "\n";

$post->restore();
echo "Восстановлен!\n";
?>

Readonly и Constructor Promotion (PHP 8)

<?php
// Constructor Promotion — краткий синтаксис
class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
        public string $label = "P",
    ) {}

    public function distance(Point $other): float {
        return sqrt(($this->x - $other->x)**2 + ($this->y - $other->y)**2);
    }
}

$p1 = new Point(0, 0, "O");
$p2 = new Point(3, 4, "A");
echo "Расстояние: " . $p1->distance($p2) . "\n";  // 5.0
// $p1->x = 5;  // Error: readonly!

// Enum (PHP 8.1) — перечисления
enum Status {
    case Active;
    case Inactive;
    case Pending;
}

enum Color: string {
    case Red   = 'red';
    case Green = 'green';
    case Blue  = 'blue';
}

$s = Status::Active;
echo match($s) {
    Status::Active   => "Активен",
    Status::Inactive => "Неактивен",
    Status::Pending  => "Ожидает",
} . "\n";

echo Color::Red->value . "\n";  // red
?>

Composer — менеджер пакетов

# Установка
php -r "copy('https://getcomposer.org/installer', 'c.php');"
php c.php

# Создание проекта
composer init

# Установка пакетов
composer require monolog/monolog

# composer.json:
# {
#   "require": { "monolog/monolog": "^3.0" },
#   "autoload": { "psr-4": { "App\\": "src/" } }
# }
<?php
require __DIR__ . '/vendor/autoload.php';  // автозагрузка классов

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::DEBUG));
$log->info('Запуск приложения');
$log->error('Что-то пошло не так!');
?>

Типичные ошибки

❌ Забыл $this->

class Foo {
    public string $name;
    public function greet() {
        echo $name;         // Notice: Undefined variable
        echo $this->name;   // ✅
    }
}

❌ Обращение к private в подклассе

class A { private $x = 1; }
class B extends A {
    public function show() { echo $this->x; }  // Fatal Error!
}
// Используй protected вместо private

Итого

Классы: свойства + методы. $this — текущий объект. __construct — конструктор. public/protected/private. Наследование: extends, parent::. Abstract: нельзя создать экземпляр, обязывает реализовать методы. Interfaces: контракт, класс реализует несколько. Traits: переиспользуемый код, множественное наследование. Static: принадлежит классу. Readonly (PHP 8.1): нельзя изменить после создания. Enum (PHP 8.1): перечисления. Composer: менеджер пакетов, автозагрузка.

Дополнительные примеры и задачи

Пример 1: Работа с датами и временем

<?php
// PHP имеет богатые встроенные функции для работы с датами
$now = new DateTime();
echo $now->format('d.m.Y H:i:s') . "\n";

// Создание конкретной даты
$birthday = new DateTime('2012-03-15');
$today = new DateTime();
$diff = $today->diff($birthday);
echo "Прошло {$diff->y} лет {$diff->m} месяцев {$diff->d} дней\n";

// Добавление/вычитание времени
$next_week = new DateTime();
$next_week->modify('+7 days');
echo "Через неделю: " . $next_week->format('d.m.Y') . "\n";

$past = new DateTime();
$past->modify('-30 days');
echo "Месяц назад: " . $past->format('d.m.Y') . "\n";

// Форматы даты
$date = new DateTime('2024-01-15 14:30:00');
echo $date->format('d MMMM Y') . "\n";  // нет: это ICU формат
echo $date->format('j F Y') . "\n";      // 15 January 2024
echo $date->format('U') . "\n";          // Unix timestamp

// strtotime — удобное создание дат
$ts = strtotime('next Monday');
echo date('d.m.Y', $ts) . "\n";
$ts2 = strtotime('+2 months');
echo date('d.m.Y', $ts2) . "\n";
?>

Пример 2: Работа с файлами

<?php
// Запись в файл
$data = "Привет, PHP!\nВторая строка\n";
file_put_contents('/tmp/test.txt', $data);
echo "Записано: " . strlen($data) . " байт\n";

// Добавление к файлу
file_put_contents('/tmp/test.txt', "Третья строка\n", FILE_APPEND);

// Чтение файла
$content = file_get_contents('/tmp/test.txt');
echo $content;

// Чтение построчно
$lines = file('/tmp/test.txt', FILE_IGNORE_NEW_LINES);
foreach ($lines as $i => $line) {
    echo "$i: $line\n";
}

// Проверка существования
if (file_exists('/tmp/test.txt')) {
    echo "Файл существует\n";
    echo "Размер: " . filesize('/tmp/test.txt') . " байт\n";
}

// Удаление
unlink('/tmp/test.txt');
echo "Файл удалён\n";
?>

Пример 3: Регулярные выражения

<?php
// Валидация email
function validateEmail(string $email): bool {
    return (bool) preg_match('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $email);
}

$emails = ["user@example.com", "invalid-email", "test@test.ru", "no_at_sign"];
foreach ($emails as $email) {
    echo "$email: " . (validateEmail($email) ? "✅" : "❌") . "\n";
}

// Извлечение данных
$text = "Телефоны: +7(495)123-45-67 и 8-800-555-35-35";
preg_match_all('/[+7|8][\d\s\-()]+/', $text, $matches);
print_r($matches[0]);

// Замена
$html = "<p>Hello <b>World</b></p>";
$clean = preg_replace('/<[^>]+>/', '', $html);
echo $clean . "\n";  // Hello World

// Разбивка по паттерну
$str = "яблоко,  банан;вишня  дыня";
$fruits = preg_split('/[,;\s]+/', trim($str));
print_r(array_filter($fruits));
?>

Практическое задание: калькулятор оценок

<?php
class GradeCalculator {
    private array $subjects = [];

    public function addGrade(string $subject, float $grade): void {
        if ($grade < 1 || $grade > 5) {
            throw new InvalidArgumentException("Оценка должна быть от 1 до 5");
        }
        $this->subjects[$subject][] = $grade;
    }

    public function getAverage(string $subject): float {
        if (!isset($this->subjects[$subject])) {
            throw new RuntimeException("Предмет не найден: $subject");
        }
        return array_sum($this->subjects[$subject]) / count($this->subjects[$subject]);
    }

    public function getOverallAverage(): float {
        $all = [];
        foreach ($this->subjects as $grades) {
            $all = array_merge($all, $grades);
        }
        return empty($all) ? 0.0 : array_sum($all) / count($all);
    }

    public function getReport(): string {
        $lines = ["=== Отчёт об успеваемости ==="];
        foreach ($this->subjects as $subject => $grades) {
            $avg = $this->getAverage($subject);
            $lines[] = sprintf("%-15s: %.2f (оценок: %d)", $subject, $avg, count($grades));
        }
        $lines[] = str_repeat("-", 35);
        $lines[] = sprintf("%-15s: %.2f", "ИТОГО", $this->getOverallAverage());
        return implode("\n", $lines);
    }
}

$calc = new GradeCalculator();
$calc->addGrade("Математика", 5);
$calc->addGrade("Математика", 4);
$calc->addGrade("Математика", 5);
$calc->addGrade("Русский язык", 4);
$calc->addGrade("Русский язык", 5);
$calc->addGrade("Английский", 3);
$calc->addGrade("Английский", 4);

echo $calc->getReport() . "\n";
?>

Полезные PHP-паттерны

Паттерн Repository (хранилище данных)

<?php
interface UserRepositoryInterface {
    public function findById(int $id): ?array;
    public function findAll(): array;
    public function save(array $user): int;
    public function delete(int $id): bool;
}

class InMemoryUserRepository implements UserRepositoryInterface {
    private array $users = [];
    private int $nextId = 1;

    public function findById(int $id): ?array {
        return $this->users[$id] ?? null;
    }

    public function findAll(): array {
        return array_values($this->users);
    }

    public function save(array $user): int {
        $id = $user['id'] ?? $this->nextId++;
        $this->users[$id] = array_merge($user, ['id' => $id]);
        return $id;
    }

    public function delete(int $id): bool {
        if (!isset($this->users[$id])) return false;
        unset($this->users[$id]);
        return true;
    }
}

$repo = new InMemoryUserRepository();
$id1 = $repo->save(['name' => 'Алиса', 'email' => 'alice@test.com']);
$id2 = $repo->save(['name' => 'Боря',  'email' => 'bob@test.com']);

print_r($repo->findAll());
print_r($repo->findById($id1));

$repo->delete($id2);
echo "Осталось: " . count($repo->findAll()) . "\n";
?>