40 вопросов
PHP поддерживает классическое ООП на основе классов с PHP 5+:
extends), множественное через трейтыС PHP 8.0+ появились: именованные аргументы, enum, readonly свойства, типизированные свойства, конструктор property promotion.
Класс - описание (blueprint): свойства, методы, константы. Объект - конкретный экземпляр класса в памяти.
class Car { // Класс
public string $color;
public function drive() {}
}
$myCar = new Car(); // Объект (экземпляр)
$myCar->color = 'red';
$yourCar = new Car(); // Другой объект того же класса
$yourCar->color = 'blue';Класс существует один раз в коде. Объектов одного класса может быть сколько угодно, каждый со своими значениями свойств.
class User {
public string $name; // доступен отовсюду
protected int $age; // доступен в классе и наследниках
private string $password; // доступен только в этом классе
public function getName(): string { return $this->name; }
protected function validate(): bool { /* ... */ }
private function hashPassword(): string { /* ... */ }
}С PHP 8.4 появилась асимметричная видимость:
class User {
public private(set) string $name; // чтение public, запись private
}Рекомендация: делайте все private по умолчанию, расширяя видимость по необходимости.
Интерфейс - контракт, описывающий набор методов без реализации:
interface Cacheable {
public function getCacheKey(): string;
public function getCacheTTL(): int;
}
class User implements Cacheable {
public function getCacheKey(): string { return "user:{$this->id}"; }
public function getCacheTTL(): int { return 3600; }
}Различия от абстрактных классов:
Да, абстрактный класс может содержать private обычные (не абстрактные) методы:
abstract class Base {
private function helper(): string {
return 'internal logic';
}
public function process(): string {
return $this->helper();
}
abstract public function execute(): void;
}Однако private абстрактный метод не имеет смысла и вызовет ошибку - наследник не сможет его переопределить. Абстрактные методы должны быть public или protected.
Да, абстрактный класс может наследовать другой абстрактный класс:
abstract class Animal {
abstract public function speak(): string;
public function breathe(): string { return 'breathing'; }
}
abstract class Pet extends Animal {
abstract public function getName(): string;
// speak() все еще абстрактный
}
class Dog extends Pet {
public function speak(): string { return 'Woof!'; }
public function getName(): string { return 'Rex'; }
}Промежуточный абстрактный класс может реализовать часть абстрактных методов и добавить новые.
Трейты - механизм повторного использования кода без наследования (горизонтальное переиспользование):
trait Timestampable {
public ?DateTime $createdAt = null;
public function setCreatedAt(): void {
$this->createdAt = new DateTime();
}
}
trait SoftDeletable {
public ?DateTime $deletedAt = null;
public function softDelete(): void {
$this->deletedAt = new DateTime();
}
}
class User {
use Timestampable, SoftDeletable;
}Трейты не нарушают SOLID если используются для переиспользования инфраструктурного кода. Проблемы: неявные зависимости, конфликты имен, сложность отладки.
trait A {
public function hello(): string { return 'A'; }
}
trait B {
public function hello(): string { return 'B'; }
}
class MyClass {
use A, B {
A::hello insteadof B; // использовать метод из A
B::hello as helloB; // метод B доступен как helloB
}
}
$obj = new MyClass();
echo $obj->hello(); // "A"
echo $obj->helloB(); // "B"Без разрешения конфликта PHP выдаст фатальную ошибку. as также позволяет менять видимость: A::hello as private;
// final класс - нельзя наследовать
final class Singleton {
private static ?self $instance = null;
public static function getInstance(): self {
return self::$instance ??= new self();
}
}
// class Child extends Singleton {} // Fatal error!
// final метод - нельзя переопределять в наследниках
class Base {
final public function critical(): void { /* ... */ }
}
class Child extends Base {
// public function critical(): void {} // Fatal error!
}Используйте final когда наследование может сломать логику. В DDD часто Entity и Value Object делают final.
class ParentClass {
public static function create(): static {
return new static(); // Late Static Binding
}
public static function createSelf(): self {
return new self(); // всегда ParentClass
}
}
class ChildClass extends ParentClass {}
$obj = ChildClass::create(); // ChildClass
$obj = ChildClass::createSelf(); // ParentClass!self ссылается на класс, в котором написан код (compile-time). static ссылается на класс, который вызвал метод (runtime). $this - ссылка на текущий объект (только в нестатическом контексте).
self:: привязано к классу, в котором записан код. static:: привязано к классу, который вызвал метод (Late Static Binding, PHP 5.3+).
class Model {
protected static string $table = 'models';
public static function getTable(): string {
return static::$table; // берет $table из вызывающего класса
}
}
class User extends Model {
protected static string $table = 'users';
}
echo User::getTable(); // "users" (static::)
// Если бы было self::$table - вернул бы "models"LSB критически важен для паттернов Active Record, Factory Method.
$this - ссылка на текущий объект (экземпляр класса). Доступен только в нестатическом контексте. self - ссылка на текущий класс. Доступен в статическом и нестатическом контексте.
class Counter {
private int $count = 0;
private static int $total = 0;
public function increment(): void {
$this->count++; // свойство объекта
self::$total++; // статическое свойство класса
}
public static function getTotal(): int {
// $this не доступен здесь!
return self::$total;
}
}LSB позволяет ссылаться на класс, который реально вызвал статический метод, а не на класс, в котором метод определен:
class Base {
public static function create(): static {
return new static(); // класс вызова, не определения
}
}
class User extends Base {}
class Admin extends Base {}
$user = User::create(); // User, а не Base
$admin = Admin::create(); // Admin, а не BaseКлючевое слово static (вместо self) реализует LSB. Это основа для паттернов Active Record (Eloquent), Factory Method, и любых иерархий с наследуемым поведением.
Магические методы начинаются с __ и вызываются при определенных событиях:
__construct() / __destruct() - создание/уничтожение объекта__get($name) / __set($name, $val) - доступ к несуществующим/private свойствам__call($name, $args) / __callStatic() - вызов несуществующих методов__toString() - преобразование в строку__invoke() - вызов объекта как функции__clone() - клонирование объекта__sleep() / __wakeup() - сериализация/десериализация__serialize() / __unserialize() - PHP 7.4+, замена sleep/wakeup__debugInfo() - вывод var_dump__isset($name) / __unset($name) - isset/unset на свойствахclass Config {
private array $data = [];
// Вызывается при записи в несуществующее/недоступное свойство
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
// Вызывается при чтении несуществующего/недоступного свойства
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
}
$cfg = new Config();
$cfg->host = 'localhost'; // __set()
echo $cfg->host; // __get()Используются для: dynamic properties, прокси-объектов, ORM. Злоупотребление усложняет отладку и IDE-подсказки.
__invoke() позволяет вызывать объект как функцию:
class Multiplier {
public function __construct(private int $factor) {}
public function __invoke(int $value): int {
return $value * $this->factor;
}
}
$double = new Multiplier(2);
echo $double(5); // 10
echo $double(10); // 20
// Можно передавать как callable
$result = array_map(new Multiplier(3), [1, 2, 3]); // [3, 6, 9]Паттерн Single Action Class - класс с единственным действием: валидаторы, конвертеры, pipeline-шаги.
class Order {
public array $items = [];
public Address $address;
public function __clone() {
// Без __clone: $address будет общим (shallow copy)
$this->address = clone $this->address; // deep copy
// Массив скаляров копируется автоматически
}
}
$order1 = new Order();
$order2 = clone $order1; // вызывает __clone()Shallow copy (по умолчанию) - копирует примитивы, но объекты и ресурсы остаются общими. Deep copy - нужно реализовать через __clone(), клонируя вложенные объекты.
// Старый способ (PHP < 7.4)
class OldStyle {
public function __sleep(): array {
return ['name', 'age']; // какие свойства сериализовать
}
public function __wakeup(): void {
$this->connection = new DB(); // восстановить ресурсы
}
}
// Новый способ (PHP 7.4+)
class NewStyle {
public function __serialize(): array {
return ['name' => $this->name, 'age' => $this->age];
}
public function __unserialize(array $data): void {
$this->name = $data['name'];
$this->age = $data['age'];
}
}
$serialized = serialize($obj);
$obj = unserialize($serialized);Новые методы дают полный контроль над форматом данных. Для API используйте JSON, не serialize().
class Connection {
private $resource;
public function __construct(string $dsn) {
$this->resource = pg_connect($dsn);
}
public function __destruct() {
if ($this->resource) {
pg_close($this->resource);
}
}
}
// PHP 8.0: Constructor Property Promotion
class User {
public function __construct(
public readonly string $name,
public readonly int $age,
) {}
}Деструктор вызывается при уничтожении объекта (когда refcount = 0 или при завершении скрипта). Порядок вызова деструкторов не гарантирован.
В PHP нет прямого аналога Go embedding. Ближайший механизм - трейты и делегирование:
// Трейт - встраивание поведения
trait HasTimestamps {
public DateTime $createdAt;
public DateTime $updatedAt;
}
class User {
use HasTimestamps; // "встраивает" свойства и методы
}
// Делегирование (composition)
class UserService {
public function __construct(
private UserRepository $repo, // композиция
) {}
public function find(int $id): User {
return $this->repo->find($id); // делегируем
}
}В Go embedding автоматически проксирует методы; в PHP нужно явно делегировать или использовать __call().
Полиморфизм - способность объектов разных классов обрабатывать одно и то же сообщение по-своему:
interface NotificationChannel {
public function send(string $message): void;
}
class EmailChannel implements NotificationChannel {
public function send(string $message): void {
mail($this->to, 'Notification', $message);
}
}
class SlackChannel implements NotificationChannel {
public function send(string $message): void {
$this->api->post($this->webhook, $message);
}
}
// Полиморфный вызов - код не знает конкретный тип
function notify(NotificationChannel $channel, string $msg): void {
$channel->send($msg); // разное поведение
}Реализуется через интерфейсы (наиболее правильно) и наследование с переопределением методов.
Инкапсуляция - не просто сокрытие данных (это лишь один аспект). Это объединение данных и методов работы с ними в единый блок с контролируемым доступом:
class BankAccount {
private float $balance = 0;
public function deposit(float $amount): void {
if ($amount <= 0) throw new InvalidArgumentException();
$this->balance += $amount;
}
public function withdraw(float $amount): void {
if ($amount > $this->balance) throw new InsufficientFundsException();
$this->balance -= $amount;
}
public function getBalance(): float {
return $this->balance;
}
}Ключевой смысл: класс гарантирует свои инварианты. Нельзя установить отрицательный баланс, потому что доступ к $balance только через методы с валидацией.
Проблемы наследования:
Композиция - объект содержит другие объекты и делегирует им работу:
// Плохо: наследование
class ElectricCar extends Car { /* ... */ }
// Хорошо: композиция
class Car {
public function __construct(
private Engine $engine,
private Transmission $transmission,
) {}
}Правило: используйте наследование для отношения "является" (is-a), композицию для "содержит" (has-a).
// Анонимный класс - одноразовый класс без имени
$logger = new class implements LoggerInterface {
public function log(string $msg): void {
echo $msg;
}
};
// Полезно в тестах для создания mock/stub
$mock = new class extends AbstractRepository {
public function find(int $id): ?Entity {
return new Entity(id: $id, name: 'test');
}
};
// Можно передавать зависимости через конструктор
$handler = new class($db) {
public function __construct(private DB $db) {}
public function handle(): void { /* ... */ }
};Используются для quick implementations, тестов, одноразовых стратегий.
// Unit enum - без скалярных значений
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
// Backed enum - с привязкой к int или string
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
// Могут содержать методы
public function label(): string {
return match($this) {
self::Active => 'Активен',
self::Inactive => 'Неактивен',
};
}
}
// Backed enum: from/tryFrom
$s = Status::from('active'); // Status::Active
$s = Status::tryFrom('unknown'); // null
$s = Status::Active->value; // 'active'
Status::cases(); // все значения// readonly свойство (PHP 8.1)
class User {
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
$user = new User('John', 'john@example.com');
$user->name = 'Jane'; // Error: Cannot modify readonly property
// readonly класс (PHP 8.2) - все свойства readonly
readonly class Point {
public function __construct(
public float $x,
public float $y,
) {}
}readonly свойство можно установить только один раз (обычно в конструкторе). Нельзя unset. Нельзя изменять после инициализации (даже из самого класса). В PHP 8.3 readonly свойства можно клонировать с изменением.
class User {
// Виртуальное свойство с хуками get/set
public string $fullName {
get => "$this->firstName $this->lastName";
set(string $value) {
[$this->firstName, $this->lastName] = explode(' ', $value, 2);
}
}
// Хук только для set (валидация)
public string $email {
set(string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException();
}
$this->email = $value;
}
}
}Property hooks заменяют getters/setters и делают API чище. Это аналог C# properties. Свойства с хуками работают с readonly, интерфейсами и абстрактными классами.
class User {
// Чтение публичное, запись приватная
public private(set) string $name;
// Чтение публичное, запись защищенная (для наследников)
public protected(set) int $age;
public function __construct(string $name, int $age) {
$this->name = $name; // OK - внутри класса
$this->age = $age;
}
}
$user = new User('John', 30);
echo $user->name; // OK
$user->name = 'Jane'; // Error: Cannot modifyУровень видимости для set не может быть шире, чем для get. Это делает ненужными многие геттеры.
// До PHP 8.0
class User {
public string $name;
public int $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
}
// PHP 8.0+ - в 3 раза короче
class User {
public function __construct(
public string $name,
public int $age,
) {}
}
// Можно комбинировать с readonly (PHP 8.1)
class User {
public function __construct(
public readonly string $name,
public readonly int $age,
) {}
}Модификатор видимости перед параметром конструктора автоматически создает свойство и присваивает значение.
// Value Object - неизменяемый объект, сравнивается по значению
readonly class Money {
public function __construct(
public int $amount, // в копейках
public string $currency,
) {
if ($amount < 0) throw new InvalidArgumentException('Negative amount');
}
public function add(Money $other): self {
if ($this->currency !== $other->currency) {
throw new DomainException('Currency mismatch');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool {
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}Ключевые свойства VO: иммутабельность, сравнение по значению (не по ссылке), самовалидация.
DTO - объект для передачи данных между слоями приложения без бизнес-логики:
readonly class CreateUserDTO {
public function __construct(
public string $name,
public string $email,
public int $age,
) {}
public static function fromRequest(Request $request): self {
return new self(
name: $request->input('name'),
email: $request->input('email'),
age: (int) $request->input('age'),
);
}
}
// Использование
class UserService {
public function create(CreateUserDTO $dto): User {
return $this->repo->create($dto->name, $dto->email, $dto->age);
}
}DTO отличается от Entity (нет бизнес-логики, нет identity) и от Value Object (не обязан быть иммутабельным, нет поведения).
readonly class Email {
public function __construct(public string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}
// Методы возвращают НОВЫЙ объект
public function withDomain(string $domain): self {
$local = explode('@', $this->value)[0];
return new self("$local@$domain");
}
}
$email = new Email('john@old.com');
$new = $email->withDomain('new.com');
// $email не изменился, $new - новый объектПравила иммутабельности: readonly свойства, методы-модификаторы возвращают new self(...), нет сеттеров.
Ковариантность (PHP 7.4+) - возвращаемый тип в наследнике может быть более конкретным:
interface Factory {
public function create(): Animal;
}
class DogFactory implements Factory {
public function create(): Dog { /* ... */ } // Dog extends Animal - OK
}Контравариантность (PHP 7.4+) - тип параметра в наследнике может быть более общим:
interface Handler {
public function handle(Dog $animal): void;
}
class AnimalHandler implements Handler {
public function handle(Animal $animal): void { /* ... */ } // Animal <- Dog - OK
}Это следствие принципа подстановки Лисков (LSP): наследник должен быть безопасной заменой родителя.
В PHP этой проблемы нет - объекты всегда передаются по идентификатору (handle), аналогично указателям в Go.
В Go value receiver (func (v Value) Method()) получает копию, pointer receiver (func (p *Value) Method()) - ссылку. Интерфейс, требующий pointer receiver, не может быть удовлетворен value.
В PHP все объекты ведут себя как Go pointer receivers - методы всегда работают с оригинальным объектом. Клонирование нужно делать явно через clone.
class Secret {
private string $password = 'hidden';
}
// Через Reflection API
$obj = new Secret();
$ref = new ReflectionClass($obj);
$prop = $ref->getProperty('password');
$prop->setAccessible(true); // PHP < 8.1 требовался
echo $prop->getValue($obj); // "hidden"
// С PHP 8.1 setAccessible() не нужен
$prop = (new ReflectionClass($obj))->getProperty('password');
echo $prop->getValue($obj); // "hidden"
// Через Closure::bind (хитрый способ)
$getter = Closure::bind(fn() => $this->password, $obj, Secret::class);
echo $getter(); // "hidden"Reflection используется в DI-контейнерах, ORM, сериализаторах, тестовых фреймворках.
Reflection API - механизм интроспекции: анализ классов, методов, свойств, параметров в runtime:
$ref = new ReflectionClass(User::class);
$ref->getMethods(); // все методы
$ref->getProperties(); // все свойства
$ref->getConstructor()->getParameters(); // параметры конструктора
$ref->getAttributes(); // атрибуты (PHP 8.0)
$ref->isAbstract();
$ref->isFinal();
$ref->implementsInterface(Serializable::class);
// ReflectionMethod
$method = new ReflectionMethod(User::class, 'getName');
$method->getReturnType();
$method->invoke($obj);Применение: DI-контейнеры (autowiring), ORM (маппинг), сериализаторы, генераторы документации, тестовые фреймворки.
Нет. Все методы интерфейса неявно public. Интерфейс описывает публичный контракт - что класс обещает внешнему миру. Private методы - это детали реализации, им не место в контракте.
interface UserRepositoryInterface {
public function find(int $id): ?User; // OK
// private function validate(): bool; // Fatal error!
}Нет, нельзя. С PHP 8.0 интерфейсы не могут содержать статические методы (deprecated в PHP 8.0, fatal error в будущих версиях).
Причина: статические методы не являются частью полиморфного контракта. Нельзя вызвать $interface::staticMethod() через переменную типа интерфейса.
Альтернативы: фабрика как обычный метод, именованные конструкторы как обычные методы, абстрактный класс со статическими методами.
// PHP 8.0: интерфейс Stringable
interface Stringable {
public function __toString(): string;
}
// Любой класс с __toString() автоматически реализует Stringable
class Name {
public function __construct(private string $value) {}
public function __toString(): string { return $this->value; }
}
// Type hint
function greet(string|Stringable $name): string {
return "Hello, $name";
}
greet('World'); // OK
greet(new Name('PHP')); // OKStringable реализуется неявно - не нужно писать implements Stringable. Полезен для type hint когда функция принимает строку или объект, приводимый к строке.
В PHP alignment и padding не актуальны на уровне пользовательского кода - PHP абстрагирует управление памятью через zval-контейнеры.
В Go и C struct padding добавляется компилятором для выравнивания полей по границам слова процессора (4/8 байт). Неправильный порядок полей увеличивает размер структуры.
В PHP это имеет значение только при написании C-расширений (php-src) или при работе с pack()/unpack() для бинарных протоколов.