What if code spoke the language of your business?
\n\nImagine: a sales manager writes a discount condition in plain English, and the system itself turns it into working code. Or an accountant describes a tax calculation with the phrase "If the amount is greater than 1000 and the client is from the EU, apply a 5% rate." Without a single line of PHP. Sounds like science fiction? Welcome to the world of domain-specific languages (DSL) in PHP.
\n\nMost developers know PHP as a web language, but few use its superpower — creating DSLs. This is not just a technical trick, but a powerful tool that turns chaos of business rules into elegant, readable code. In this article, we will not just talk about theory, but build a working DSL in PHP from scratch. You will see how DSL development can simplify life for both you and your clients.
\n\nWhat is a DSL and why do you need it in PHP?
\n\nA DSL (Domain Specific Language) is a language tailored to a specific task. Unlike a general-purpose programming language (like PHP, Java, or Python), a DSL speaks the language of your domain. Examples: SQL for databases, regular expressions for pattern matching, HTML for layout.
\n\nWhy should you create a DSL in PHP? Three main reasons:
\n\n- \n
- Readability. DSL code looks like instructions in Russian or English. You don't need to be a programmer to understand the logic. \n
- Security. You control what the user can do. No eval or shell_exec. \n
- Development speed. Business analysts and managers can describe rules themselves without distracting the development team. \n
In PHP, there are two main approaches to creating domain-specific languages: internal DSLs (built on PHP syntax) and external DSLs (requiring their own parser). We will start with the first one — it is simpler and often covers 80% of tasks.
\n\nApproach 1: Internal DSL — Fluent Interface and Method Magic
\n\nAn internal DSL is when you use PHP syntax but organize it so that it resembles natural language. The classic is a fluent interface. Let's write a DSL for building SQL queries to feel the magic right now.
\n\nExample: DSL for Building SQL Queries
\n\nInstead of writing:
\n\n$query = "SELECT * FROM users WHERE age > 18 AND city = 'Moscow' ORDER BY name ASC LIMIT 10";\n\nWe want to write like this:
\n\n$query = QueryBuilder::table('users')\n ->select('*')\n ->where('age', '>', 18)\n ->where('city', '=', 'Moscow')\n ->orderBy('name', 'ASC')\n ->limit(10)\n ->get();\n\nAgree, it reads like a sentence. The implementation is simple: each method returns $this, and at the end, the get() method assembles the string.
\n\nclass QueryBuilder {\n private array $parts = [];\n\n public function table(string $name): self {\n $this->parts['table'] = $name;\n return $this;\n }\n\n public function select(string $columns = '*'): self {\n $this->parts['select'] = $columns;\n return $this;\n }\n\n public function where(string $column, string $operator, $value): self {\n $this->parts['where'][] = "$column $operator " . (is_string($value) ? "'$value'" : $value);\n return $this;\n }\n\n public function orderBy(string $column, string $direction = 'ASC'): self {\n $this->parts['order'] = "$column $direction";\n return $this;\n }\n\n public function limit(int $limit): self {\n $this->parts['limit'] = $limit;\n return $this;\n }\n\n public function get(): string {\n $sql = "SELECT {$this->parts['select']} FROM {$this->parts['table']}";\n if (!empty($this->parts['where'])) {\n $sql .= " WHERE " . implode(' AND