Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о замене реализации и говорить не приходилось. Мысль о том, чтобы написать нормальные юнит-тесты, вызывала нервный смех. Знакомая ситуация? Внедрение зависимостей (Dependency Injection, DI) – тот самый инструмент, который помогает разрубить этот гордиев узел.
В сути DI нет ничего сверхъестественного: вместо создания зависимостей внутри класса, мы передаём их извне. Простая идея, которая радикально меняет архитектуру приложения. Она переворачивает с ног на голову классический подход к проектированию, когда объект сам решает, какие конкретные реализации других объектов ему нужны.
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Без DI - жосткая связанность
class UserService {
private $repository;
public function __construct() {
$this->repository = new MySqlUserRepository(); // Жесткая зависимость
}
}
// С DI - гибкая архитектура
class UserService {
private $repository;
public function __construct(UserRepositoryInterface $repository) {
$this->repository = $repository; // Зависимость внедрена извне
}
} |
|
DI становится особенно мощным инструментом в контексте SOLID-принципов. Внедрение через конструктор помогает соблюдать принцип единой ответсвенности (SRP), поскольку класс больше не отвечает за создание своих зависимостей. Принцип открытости/закрытости (OCP) реализуется через возможность легко расширять функциональность без модификации существующего кода. Но самая красивая связь — с принципом инверсии зависимостей (DIP): высокоуровневые модули зависят от абстракций, а не от конкретных низкоуровневых модулей.
Немного теории. В PHP существует три основных способа внедрения зависимостей:
1. Внедрение через конструктор – самый распространенный способ, когда зависимости передаются в конструктор класса.
2. Внедрение через метод (сеттер) – зависимости устанавливаются через отдельные методы.
3. Внедрение через свойства – менее распространённый подход, зависимости устанавливаются напрямую в свойства класса.
Для меня внедрение через конструктор почти всегда предпочтительно — оно гарантирует, что объект всегда находится в валидном состоянии после создания. Сеттеры хороши для опциональных зависимостей или когда зависимость может менятся в течение жизни объекта.
Реальную силу DI в PHP-проектах раскрывают контейнеры зависимостей — специальные компоненты, автоматизирующие создание объектов вместе с их зависимостями. Такие инструменты как PHP-DI, Symfony DependencyInjection или Laravel Service Container существенно упрощают работу с DI в больших приложениях, автоматически разрешая графы зависимостей.
PHP | 1
2
3
| // Пример использования PHP-DI
$container = new \DI\Container();
$userService = $container->get(UserService::class); // Автоматически внедряет все зависимости |
|
Внедрение зависимостей не просто модный паттерн, а практический инструмент, делающий код более тестируемым, модульным и поддерживаемым. Вместо джунглей запутаных взаимозависимостей, мы получаем ясную структуру компонентов, где каждый выполняет свою функцию и ничего не знает о внутренностях других.
Основы внедрения зависимостей
Прежде чем углубиться в техническую реализацию, давайте разберёмся с фундаментальной проблемой, которую решает внедрение зависимостей – тесной связанностью кода. Я потратил несколько лет, создавая приложения, где каждый компонент знал слишком много о других компонентах. Это делало системы хрупкими как карточный домик – измени что-то в одном месте, и полетит десяток других классов. Тесная связанность (tight coupling) – ахиллесова пята многих PHP-приложений. Представьте класс, который создаёт свои зависимости напрямую:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class NewsletterManager {
private $mailer;
private $userRepository;
public function __construct() {
$this->mailer = new SmtpMailer('smtp.example.com', 587);
$this->userRepository = new MySqlUserRepository();
}
public function sendNewsletterToAllUsers() {
$users = $this->userRepository->findAllSubscribed();
foreach ($users as $user) {
$this->mailer->send($user->getEmail(), 'Новый выпуск', 'Содержание...');
}
}
} |
|
Этот код выглядит простым, но в нём заложена мина замедленного действия. Класс NewsletterManager жестко привязан к конкретным реализациям SmtpMailer и MySqlUserRepository . Что если вы захотите переключиться на другой способ отправки почты? Или сменить базу данных? Придётся переписывать сам класс менеджера. А теперь представьте, что нужно написать юнит-тест для метода sendNewsletterToAllUsers() . Как протестировать его, не затрагивая реальную базу данных и не отправляя реальные письма? Никак. Вот тут-то и приходит на помощь внедрение зависимостей.
Принципы внедрения зависимостей
Суть DI можно свести к простой идее: класс не должен создавать свои зависимости; вместо этого они должны передаваться извне. Это превращает зависимости из внутренних деталей реализации в явные требования. Переписав наш пример с использованием DI, получим:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class NewsletterManager {
private $mailer;
private $userRepository;
public function __construct(
MailerInterface $mailer,
UserRepositoryInterface $userRepository
) {
$this->mailer = $mailer;
$this->userRepository = $userRepository;
}
public function sendNewsletterToAllUsers() {
$users = $this->userRepository->findAllSubscribed();
foreach ($users as $user) {
$this->mailer->send($user->getEmail(), 'Новый выпуск', 'Содержание...');
}
}
} |
|
Теперь класс требует интерфейсы вместо конкретных реализаций. Он больше не знает и не заботится, как именно письма отправляются или как извлекаются пользователи. Это открывает дверь для следующих возможностей:
1. Легкая замена реализаций – можно использовать разные реализации одного интерфейса без изменения потребителей этого интерфейса.
2. Улучшенная тестируемость – можно легко создать мок-объекты для тестирования.
3. Модульность – компоненты приложения можно разрабатывать и тестировать независимо.
4. Повторное использование кода – компоненты легче использовать в разных контекстах.
Инверсия контроля: переворот с ног на голову
Внедрение зависимостей – это проявление принципа инверсии контроля (Inversion of Control, IoC). В традиционном подходе компонент явно контролирует процесс создания или поиска своих зависимостей. При инверсии контроля компонент пасивно получает зависимости. Можно провести аналогию с повседневной жизнью. Представьте, что вы голодны и хотите приготовить обед:
Традиционный подход: вы сами идёте в магазин, выбираете продукты, готовите и едите.
Подход с IoC: вы просто говорите "Я голоден" и кто-то приносит вам готовую еду.
В контексте кода IoC означает, что класс больше не контролирует создание объектов, которые ему нужны для работы. Это контроль переходит внешнему механизму – контейнеру или фабрике:
PHP | 1
2
3
4
5
6
7
8
9
| // Традиционный подход
$newsletterManager = new NewsletterManager(); // Внутренне создаёт все зависимости
$newsletterManager->sendNewsletterToAllUsers();
// Подход с IoC
$mailer = new SmtpMailer('smtp.example.com', 587);
$userRepository = new MySqlUserRepository();
$newsletterManager = new NewsletterManager($mailer, $userRepository);
$newsletterManager->sendNewsletterToAllUsers(); |
|
Многих смущает название "инверсия контроля" - в чём тут инверсия? Фокус в том, что мы инвертируем направление зависимости. Вместо того чтобы высокоуровневый компонент (NewsletterManager) зависел от конкретных низкоуровневых компонентов (SmtpMailer), теперь и высокоуровневые, и низкоуровневые компоненты зависят от абстракций (MailerInterface).
Именно эта инверсия зависимостей (буква D в SOLID) и позволяет создавать гибкие, расширяемые системы, где компоненты можно подменять без изменения их потребителей.
От концепции внедрения зависимостей можно перейти к практической реализации. А реализация в PHP имеет свои нюансы, особенности и инструменты, о которых мы поговорим далее.
Что такое "webapp" и "php" в запросе, в Cmd " php YiiRoot/framework/yiic.php webapp testdrive" Ребята разъясните суть команды "webapp" и "php" в запросе, в Cmd " php YiiRoot/framework/yiic.php... Как передать переменную php из 1.php в 2.php без include Всем добрый день, как передать в подобном примере переменную без include, require?
Есть 1.php с... Передать значение из php в php и из php в js Здравствуйте!
У меня есть php файл (назовем тест1), содержащий:
while (true) {
... Реализация алгоритма Дейкстры на PHP Доброй ночи.
Необходимо реализовать алгоритм Дейкстры на PHP.
Читал много статей, сама идея...
Реализация DI в PHP
За годы работы с PHP-проектами разного масштаба я перепробовал множество подходов к DI и хочу поделиться самыми практичными из них.
Типы внедрения зависимостей
В PHP существует три основных способа внедрения зависимостей, и каждый имеет свои сценарии применения.
Внедрение через конструктор
Самый распространенный и, на мой взгляд, предпочтительный способ — внедрение через конструктор. Зависимости передаются в момент создания объекта, что гарантирует его корректное состояние с самого начала:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class OrderProcessor {
private $paymentGateway;
private $notifier;
public function __construct(
PaymentGatewayInterface $paymentGateway,
NotifierInterface $notifier
) {
$this->paymentGateway = $paymentGateway;
$this->notifier = $notifier;
}
public function processOrder(Order $order) {
$this->paymentGateway->processPayment($order->getTotal());
$this->notifier->notify($order->getCustomer(), 'Заказ обработан');
}
} |
|
Преимущества этого подхода:- Зависимости явно обозначены в сигнатуре конструктора.
- Объект никогда не будет находиться в "полусобранном" состоянии.
- Легко реализовать иммутабельные объекты.
Недостатки:- При большом количестве зависимостей конструктор может стать громоздким.
- Все зависимости иницилизируются сразу, даже если реально будут использованы позже.
Внедрение через сеттеры
Иногда зависимость не обязательна для базовой функциональности объекта или может меняться в процессе его жизни. В таких случаях используют внедрение через сеттеры:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class Logger {
private $formatter;
public function setFormatter(FormatterInterface $formatter) {
$this->formatter = $formatter;
return $this;
}
public function log($message) {
if ($this->formatter) {
$message = $this->formatter->format($message);
}
// Логика логирования
}
}
// Использование
$logger = new Logger();
$logger->setFormatter(new JsonFormatter());
$logger->log('Событие произошло'); |
|
Главное преимущество этого подхода — гибкость и опциональность зависимостей. Подход также позволяет реализовать fluid interface (цепочку методов), что иногда улучшает читаемость кода.
Внедрение через свойства
Реже используемый способ — прямое внедрение в свойства объекта. Его используют, когда нужно максимально упростить API класса:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class TemplateRenderer {
public $cache;
public function render($template, $data) {
if ($this->cache && $cached = $this->cache->get($template)) {
return $cached;
}
// Логика рендеринга
}
}
// Использование
$renderer = new TemplateRenderer();
$renderer->cache = new FileCache(); |
|
Этот подход я рекомендую использовать с осторожностью, поскольку он снижает инкапсуляцию. Более безопасный вариант — использование атрибутов PHP 8, о чем мы поговорим чуть позже.
Автоматическая резолюция зависимостей
Создание объектов с большим количеством зависимостей может быть утомительным, особенно когда эти зависимости имеют собственные зависимости. Здесь на помощь приходит автоматическая резолюция с помощью Reflection API.
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| class DependencyResolver {
private $container = [];
public function register($id, $concrete = null) {
if ($concrete === null) {
$concrete = $id;
}
$this->container[$id] = $concrete;
}
public function resolve($id) {
// Если есть готовый экземпляр или фабрика, используем их
if (isset($this->container[$id])) {
$concrete = $this->container[$id];
if ($concrete instanceof Closure) {
return $concrete($this);
}
if (!is_string($concrete)) {
return $concrete;
}
} else {
$concrete = $id;
}
// Используем рефлексию для создания экземпляра
$reflector = new ReflectionClass($concrete);
if (!$reflector->isInstantiable()) {
throw new Exception("Класс {$concrete} не может быть инстанциирован");
}
$constructor = $reflector->getConstructor();
if (null === $constructor) {
return new $concrete;
}
// Разрешаем аргументы конструктора
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if (!$type || $type->isBuiltin()) {
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Не удалось разрешить параметр {$parameter->getName()}");
}
} else {
$dependencies[] = $this->resolve($type->getName());
}
}
return $reflector->newInstanceArgs($dependencies);
}
} |
|
Этот простой резолвер демонстрирует принцип работы автоматической резолюции зависимостей. Профессиональные контейнеры DI, такие как PHP-DI или Symfony DependencyInjection, используют более сложные алгоритмы с кэшированием и оптимизацией производительности.
Использование атрибутов PHP 8
С появлением атрибутов в PHP 8 реализация DI стала ещё элегантнее. Атрибуты позволяют помечать параметры и свойства для внедрения зависимостей прямо в коде:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
| class ProductController {
#[Inject]
private ProductRepository $repository;
#[Inject]
private Logger $logger;
public function show(#[Inject] Request $request, int $id) {
$this->logger->info("Запрошен продукт с ID: {$id}");
return $this->repository->find($id);
}
} |
|
Для работы с такими атрибутами необходимо реализовать обработчик, который будет использовать рефлексию для их чтения и внедрения соответствующих зависимостей. Этот подход особенно популярен в современных PHP-фреймворках.
Мне нравится, что атрибуты делают код более декларативным и читаемым, хотя есть мнение, что они могут усложнять отладку. Как и с любым инструментом, важен баланс и понимание, когда их применение действительно оправдано.
Инъекция методом в PHP: специальные сценарии
Инъекция методом (method injection) часто недооценивается, но в определенных сценариях она блистает своими преимуществами. Представьте, что у вас есть класс ReportGenerator , которому требуется форматтер для вывода данных, но формат меняется в зависимости от контекста:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class ReportGenerator {
public function generateReport(array $data, FormatterInterface $formatter) {
// Генерируем отчет и форматируем его с помощью переданного форматтера
$processedData = $this->processData($data);
return $formatter->format($processedData);
}
private function processData(array $data) {
// Обработка данных
return $data;
}
}
// Использование
$generator = new ReportGenerator();
$pdfReport = $generator->generateReport($salesData, new PdfFormatter());
$csvReport = $generator->generateReport($salesData, new CsvFormatter()); |
|
Этот паттерн особенно полезен, когда:- Зависимость нужна только для одного метода.
- Зависимость меняется при каждом вызове метода.
- Метод предоставляет определеный контекст для зависимости.
Существует малоизвестная, но очень мощная разновидность инъекции методом — delegate injection. При таком подходе мы передаем функцию или объект, который инкапсулирует определенное поведение:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class PaymentProcessor {
public function processPayment(Order $order, callable $paymentStrategyResolver) {
$paymentMethod = $order->getPaymentMethod();
$strategy = $paymentStrategyResolver($paymentMethod);
return $strategy->execute($order);
}
}
// Использование
$processor = new PaymentProcessor();
$result = $processor->processPayment($order, function($method) {
switch ($method) {
case 'credit_card': return new CreditCardPaymentStrategy();
case 'paypal': return new PayPalPaymentStrategy();
default: throw new UnsuportedPaymentMethodException();
}
}); |
|
Этот подход даёт гибкость и сохраняет контроль над созданием объектов там, где это логически обосновано.
Циклические зависимости: головная боль разработчика
Одна из самых неприятных проблем при работе с DI — это циклические зависимости. Они возникают, когда компонент A зависит от компонента B, который, в свою очередь, зависит от компонента A. В простейшем случае это выглядит так:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class A {
public function __construct(B $b) {
$this->b = $b;
}
}
class B {
public function __construct(A $a) {
$this->a = $a;
}
}
// Теперь попробуйте создать экземпляр A или B...
// Получите бесконечную рекурсию! |
|
Циклические зависимости почти всегда указывают на проблемы в архитектуре. Они нарушают принцип единственной ответсвенности и создают излишние связи между компонентами. Но иногда их невозможно избежать, особенно в сложных системах или при работе с легаси-кодом. Существует несколько подходов к решению проблемы:
1. Реструктуризация кода — идеальное решение, когда возможно. Выделите общую функциональность в отдельный сервис, на который будут зависеть оба компонента.
2. Использование интерфейсов — вместо прямой зависимости от класса, зависьте от интерфейса:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| interface AInterface { /* ... */ }
interface BInterface { /* ... */ }
class A implements AInterface {
public function __construct(BInterface $b) {
$this->b = $b;
}
}
class B implements BInterface {
private $aFactory;
public function __construct(callable $aFactory) {
$this->aFactory = $aFactory;
}
public function someMethod() {
$a = ($this->aFactory)();
// Используем $a
}
} |
|
3. Отложенная инициализация — используйте лениво-загруженные прокси для разрыва цикла:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class LazyServiceProxy {
private $container;
private $serviceId;
private $service;
public function __construct($container, $serviceId) {
$this->container = $container;
$this->serviceId = $serviceId;
}
public function __call($method, $arguments) {
if (!$this->service) {
$this->service = $this->container->get($this->serviceId);
}
return call_user_func_array([$this->service, $method], $arguments);
}
} |
|
Лениво-загруженные зависимости
Когда приложение разрастается, инициализация всех зависимостей сразу может вызвать проблемы с производительностью. Представьте сервис с десятком зависимостей, некоторые из которых редко используются. Лениво-загруженные (lazy-loaded) зависимости загружаются только в момент фактического использования. PHP предоставляет несколько механизмов для реализации ленивой загрузки:
1. Фабрики — вместо объекта внедряется фабрика, которая создаст объект при необходимости:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| class ExpensiveService {
public function __construct() {
// Тяжелая инициализация
sleep(2); // Имитация тяжелой работы
}
public function doSomething() {
return "Результат";
}
}
class ExpensiveServiceFactory {
private $instance;
public function getService() {
if (!$this->instance) {
$this->instance = new ExpensiveService();
}
return $this->instance;
}
}
class Consumer {
private $serviceFactory;
public function __construct(ExpensiveServiceFactory $factory) {
$this->serviceFactory = $factory;
}
public function rarlyUsedMethod() {
$service = $this->serviceFactory->getService();
return $service->doSomething();
}
} |
|
2. Прокси-классы — создание "заглушек", которые загружают настоящий объект по требованию:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class UserRepositoryProxy implements UserRepositoryInterface {
private $container;
private $realRepository;
public function __construct($container) {
$this->container = $container;
}
public function findById($id) {
$this->initializeRepository();
return $this->realRepository->findById($id);
}
private function initializeRepository() {
if (!$this->realRepository) {
$this->realRepository = $this->container->get('user.repository');
}
}
} |
|
В PHP 8 появилась возможность упростить создание прокси с помощью атрибутов и генерации кода:
PHP | 1
2
| #[LazyDependency]
private ExpensiveService $service; |
|
Конечно, большинство современных контейнеров DI уже имеют встроенную поддержку ленивой загрузки, что значительно упрощает работу с тяжёлыми зависимостями.
Внедрение зависимостей и проблема конфигурации
Еще один важный аспект реализации DI — передача конфигурации. Как внедрять не только сервисы, но и простые значения? Большинство контейнеров DI позволяют регистрировать параметры:
PHP | 1
2
| $container->set('database.host', 'localhost');
$container->set('database.port', 3306); |
|
Которые затем можно внедрять через конструктор:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
| class DatabaseConnection {
private $host;
private $port;
public function __construct(
#[Inject('database.host')] string $host,
#[Inject('database.port')] int $port
) {
$this->host = $host;
$this->port = $port;
}
} |
|
Или через специальные фабрики:
PHP | 1
2
3
4
5
6
| $container->set(DatabaseConnection::class, function($container) {
return new DatabaseConnection(
$container->get('database.host'),
$container->get('database.port')
);
}); |
|
Внедрение зависимостей в PHP — мощный и гибкий инструмент, но как и любая технология, требует понимания и правильного применения. К счастью, современные PHP-фреймворки и библиотеки значительно упрощают эту задачу, предоставляя готовые решения для большинства типичных сценариев.
Контейнеры внедрения зависимостей
После того как я годами мучался с ручным созданием объектов и их зависимостей, открытие DI-контейнеров стало для меня настоящим откровением. Представьте, что вы годами складывали кирпичики вручную, а потом вам вдруг показали бетономешалку. Примерно такой же эффект производят DI-контейнеры, когда вы впервые понимаете их мощь.
DI-контейнер – это компонент, который автоматизирует процесс создания объектов и управления их зависимостями. По сути, он выступает в качестве фабрики объектов на стероидах. Не нужно больше вручную создавать каждый объект и передавать ему зависимости – контейнер сделает это за вас.
PHP | 1
2
3
4
5
6
7
8
9
10
| // Без контейнера
$logger = new FileLogger('/var/log/app.log');
$mailer = new SmtpMailer('smtp.example.com', 587, 'user', 'pass');
$userRepository = new UserRepository(new MySqlConnection('localhost', 'root', 'password', 'mydb'));
$notificationService = new NotificationService($mailer, $logger);
$userService = new UserService($userRepository, $notificationService);
// С контейнером
$container = new Container();
$userService = $container->get(UserService::class); |
|
Разница очевидна. Контейнер автоматически резолвит все зависимости в дереве объектов, создавая их в правильном порядке. Но его возможности этим не ограничиваются.
Популярные PHP-контейнеры и их особенности
За долгие годы работы с PHP я перепробовал большинство популярных DI-контейнеров, и у каждого есть свои сильные стороны.
PHP-DI
PHP-DI, пожалуй, самый дружелюбный к разработчику контейнер. Он поддерживает множество способов определения зависимостей, включая аннотации, атрибуты PHP 8 и программную конфигурацию:
PHP | 1
2
3
4
5
6
7
8
9
| // Конфигурация через PHP
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->addDefinitions([
Logger::class => \DI\create()->constructor('/var/log/app.log'),
UserRepository::class => \DI\autowire(),
NotificationService::class => \DI\create()
->constructor(\DI\get(Mailer::class), \DI\get(Logger::class)),
]);
$container = $containerBuilder->build(); |
|
Одна из сильных сторон PHP-DI – автовайринг, позволяющий контейнеру автоматически резолвить зависимости на основе типов:
PHP | 1
2
| // PHP-DI автоматически определит, какие объекты нужно создать и внедрить
$userService = $container->get(UserService::class); |
|
Symfony DependencyInjection
Компонент Symfony DependencyInjection мощнейший и зрелый контейнер. Он поддерживает множество форматов конфигурации (YAML, XML, PHP) и предлагает продвинутые возможности:
PHP | 1
2
3
4
5
6
7
8
9
10
11
| // Пример конфигурации в PHP
$container = new ContainerBuilder();
$container->register('logger', Logger::class)
->setArguments(['/var/log/app.log']);
$container->register('mailer', SmtpMailer::class)
->setArguments(['smtp.example.com', 587, 'user', 'pass']);
$container->register('notification_service', NotificationService::class)
->setArguments([new Reference('mailer'), new Reference('logger')]); |
|
Symfony DependencyInjection отлично подходит для больших проектов благодоря мощным возможностям компиляции и оптимизации.
Laravel Service Container
Если вы работаете с Laravel, у вас уже есть встроенный DI-контейнер:
PHP | 1
2
3
4
5
6
7
8
9
| // Регистрация в провайдере сервиса
$this->app->bind(LoggerInterface::class, function ($app) {
return new FileLogger('/var/log/app.log');
});
// Использование
$logger = app(LoggerInterface::class);
// или через автоматическое разрешение в конструкторе контроллера
public function __construct(LoggerInterface $logger) { ... } |
|
Laravel Container не просто резолвит зависимости – он интегрирован во все аспекты фреймворка, от контроллеров до консольных команд.
Производительность и оптимизация контейнеров
Одно из распостраненных заблуждений: "DI-контейнеры замедляют приложение". Да, использование рефлексии имеет некоторые накладные расходы, но современные контейнеры оптимизированы для минимизации этих затрат. Ключевой инструмент оптимизации – компиляция контейнера. Symfony, например, генерирует PHP-класс, который оптимизирует процесс создания объектов:
PHP | 1
2
3
4
5
6
7
8
| // Включение компиляции в Symfony
$container = new ContainerBuilder();
// ...настройка контейнера...
$container->compile();
// Сохраняем скомпилированный контейнер
$dumper = new PhpDumper($container);
file_put_contents('CompiledContainer.php', $dumper->dump()); |
|
Скомпилированный контейнер не использует рефлексию во время выполнения, что существенно повышает производительность.
PHP-DI также поддерживает компиляцию:
PHP | 1
2
3
| $containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->enableCompilation(__DIR__ . '/cache');
$containerBuilder->writeProxiesToFile(true, __DIR__ . '/cache/proxies'); |
|
Другой важный аспект оптимизации – это использование безымянных классов для прокси-объектов, что уменьшает накладные расходы на их загрузку.
Создание собственного контейнера DI
Несмотря на обилие готовых решений, иногда требуется создать собственный контейнер – например, для лучшего понимания принципов работы или для специфичных потребностей проекта. Вот минималистичная реализация:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| class SimpleContainer {
private $services = [];
private $singletons = [];
public function bind($id, callable $factory) {
$this->services[$id] = $factory;
}
public function singleton($id, callable $factory) {
$this->services[$id] = function($container) use ($factory, $id) {
if (!isset($this->singletons[$id])) {
$this->singletons[$id] = $factory($container);
}
return $this->singletons[$id];
};
}
public function get($id) {
if (!isset($this->services[$id])) {
throw new Exception("Сервис не найден: $id");
}
return $this->services[$id]($this);
}
}
// Использование
$container = new SimpleContainer();
$container->singleton(Logger::class, function() {
return new FileLogger('/var/log/app.log');
});
$container->bind(UserRepository::class, function() {
return new UserRepository();
});
$container->bind(UserService::class, function($container) {
return new UserService(
$container->get(UserRepository::class),
$container->get(Logger::class)
);
});
$userService = $container->get(UserService::class); |
|
Эта простая реализация демонстрирует основные принципы работы DI-контейнера. Конечно, в реальных проектах используются более сложные решения с поддрежкой автовайринга, компиляции и многих других возможностей.
Фабрики и провайдеры в контейнерах DI
Одна из проблем, с которыми я часто сталкивался – необходимость передавать параметры при создании объекта. Допустим, у вас есть сервис, который принимает ID пользователя в конструкторе:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
| class UserProfileService {
private $userId;
private $userRepository;
public function __construct($userId, UserRepository $userRepository) {
$this->userId = $userId;
$this->userRepository = $userRepository;
}
public function getProfileData() {
return $this->userRepository->findById($this->userId);
}
} |
|
DI-контейнеры могут справиться с внедрением UserRepository , но откуда им взять $userId ? Тут на помощь приходят фабрики:
PHP | 1
2
3
| $container->bind(UserProfileService::class, function($container) use ($userId) {
return new UserProfileService($userId, $container->get(UserRepository::class));
}); |
|
Но что если вам нужно создавать этот сервис в разных частях приложения с разными ID? Здесь может помочь паттерн Factory:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class UserProfileServiceFactory {
private $container;
public function __construct($container) {
$this->container = $container;
}
public function create($userId) {
return new UserProfileService(
$userId,
$this->container->get(UserRepository::class)
);
}
}
// Регистрация фабрики
$container->singleton(UserProfileServiceFactory::class, function($container) {
return new UserProfileServiceFactory($container);
});
// Использование
$factory = $container->get(UserProfileServiceFactory::class);
$service = $factory->create($userId); |
|
Фабрики – это мощный инструмент, позволяющий совместить удобство DI-контейнеров с гибкостью ручного создания объектов.
Ленивая загрузка зависимостей в действии
Когда речь заходит о крупных приложениях, ленивая загрузка (lazy loading) зависимостей превращается из милой оптимизации в жизненную необходимость. Я помню, как однажды мне нужно было оптимизировать API-сервис, где время ответа выросло до неприличных значений. Анализ показал, что система инициализировала десятки тяжёлых зависимостей, хотя для конкретного запроса требовалось всего несколько. Большинство современных DI-контейнеров предлагают элегантные решения для ленивой загрузки. В Symfony это делается особенно просто:
PHP | 1
2
| $container->register('heavy_service', HeavyService::class)
->setLazy(true); |
|
Что происходит под капотом? Контейнер генерирует прокси-класс, который выглядит и ведёт себя как настоящий сервис, но на самом деле создаёт его только при первом обращении к методам:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Псевдокод сгенерированного прокси
class HeavyServiceProxy extends HeavyService
{
private $container;
private $realInstance;
public function __construct($container)
{
$this->container = $container;
}
public function someMethod($arg)
{
if (null === $this->realInstance) {
$this->realInstance = $this->container->createHeavyService();
}
return $this->realInstance->someMethod($arg);
}
} |
|
PHP-DI предлагает похожую функциональность через более явный интерфейс:
PHP | 1
2
3
4
5
| $containerBuilder = new DI\ContainerBuilder();
$containerBuilder->writeProxiesToFile(true, __DIR__ . '/cache/proxies');
$containerBuilder->addDefinitions([
HeavyService::class => \DI\create()->lazy(),
]); |
|
В Laravel тот же эффект можно получить, используя фабрики и отложенные поставщики сервисов:
PHP | 1
2
3
4
5
6
| $this->app->bind('heavy', function ($app) {
return new HeavyService();
});
// Где-то в коде
$service = app()->make('heavy'); // Создастся только здесь |
|
Тегирование сервисов и коллекции
Еще одна мощная функция современных DI-контейнеров — тегирование сервисов. Это позволяет группировать сервисы по определённому признаку и получать их как коллекцию.
Допустим, у нас есть несколько обработчиков платежей:
PHP | 1
2
3
4
5
6
7
8
| interface PaymentHandlerInterface {
public function supports($method): bool;
public function handle(Payment $payment);
}
class StripeHandler implements PaymentHandlerInterface { /* ... */ }
class PayPalHandler implements PaymentHandlerInterface { /* ... */ }
class BitcoinHandler implements PaymentHandlerInterface { /* ... */ } |
|
С помощью тегирования в Symfony мы можем собрать их все вместе:
PHP | 1
2
3
4
5
6
7
8
9
10
| // config.php или services.yaml
$container->register(StripeHandler::class)
->addTag('payment.handler');
$container->register(PayPalHandler::class)
->addTag('payment.handler');
$container->register(BitcoinHandler::class)
->addTag('payment.handler');
// Получение всех обработчиков
$handlers = $container->findTaggedServiceIds('payment.handler'); |
|
В PHP-DI эта же задача решается с помощью агрегации:
PHP | 1
2
3
4
5
6
7
| $containerBuilder->addDefinitions([
'payment.handlers' => \DI\get([
\DI\get(StripeHandler::class),
\DI\get(PayPalHandler::class),
\DI\get(BitcoinHandler::class)
])
]); |
|
Контекстно-зависимые сервисы
Иногда в разных частях приложения требуются разные реализации одного интерфейса. Например, в админке нужен один логгер, а в публичной части сайта — другой. Контейнеры DI предлагают решения и для этой задачи. Symfony поддерживает контекстные сервисы через алиасы:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
| $container->register('logger.admin', FileLogger::class)
->setArguments(['/var/log/admin.log']);
$container->register('logger.public', FileLogger::class)
->setArguments(['/var/log/public.log']);
// В админ-контроллерах
$container->register(AdminController::class)
->setArguments([new Reference('logger.admin')]);
// В публичных контроллерах
$container->register(PublicController::class)
->setArguments([new Reference('logger.public')]); |
|
В PHP-DI можно использовать контексные определения:
PHP | 1
2
3
4
5
6
7
8
9
| $containerBuilder->addDefinitions([
LoggerInterface::class => \DI\autowire(FileLogger::class)
->constructorParameter('path', '/var/log/default.log'),
// Переопределение для конкретного класса
AdminController::class => \DI\autowire()
->constructorParameter('logger', \DI\autowire(FileLogger::class)
->constructorParameter('path', '/var/log/admin.log'))
]); |
|
Декораторы и цепочки ответсвености
Еще одна мощная возможность, которую предоставляют контейнеры DI — применение шаблона "Декоратор". Это позволяет обернуть существующий сервис дополнительной функциональностью без изменения его кода. Например, добавление кэширования к репозиторию:
PHP | 1
2
3
4
5
| $container->register(UserRepository::class, MySqlUserRepository::class);
$container->register('user_repository.cached', CachedUserRepository::class)
->setDecoratedService(UserRepository::class)
->setArguments([new Reference('user_repository.cached.inner')]); |
|
Symfony автоматически создаст ссылку user_repository.cached.inner на оригинальный сервис.
В PHP-DI декораторы реализуются немного иначе:
PHP | 1
2
3
4
5
| $containerBuilder->addDefinitions([
UserRepository::class => \DI\decorate(function ($previous, $container) {
return new CachedUserRepository($previous);
})
]); |
|
Применяя декораторы, можно создавать гибкие цепочки ответственности, где каждый слой добавляет свою функциональность: логирование, кэширование, валидацию и т.д.
Ограничения автовайринга
Несмотря на удобство автоматического разрешения зависимостей через типы (автовайринг), у этого подхода есть свои ограничения, которые я не раз встречал в реальных проектах.
Во-первых, PHP не позволяет указать типы для скалярных параметров конструктора до версии 7.4. Даже с появлением типизированных свойств, есть сценарии, когда контейнер не может автоматически определить, какое значение нужно внедрить:
PHP | 1
2
3
4
5
6
7
| public function __construct(
LoggerInterface $logger,
string $logPath, // Откуда контейнеру знать это значение?
bool $enableDebug // И это?
) {
// ...
} |
|
Во-вторых, автовайринг может быть неоднозначным, если несколько сервисов реализуют один интерфейс:
PHP | 1
2
3
4
| // Какой именно логер должен быть внедрен?
$container->register('file_logger', FileLogger::class);
$container->register('syslog_logger', SyslogLogger::class);
// Оба реализуют LoggerInterface |
|
Обычно это решается с помощью явного указания конкретных сервисов для конкретных классов или с использованием приоритетов.
В-третьих, автовайринг может усложнить отладку. Когда зависимости определены явно, легче отследить, откуда что берётся. С автовайрингом эти связи становятся неявными, что иногда затрудняет понимание, почему что-то не работает.
Помню случай, когда в крупном проекте сервис неожидано менял поведение в зависимости от контекста использования. После долгих часов отладки оказалось, что автовайринг резолвил разные реализации интерфейса в разных местах из-за тонких различий в конфигурации. Такие ситуации заставляют ценить явное объявление зависимостей.
Интеграция в реальные проекты
Стратегии интеграции DI в существующие проекты
Невозможно переписать большое приложение за один день, поэтому разумнее всего применять поэтапный подход. Моя любимая стратегия — "обнимать, а не переписывать", когда мы постепенно обёртываем существующие компоненты в DI-совместимый слой, не выбрасывая работающий код:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Старый код, который нельзя сразу переписать
class LegacyUserRepository {
public function findById($id) { /* ... */ }
}
// Обёртка для интеграции в DI-систему
class UserRepository implements UserRepositoryInterface {
private $legacyRepository;
public function __construct() {
$this->legacyRepository = new LegacyUserRepository();
}
public function findById($id) {
return $this->legacyRepository->findById($id);
}
} |
|
Это временное решение позволяет новому коду использовать интерфейсы и DI, в то время как старый код продолжает работать без изменений. Постепенно, класс за классом, можно заменить прямые зависимости на внедряемые. Другой полезный подход — "стрэнглер" (strangler pattern), названный в честь фикуса-душителя, который обвивает дерево-хозяина, постепенно заменяя его. По этому паттерну новая фунциональность разрабатывается с применением DI, в то время как старая обёртывается в сервисные фасады.
Рефакторинг с жестких зависимостей на DI
Как-то мне довелось работать над рефакторингом монолитного приложения для интернет-магазина. Одним из ключевых классов был OrderProcessor , который прямо создавал свои зависимости:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class OrderProcessor {
private $paymentGateway;
private $inventoryManager;
public function __construct() {
$this->paymentGateway = new StripeGateway(
getenv('STRIPE_API_KEY'),
getenv('STRIPE_SECRET')
);
$this->inventoryManager = new InventoryManager(
new MySqlConnection(/* ... */)
);
}
public function processOrder($order) {
// Логика обработки заказа
}
} |
|
Первым шагом было извлечение интерфейсов для зависимостей:
PHP | 1
2
3
4
5
6
7
| interface PaymentGatewayInterface {
public function processPayment($amount, $paymentDetails);
}
interface InventoryManagerInterface {
public function updateStock($productId, $quantity);
} |
|
Затем рефакторинг самого класса для принятия этих интерфейсов:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class OrderProcessor {
private $paymentGateway;
private $inventoryManager;
public function __construct(
PaymentGatewayInterface $paymentGateway,
InventoryManagerInterface $inventoryManager
) {
$this->paymentGateway = $paymentGateway;
$this->inventoryManager = $inventoryManager;
}
public function processOrder($order) {
// Логика обработки заказа
}
} |
|
Однако в местах, где OrderProcessor использовался, потребовались изменения. Мы создали фабрику, которая временно выступала в роли простейшего контейнера:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class OrderProcessorFactory {
public static function create() {
return new OrderProcessor(
new StripeGateway(
getenv('STRIPE_API_KEY'),
getenv('STRIPE_SECRET')
),
new InventoryManager(
new MySqlConnection(/* ... */)
)
);
}
}
// Использование в существующем коде
$processor = OrderProcessorFactory::create(); |
|
Такой промежуточный шаг позволил нам постепенно заменять другие компоненты, не ломая существующий код.
Интеграция с популярными PHP-фреймворками
Один из самых быстрых способов внедрить DI в проект — использовать встроенные контейнеры, которые предлагают современные фреймворки.
Laravel
В Laravel сервис-контейнер является центральной частью фреймворка:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
| // AppServiceProvider.php
public function register()
{
$this->app->bind(PaymentGatewayInterface::class, function ($app) {
return new StripeGateway(
config('services.stripe.key'),
config('services.stripe.secret')
);
});
$this->app->bind(InventoryManagerInterface::class, InventoryManager::class);
} |
|
Laravel автоматически разрешит эти зависимости в контроллерах, командах и других компонентах.
Symfony
Symfony использует мощный компонент DependencyInjection, который можно настраивать через YAML, XML или PHP-конфигурацию:
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # services.yaml
services:
app.payment_gateway:
class: App\Payment\StripeGateway
arguments:
- '%env(STRIPE_API_KEY)%'
- '%env(STRIPE_SECRET)%'
app.inventory_manager:
class: App\Inventory\InventoryManager
arguments:
- '@doctrine.orm.entity_manager'
App\Order\OrderProcessor:
arguments:
- '@app.payment_gateway'
- '@app.inventory_manager' |
|
Slim + PHP-DI
Для микро-фреймворков, таких как Slim, можно использовать внешние контейнеры:
PHP | 1
2
3
4
5
| $container = new \DI\Container();
$container->set(PaymentGatewayInterface::class, \DI\create(StripeGateway::class)
->constructor(getenv('STRIPE_API_KEY'), getenv('STRIPE_SECRET')));
$app = \Slim\Factory\AppFactory::createFromContainer($container); |
|
Итерационный подход к внедрению DI
В одном из проектов я столкнулся с монолитным приложением из более чем 500 классов, тесно связанных между собой. Там мы применили итерационную стратегию "от периферии к ядру":
1. Сначала идентифицировали "пограничные" компоненты — те, что реже меняются и имеют меньше зависимостей (часто это классы для работы с файловой системой, почтой и т.д.).
2. Для них создали интерфейсы и интегрировали с DI-контейнером.
3. Постепенно продвигались к более сложным компонентам, разрешая их зависимости через контейнер.
4. В промежуточной стадии использовали "гибридную" модель, где одни компоненты получали зависимости через DI, а другие ещё по-старому.
Этот подход позволил нам постепенно модернизировать приложение без остановки разработки новых функций. Через шесть месяцев более 70% кода использовало DI, что значительно упростило разработку новых функций и тестирование.
Помню, как скептически некоторые члены команды отнеслись к идее перехода на DI. "Это же куча дополнительного кода!" — говорили они. Но спустя несколько месяцев даже самые упрямые критики оценили, насколько проще стало добавлять новую функциональность и тестировать код. Секрет успешной интеграции DI в существующий проект не в идеальной архитектуре с первого дня, а в последовательности и терпении. Шаг за шагом, класс за классом — и сложная, запутанная система начинает трансформироваться в нечто более управляемое.
Лучшие практики и рекомендации экспертов
За годы работы с DI в PHP-проектах разного масштаба я наступил на все возможные грабли и выработал набор правил, которые помогают избежать типичных ошибок. Делюсь своим опытом и советами других экспертов, которые могут сэкономить вам немало времени и нервов.
Не превращайте DI в антипаттерн Service Locator
Одна из распространенных ошибок — использование контейнера DI непосредственно в бизнес-логике:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class BadExampleController {
private $container;
public function __construct($container) {
$this->container = $container;
}
public function showUser($id) {
// Антипаттерн! Запрашиваем сервисы напрямую из контейнера
$userRepository = $this->container->get(UserRepository::class);
$logger = $this->container->get(LoggerInterface::class);
$logger->info("Запрошен пользователь: {$id}");
return $userRepository->find($id);
}
} |
|
Это превращает DI-контейнер в Service Locator — глобальный объект, от которого зависит весь код. Вместо этого запрашивайте конкретные зависимости:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class GoodExampleController {
private $userRepository;
private $logger;
public function __construct(
UserRepository $userRepository,
LoggerInterface $logger
) {
$this->userRepository = $userRepository;
$this->logger = $logger;
}
public function showUser($id) {
$this->logger->info("Запрошен пользователь: {$id}");
return $this->userRepository->find($id);
}
} |
|
Избегайте слишком глубоких графов зависимостей
Если для создания объекта требуется 10 других объектов, каждый из которых имеет свои зависимости — у вас проблемы. Я работал с сервисом, который принимал в конструктор 14 (!) других сервисов. Такой код невозможно поддерживать.
Глубокие графы зависимостей указывают на нарушение принципа единственной ответсвенности. Разделяйте большие классы на меньшие компоненты, каждый со своей четкой зоной ответственности.
Применяйте инъекцию правильно для каждого типа зависимости
Обязательные, неизменяемые зависимости → конструктор
Опциональные зависимости → сеттеры
Зависимости для конкретных операций → аргументы метода
Контекстные зависимости → фабрики
Не пережимайте с абстракцией
Иногда создание интерфейса для каждого сервиса — перебор. Я видел проекты, где даже для простых объектов-значений создавались интерфейсы, которые никогда не переопределялись. Применяйте YAGNI (You Aren't Gonna Need It) и создавайте абстракции только там, где это действительно нужно.
Используйте внедрение констант осмотрительно
Одна из частых ошибок — внедрять через DI конфигурационные значения:
PHP | 1
2
3
4
5
6
7
| // Не очень хорошо
class ApiClient {
public function __construct(
#[Inject('api.base_url')] string $baseUrl,
#[Inject('api.token')] string $token
) { /* ... */ }
} |
|
Лучше сгруппировать связанные настройки в объекты-значения:
PHP | 1
2
3
4
5
6
7
8
9
10
11
| // Лучше
class ApiConfig {
public function __construct(
public readonly string $baseUrl,
public readonly string $token
) {}
}
class ApiClient {
public function __construct(ApiConfig $config) { /* ... */ }
} |
|
Остерегайтесь автовайринга в продакшене
Автовайринг удобен, но может стать источником неожиданных проблем. Я сталкивался с ситуацией, когда после рефакторинга автовайринг начал подставлять не тот сервис, и это вскрылось только в продакшене. Поэтому всегда компилируйте контейнер в режиме, выбрасывающем исключения при неоднозначностях.
Документируйте свои сервисы
В крупных проектах количество сервисов может исчисляться сотнями. Без документации новым разработчикам (да и вам через полгода) будет сложно понять, какой сервис за что отвечает. Используйте PHPDoc и инструменты вроде Symfony Debug для визуализации графа зависимостей.
Тестируйте свои сервисы изолированно
DI делает тестирование гораздо проще, но этим преимуществом нужно пользоваться. Пишите модульные тесты, заменяя реальные зависимости на моки или стабы:
PHP | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public function testOrderProcessing()
{
// Создаем моки зависимостей
$paymentGateway = $this->createMock(PaymentGatewayInterface::class);
$paymentGateway->expects($this->once())
->method('processPayment')
->willReturn(true);
// Передаем моки в тестируемый сервис
$processor = new OrderProcessor($paymentGateway, $this->createMock(InventoryManagerInterface::class));
// Проверяем результат
$this->assertTrue($processor->processOrder(new Order()));
} |
|
Внедрение зависимостей — это не просто технический приём, а философия проектирования, которая меняет подход к архитектуре приложений. Правильно применённая, она делает код гибким, тестируемым и поддерживаемым на протяжении всего жизненного цикла проекта.
Реализация Шифра частокола в PHP Доброго времени суток!
Нужна помощь с реализацией Шифра частокола в PHP!
Максимальный ключ равен... Реализация фильтров товаров на php Как реализовать фильтрацию товаров по определенным параметрам и правильно это спроектировать в бд?... Реализация двухуровневого меню на php&MYSQL Есть две таблицы: categories
и sub_categories
Нужно выводить это в двухуровневое меню, вот в... Переход от PHP к HTML и обратно (реализация фотоальбома) Доброго времени суток всем.
У меня такая проблема я не могу сделать переход от php к html и... Реализация по PHP его алгоритм и конструкция Здравствуйте. (я НОВИЧОК)
Вот правильно ли я строю свой алгоритм и/или конструкцию по PHP?
... Реализация хэштегов на PHP Всем доброго. Вопрос таков как с помощью регулярки в переменной отловить все хештеги и заменить их... Реализация личных сообщений PHP Доброго утра, Форумчане
Требуется Ваш совет, как лучше всего реализовать полноценный обмен... Реализация Онлайна на сайте php+mvc Делаю онлайн, нужно каждый раз как человек переходит на страницу в БД записывать данные.
Вот как... Реализация что-то наподобие потоков в php Здравствуйте, есть массив объектов класса Кот, нужно выводить на экран информацию о передвижении... Реализация меню средствами php Есть отдельный файл menu.php, вставляется в каждую страницу инклудом.
<ul class="menu">
<li><a... Реализация калькулятора на PHP По заданию необходимо создать калькулятор на PHP. Я сделал набросок, но как делать код выполнения -... Реализация метода класса в PHP У меня есть класс Person:
<?php class Person {
public $name;
public function...
|