# Phel is a Clojure-inspired language that compiles to plain PHP

> Phel is a Lisp-inspired language for PHP developers, with macros, persistent data structures and a REPL, and it targets PHP 8.5. Its compiler emits ordinary PHP rather than a runtime, so the output runs anywhere PHP runs, and the project publishes a standalone PHAR with a signed release and a digest you are told to check.

**phel-lang/phel-lang** — A functional, Lisp-inspired language that compiles to PHP. Inspired by Clojure, Phel brings macros, persistent data structures, and expressive functional idioms to the PHP ecosystem.

- Repository: https://github.com/phel-lang/phel-lang
- Website: https://phel-lang.org
- Stars: 530 · Forks: 33
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/phel-lang-phel-lang

## A Lisp that compiles to PHP

Phel is described as a functional, Lisp-inspired language that compiles to PHP, inspired by Clojure, bringing macros, persistent data structures and expressive functional idioms to the PHP ecosystem.

The framing in the repository header is shorter: Lisp for PHP, macros, persistent data structures, REPL.

The compiler target is the important part of that. Phel does not run on a virtual machine of its own. It compiles to PHP, which means the output is a PHP file that runs on any PHP installation, with no Phel runtime sitting in front of it. That is the whole argument for the language in a PHP shop, because it removes the deployment objection entirely.

A Lisp in a PHP shop is also an unusual combination, and the project does not hide the borrowings. The topics list Clojure, Lisp, Scheme, s-expression and REPL alongside PHP, and the description says it is inspired by Clojure. The parentheses, the reader macros with a hash, the threading macro, the persistent maps and vectors, the REPL with a numbered prompt: this is Clojure's shape, aimed at people whose day job is PHP.

The version requirements are stated plainly and they are recent. PHP 8.5 or newer. Linux and macOS are supported, with the full compiler, core and PHAR suites running on both in CI, and a failure on either blocks a release. Windows is best effort, running a reduced suite.

That platform detail is a deliberate policy rather than an accident. Two platforms are fully gated on the test suites, and the third is explicitly reduced. The documentation also points at a stability policy document for what a version number promises and for how long.

The project is MIT licensed, PHP as the primary language, with the default branch main and the homepage at phel-lang.org. The releases are unusually named: v0.54.0 is Road to One, v0.53.0 is Floor Raised and v0.52.0 is Honest Output. Those are milestones rather than version bumps, and they tell you the project is working towards a first stable release with a visible path.

## Installing and the REPL

The installation is one Composer command:

```sh
composer require phel-lang/phel-lang
```

And then the REPL is one command more:

```sh
./vendor/bin/phel repl
```

The session that follows is short enough to read in one go and shows the language working:

```clojure
user:1> (->> [1 2 3 4 5] (filter odd?) (map #(* % %)) (reduce +))
35
user:2> (defn greet [who] (str "Hello, " who "!"))
#'user/greet
user:3> (greet "Phel")
"Hello, Phel!"
```

The first line is a threading macro with a filter, a map and a reduce, and the answer is 35. The second defines a function and the REPL echoes back the var form. The third calls it.

The threading macro is the detail worth pausing on, because it is how a Lisp dialect stays readable for people who did not grow up with it. The expression reads left to right, the way the pipeline would read in PHP or in Python, and the parentheses are still there for the machine.

The REPL also shows the naming convention, with the var printed as user/greet, which is the Clojure namespace-qualified var naming that you either adapt to immediately or never do.

The prompt numbering is a small touch that matters in practice. user:1 then user:2 means you can refer back to an earlier form, and the number stays stable as you continue rather than resetting.

There is also a compiler CLI reference document in the repository that covers every command and the development loop, specifically the difference between compile, eval, run and build. Those four are the ones a user has to understand and they are not the same thing, which is why the documentation spends a page on them.

## Scaffolding a project and the four commands that matter

Scaffolding is one command:

```sh
./vendor/bin/phel init         # add `--minimal` for a single-file layout
```

It creates a phel-config.php, a src/main.phel and a tests/main_test.phel, and with the minimal flag the two sources sit at the project root instead.

Having a configuration file at the project root, rather than a configuration file per project in some other format, is the decision that makes Phel feel native to a PHP developer. phel-config.php is a PHP file. The tool reads the configuration with the machinery you already have.

Then there are four commands that between them are the whole development loop:

```sh
./vendor/bin/phel run src/main.phel        # run
./vendor/bin/phel test                     # tests
./vendor/bin/phel build                    # compile to PHP
./vendor/bin/phel config                   # inspect the merged config
```

Run executes a source file. Test runs the tests. Build compiles to PHP. And config inspects the merged configuration.

