Почему PHP до сих пор рулит в Enterprise, и как не ударить в грязь лицом
Когда слышишь «PHP», первая ассоциация — это лендинг за вечер или простенький блог на WordPress. Но откройте вакансии крупных банков, ритейлеров или телеком-компаний — там тонны вакансий для PHP-разработчиков. И это не про «быстро набросать код». Это — Enterprise PHP. Целая индустрия со своими законами, архитектурными паттернами и культурой код-ревью.
Чем отличается разработка на PHP в крупной компании от стартапа? Ты не просто пишешь «работающий код». Ты строишь систему, которая должна жить годы, пережить десятки релизов и выдержать нагрузку тысяч пользователей. В этой статье я разберу ключевые особенности Enterprise-разработки на PHP: от архитектуры до деплоя. Будет много кода, практических советов и минимум воды.
Архитектура: SOLID, слои и никакой магии
В Enterprise первое правило — код должен быть предсказуемым и легко тестируемым. Здесь не прокатит «вот тут я быстренько запилил запрос прямо в контроллере». В больших проектах ты обязан следовать принципам SOLID и четко разделять ответственность между слоями.
Типичная архитектура Enterprise-приложения на PHP выглядит так:
- Presentation layer — контроллеры, роуты, Middleware.
- Application layer — сервисы, DTO, команды/обработчики (CQRS).
- Domain layer — сущности, Value Objects, репозитории (интерфейсы).
- Infrastructure layer — ORM, внешние API, кэш, очереди.
Вот как это выглядит на практике. Вместо того чтобы писать SQL прямо в контроллере:
// ❌ Плохо: смешиваем слои
class OrderController {
public function create(Request $request) {
$db = new PDO('mysql:...');
$db->exec("INSERT INTO orders (user_id, total) VALUES ({$request->get('user_id')}, {$request->get('total')})");
return new JsonResponse(['status' => 'ok']);
}
}
В Enterprise ты увидишь нечто подобное:
// ✅ Хорошо: разделение ответственности
class OrderController {
public function __construct(
private OrderService $orderService
) {}
public function create(CreateOrderRequest $request): JsonResponse {
$dto = new CreateOrderDTO(
userId: $request->getUserId(),
total: $request->getTotal()
);
$order = $this->orderService->createOrder($dto);
return new JsonResponse(['id' => $order->getId()]);
}
}
class OrderService {
public function __construct(
private OrderRepositoryInterface $orderRepository,
private EventDispatcher $eventDispatcher
) {}
public function createOrder(CreateOrderDTO $dto): Order {
$order = new Order($dto->userId, $dto->total);
$this->orderRepository->save($order);
$this->eventDispatcher->dispatch(new OrderCreatedEvent($order));
return $order;
}
}
Видите разницу? Второй вариант можно протестировать, переиспользовать и легко изменить. Это и есть Enterprise PHP — когда код не просто работает, а спроектирован.
Value Objects и строгая типизация
В крупных проектах ты не увидишь string $email в параметрах. Вместо этого используются Value Objects. Это самодокументируемый код, который защищает от ошибок на этапе компиляции.
class Email {
public function __construct(private string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email');
}
}
public function getValue(): string {
return $this->value;
}
}
class User {
public function __construct(
private UserId $id,
private Email $email,
private FullName $fullName
) {}
}
Теперь никто случайно не передаст в конструктор User строку вместо Email — компилятор или статический анализатор (например, PHPStan на уровне max) сразу укажет на ошибку.
Тестирование: без покрытия релиз не пройдет
В Enterprise-разработке на PHP тесты — это не опция, а требование. Крупные компании используют три уровня тестирования:
- Unit-тесты — проверяют отдельные классы и методы. Используются моки и стабы. Покрытие часто требует >80%.
- Integration-тесты — проверяют взаимодействие с базой, кэшем, внешними сервисами. Запускаются в изолированном окружении (тестовая БД, Docker).
- Functional/E2E-тесты — симулируют действия пользователя через HTTP.
Пример unit-теста для нашего OrderService:
class OrderServiceTest extends TestCase {
public function testCreateOrderSavesCorrectly(): void {
// Arrange
$repository = $this->createMock(OrderRepositoryInterface::class);
$repository->expects($this->once())
->method('save')
->with($this->isInstanceOf(Order::class));
$dispatcher = $this->createMock(EventDispatcher::class);
$dispatcher->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(OrderCreatedEvent::class));
$service = new OrderService($repository, $dispatcher);
$dto = new CreateOrderDTO(userId: 1, total: 100.50);
// Act
$order = $service->createOrder($dto);
// Assert
$this->assertEquals($dto->userId, $order->getUserId());
$this->assertEquals($dto->total, $order->getTotal());
}
}
В Enterprise-проектах тесты запускаются автоматически при каждом пуше в репозиторий. Если тесты упали — код не мержится. Сурово, но справедливо.
Код-ревью: как не получить «минус» на ревью
Код-ревью в крупных компаниях — это священная корова. Каждый MR (Merge Request) проверяется минимум двумя разработчиками. Что ищут на ревью в Enterprise PHP?
- Архитектурные нарушения — например, если сервис начал лезть в HTTP-запрос или контроллер делает запросы к БД.
- Отсутствие тестов — новый метод без теста = причина отклонить MR.
- Проблемы безопасности — экранирование, XSS, SQL-инъекции, валидация.
- Стиль и нейминг — PSR-12 обязателен, названия классов и методов должны быть понятны без комментариев.
Вот типичный комментарий на ревью:
«This method violates Single Responsibility Principle. It both validates the input and sends an email. Consider extracting email sending to a separate service.»
Кстати, про безопасность. В Enterprise никогда не пишут:
$query = "SELECT * FROM users WHERE id = " . $_GET['id'];
Только подготовленные выражения через PDO или ORM:
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $request->get('id')]);