Что, если бы код говорил на языке вашего бизнеса?
Представьте: менеджер по продажам пишет условие скидки на понятном ему английском, а система сама превращает это в рабочий код. Или бухгалтер описывает налоговый расчет фразой «Если сумма больше 1000 и клиент из ЕС, применить ставку 5%». Без единой строчки PHP. Звучит как фантастика? Добро пожаловать в мир предметно-ориентированных языков (DSL) на PHP.
Большинство разработчиков знают PHP как язык для веба, но мало кто использует его суперсилу — создание DSL. Это не просто технический трюк, а мощный инструмент, который превращает хаос бизнес-правил в элегантный, читаемый код. В этой статье мы не просто поговорим о теории, а соберем рабочий DSL на PHP с нуля. Вы увидите, как разработка DSL может упростить жизнь и вам, и вашим заказчикам.
Что такое DSL и зачем он нужен в PHP?
DSL (Domain Specific Language) — это язык, заточенный под конкретную задачу. В отличие от общего языка программирования (как PHP, Java или Python), DSL говорит на языке вашей предметной области. Примеры: SQL для баз данных, регулярные выражения для поиска шаблонов, HTML для верстки.
Зачем вам создавать DSL на PHP? Три главные причины:
- Читаемость. Код на DSL выглядит как инструкция на русском или английском. Не нужно быть программистом, чтобы понять логику.
- Безопасность. Вы контролируете, что может сделать пользователь. Никаких eval и shell_exec.
- Скорость разработки. Бизнес-аналитики и менеджеры могут сами описывать правила, не отвлекая команду разработки.
В PHP есть два основных подхода к созданию предметно-ориентированных языков: внутренние DSL (строятся на синтаксисе PHP) и внешние DSL (требуют собственного парсера). Мы начнем с первого — он проще и часто покрывает 80% задач.
Подход 1: Внутренний DSL — Fluent Interface и магия методов
Внутренний DSL — это когда вы используете синтаксис PHP, но организуете его так, что он напоминает естественный язык. Классика — fluent interface (текучий интерфейс). Давайте напишем DSL для построения SQL-запросов, чтобы прямо сейчас ощутить магию.
Пример: DSL для построения SQL-запросов
Вместо того чтобы писать:
$query = "SELECT * FROM users WHERE age > 18 AND city = 'Moscow' ORDER BY name ASC LIMIT 10";
Мы хотим писать так:
$query = QueryBuilder::table('users')
->select('*')
->where('age', '>', 18)
->where('city', '=', 'Moscow')
->orderBy('name', 'ASC')
->limit(10)
->get();
Согласитесь, читается как предложение. Реализация проста: каждый метод возвращает $this, а в конце — метод get() собирает строку.
class QueryBuilder {
private array $parts = [];
public function table(string $name): self {
$this->parts['table'] = $name;
return $this;
}
public function select(string $columns = '*'): self {
$this->parts['select'] = $columns;
return $this;
}
public function where(string $column, string $operator, $value): self {
$this->parts['where'][] = "$column $operator " . (is_string($value) ? "'$value'" : $value);
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): self {
$this->parts['order'] = "$column $direction";
return $this;
}
public function limit(int $limit): self {
$this->parts['limit'] = $limit;
return $this;
}
public function get(): string {
$sql = "SELECT {$this->parts['select']} FROM {$this->parts['table']}";
if (!empty($this->parts['where'])) {
$sql .= " WHERE " . implode(' AND ', $this->parts['where']);
}
if (!empty($this->parts['order'])) {
$sql .= " ORDER BY {$this->parts['order']}";
}
if (!empty($this->parts['limit'])) {
$sql .= " LIMIT {$this->parts['limit']}";
}
return $sql;
}
}
Этот DSL на PHP уже решает проблему: вы не можете ошибиться с синтаксисом SQL, IDE подсказывает методы, а код выглядит как документация. Но это только начало.
Подход 2: Внешний DSL — пишем свой парсер на PHP
Иногда внутреннего DSL недостаточно. Например, если нужно, чтобы бизнес-правила писал человек, далекий от PHP. Тогда мы создаем внешний DSL — свой язык, который разбирается парсером. Это сложнее, но дает полную свободу.
Создаем DSL для бизнес-правил
Допустим, мы хотим, чтобы менеджер мог писать правила скидок так:
RULE: "Скидка для VIP"
IF total > 1000 AND client_type == "VIP"
THEN discount = 15%
Нам нужно написать парсер, который превратит этот текст в исполняемый код. Используем простой лексер и интерпретатор.
class RuleParser {
private array $tokens;
private int $pos = 0;
public function parse(string $rule): array {
$this->tokens = $this->lex($rule);
$this->pos = 0;
return $this->parseRule();
}
private function lex(string $input): array {
preg_match_all('/(RULE:|IF|THEN|AND|OR|>|<|==|!=|"[^"]*"|\d+%?|[a-zA-Z_]+)/', $input, $matches);
return $matches[0];
}
private function parseRule(): array {
$this->expect('RULE:');
$name = $this->expectString();
$this->expect('IF');
$condition = $this->parseCondition();
$this->expect('THEN');
$action = $this->parseAction();
return ['name' => $name, 'condition' => $condition, 'action' => $action];
}
private function parseCondition(): array {
$left = $this->parseComparison();
while ($this->peek() === 'AND' || $this->peek() === 'OR') {
$op = $this->advance();
$right = $this->parseComparison();
$left = ['type' => $op, 'left' => $left, 'right' => $right];
}
return $left;
}
private function parseComparison(): array {
$var = $this->expectIdentifier();
$op = $this->expect(['>', '<', '==', '!=']);
$val = $this->expectValue();
return ['type' => 'compare', 'var' => $var, 'op'