The config command is the one that most tools do not have and it is the one worth using. When a configuration value comes from several places, the question of which one wins is a recurring source of confusion, and a command that prints the merged result ends that. The options documentation also says that command line arguments are merged in with the project values and that list options are merged by concatenation while other options use the project value, which is a clear and predictable rule.

Inline evaluation is the other piece of the loop:

```sh
./vendor/bin/phel eval '(+ 1 2)'           # prints 3
echo '(println "hi")' | ./vendor/bin/phel eval -
./vendor/bin/phel eval - < script.phel
```

The first prints 3 for a sum, the second pipes a form in on standard input, and the third runs a script from standard input. The dash as the argument is the convention for reading from the pipe rather than from a literal expression, and it is the same convention the Clojure command line uses.

There is also optional shell completion, which dumps a tab-completion script for bash, zsh or fish, and it completes commands, their options and dynamic values such as function names for the doc command and project namespaces for run and test.

The zsh script has a wrinkle that the documentation explains. It starts with a compdef line for phel, so completion only triggers for a binary named phel on your path. If you call the vendor binary directly, you need a symlink with the global name, and then you remove the zcompdump files so that compinit rebuilds. That is a real annoyance with zsh completion scripts and the fact that the documentation walks through it, including the Homebrew paths, is the kind of care that distinguishes a tool people use from one they work around.

## The language, through four examples

The documentation gives four worked examples, and together they cover what Phel is actually for.

The first is a data pipeline:

```clojure
(def users
  [{:name "Ada" :age 36}
   {:name "Bob" :age 17}
   {:name "Cam" :age 41}])

(->> users
     (filter #(>= (:age %) 18))
     (map :name)
     sort)
;; => ["Ada" "Cam"]
```

A vector of maps, a filter on the age, a map to the name and a sort. The result is the two names of the adults. The interesting part is that the filter is written with a lambda rather than a named predicate, which is a choice about what a pipeline is for.

The second is an HTTP response, and it is the one that shows the intended use inside a PHP application:

The namespace requires the phel.http namespace, the request is built from the PHP globals, and the response is emitted from a map with a status, a headers map and a body. So Phel code is handling part of a request lifecycle inside a PHP application, which is a much more modest and much more achievable goal than replacing the application.

The third is macros:

```clojure
(defmacro unless [pred & body]
  `(if (not ~pred)
     (do ~@body)))

(unless (zero? 1)
  (println "not zero"))
;; => not zero

(unless false (println "ok"))
;; => ok
```

A macro named unless that expands to an if with a not, written with the syntax quote and the unquote inside it. This is the Clojure macro system, and it is the feature that the language description leads with.

The fourth is PHP interop:

```clojure
(ns app)

(def now (new DateTime))
(.format now "Y-m-d")
;; => "2026-04-20"

(def epoch (new DateTime "1970-01-01"))
(.-days (.diff now epoch))
;; => 20564
```

Two ways of calling a PHP object. The first creates a DateTime and calls a method on it with the dot syntax. The second creates a DateTime with an argument, calls the diff method and then reads a property from the returned object with the arrow notation.

Both forms are worth knowing because they cover different cases. The dot form is a method call, and the arrow form is a property access, and the fact that PHP has two different syntaxes for those two things is reflected rather than hidden.

What the four examples have in common is that they are all about bridging. Phel code calls PHP objects, Phel code emits an HTTP response into a PHP application, and Phel code manipulates Clojure-style data structures. The language is not trying to be a PHP replacement, it is trying to be the functional layer inside one.

## The PHAR, and how to check it

The standalone PHAR section is the most careful piece of documentation in the repository, and it is worth reading for the process rather than the tool.

Each release attaches a phel.phar, a single file you can run without Composer. And then the instruction that matters: check it against the sha256 digest GitHub shows for the asset before you run it. The reason is given plainly. The PHAR carries its own hash, but anyone who rebuilds it can recompute that.

That is a subtle and correct point. A checksum inside the artifact it checks proves nothing, because whoever modified the artifact could have updated the checksum too. The only meaningful check is against a digest published somewhere the attacker does not control, and the release page is that somewhere.

The commands are given for both macOS and Linux:

```sh
tag=v1.0.0    # the release you want
gh release download "$tag" --repo phel-lang/phel-lang --pattern phel.phar
expected=$(gh release view "$tag" --repo phel-lang/phel-lang --json assets \
  --jq '.assets[] | select(.name == "phel.phar") | .digest | ltrimstr("sha256:")')
