DSL на PHP: пишем предметно-ориентированные языки для бизнеса

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что, если бы код говорил на языке вашего бизнеса?

Представьте: менеджер по продажам пишет условие скидки на понятном ему английском, а система сама превращает это в рабочий код. Или бухгалтер описывает налоговый расчет фразой «Если сумма больше 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'                

Блоги

Рекомендаций по книгам