echo "$expected  phel.phar" | shasum -a 256 -c -    # macOS and Linux
echo "$expected  phel.phar" | sha256sum -c -        # Linux, if shasum is missing
```

The script downloads the asset for a given tag, reads the expected digest out of the release metadata, and compares it with the computed one. The two checksum commands are both there because shasum is missing on some Linux systems, and sha256sum is missing on macOS.

The success message is spelled out. `phel.phar: OK` means the file matches. Anything else, delete the file and do not run it. And without the gh command, the instructions are to copy the digest from the asset list on the release page and run shasum or sha256sum by hand.

The release tags are GPG-signed as well, and the documentation tells you how to verify that in a clone by running git tag -v with the maintainer's public key imported.

So there are two independent checks. The digest proves the file you downloaded is the file that was published. The signature proves the tag points at a commit the maintainer signed. Neither is much work, and doing both is the difference between running a binary someone published and running a binary you have reason to trust.

## The repository, and what the tooling says

The repository layout shows a project with an unusually complete quality setup.

There is phpstan.neon and psalm.xml at the root, and the badges state PHPStan level 9 and Psalm level 1. Level 9 is the strictest PHPStan level, which means the type analysis is as close to total as that tool goes. Psalm level 1 is the loosest level, so the two are configured in opposite directions, which is a common pairing since they catch different things.

There is also a Shepherd badge for Psalm type-coverage status, which measures how much of the codebase has type information, and an infection.json5, which is the configuration for mutation testing. Mutation testing is the practice of changing your code in small ways and checking that your tests notice, and it is the most rigorous answer to the question of whether your tests mean anything.

The rest of the tooling follows the same pattern of thoroughness. There is phpbench.json for benchmarking, phpunit.xml.dist for the test runner, rector.php for automated refactoring, a php-cs-fixer configuration for code style, an .editorconfig, and a phel-lint.phel which is presumably a linter written in Phel itself.

There is a CHANGELOG.md, a CITATION.cff for machine-readable citation metadata, and an .agnostic-ai/ directory with an agnostic-ai.yaml at the root, which appears to be a configuration for tooling that adapts to different AI assistants.

The .env.example is worth reading because it describes a tool that is not part of the language but is part of how the project is maintained. It holds a PR label mapping for a script called tools/create-pr, with a semicolon-separated list of label names and the PR title template. So there is a script that creates pull requests with the right labels and title, which is the kind of automation a project with many small contributions accumulates.

There are also docker/ and scripts/ directories, a build/ directory, resources/, tools/, tests/ and src/, plus a phel.ini and a phel-config.php at the root, and a module-rules.json.

The two workflow badges are named tests and quality, which tells you the continuous integration is split along those lines. The documentation site is at phel-lang.org with a getting started guide and a CLI reference, and there are separate template repositories for web and command line projects that you can start from with a click.

## Conclusion

Adopt Phel if you work in PHP and want macros and immutable data structures without leaving the PHP runtime and toolchain, since the compiler output is ordinary PHP. Skip it if your PHP version is below 8.5, since that is a hard requirement, and if you need the maturity of a language with a decade of ecosystem behind it. Verify first how you will install it, since the Composer package and the standalone PHAR have different trust properties, and that you check the PHAR digest and the GPG signature of the release tag before running it.

## FAQ

### What is Phel and what does it require?

Phel is a functional, Lisp-inspired language that compiles to PHP, inspired by Clojure, bringing macros, persistent data structures and expressive functional idioms to the PHP ecosystem. It requires PHP 8.5 or newer. Linux and macOS are supported with the full compiler, core and PHAR suites running in CI and a failure on either blocking a release, while Windows is best effort and runs a reduced suite.

### How do I install Phel and start a project?

You install it with `composer require phel-lang/phel-lang`, open a REPL with `./vendor/bin/phel repl`, and scaffold a project with `./vendor/bin/phel init`, which creates phel-config.php, src/main.phel and tests/main_test.phel, or a single-file layout with the minimal flag. From there you run `./vendor/bin/phel run src/main.phel`, `./vendor/bin/phel test`, `./vendor/bin/phel build` to compile to PHP, and `./vendor/bin/phel config` to inspect the merged configuration.

### How do I verify the standalone Phel PHAR before running it?

Check it against the sha256 digest GitHub shows for the asset before you run it, because the PHAR carries its own hash and anyone who rebuilds it can recompute that. The documented commands download the phel.phar asset for a tag, read the expected digest from the release metadata, and compare it with shasum on macOS or sha256sum on Linux. If the result is not `phel.phar: OK`, delete the file and do not run it. The release tags are also GPG-signed and can be checked with git tag -v.

## Sources

- [License: MIT](https://github.com/phel-lang/phel-lang/blob/main/LICENSE)
- [phel-lang/phel-lang on GitHub](https://github.com/phel-lang/phel-lang)
- [Project website](https://phel-lang.org)
- [README](https://github.com/phel-lang/phel-lang/blob/main/README.md)
- [Releases](https://github.com/phel-lang/phel-lang/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/phel-lang-phel-lang
