Requires PHP 8.4+. Pick the method matching your workflow.
Which method?#
| Goal | Use |
|---|---|
| New project with tests + scripts | Composer skeleton |
| Add to existing Composer project | Composer require |
| Run a single file, no setup | PHAR |
| No PHP installed (Docker only) | Docker |
| Reproducible dev shells | Nix |
| Fastest path | Getting Started |
Composer (recommended)#
New project from skeleton#
Ships with tests, build config, ready-to-use composer scripts (repl, dev, test, build, format).
composer create-project --stability dev phel-lang/cli-skeleton example-app
cd example-app
composer replAdd to an existing project#
composer require phel-lang/phel-lang
vendor/bin/phel init my-app # scaffold phel-config.php + src/
All commands then via vendor/bin/phel <cmd> (e.g. vendor/bin/phel repl).
PHP Does this replace my PHP app? ›
No. Phel lives alongside PHP. require 'vendor/autoload.php' and call compiled Phel namespaces from PHP, or call PHP from Phel. Drop into any Composer project (Laravel, Symfony, WordPress plugin) and use where Lisp fits better.
PHAR (no project setup)#
Run without Composer. Good for quick experiments, CI one-shots, trying the language.
curl -L https://phel-lang.org/phar -o phel.phar
php phel.phar --version
Every command works the same:
php phel.phar repl
php phel.phar run src/main.phel
php phel.phar test --filter foo
Make it globally available:
chmod +x phel.phar
sudo mv phel.phar /usr/local/bin/phel
phel replDocker (no PHP required)#
No PHP installed? With Docker, run Phel in one command.
Zero-setup REPL#
Paste and you're in a live Phel REPL. No files, no install:
docker run --rm -it php:8.4-cli sh -c \
"curl -sL https://phel-lang.org/phar -o /tmp/phel.phar && php /tmp/phel.phar repl"
Container downloads PHAR fresh each run. Fine for experimenting, wasteful for daily use. See Persistent phel alias for a cached setup.
Run a Phel file from your host#
Mount cwd, run any Phel script:
docker run --rm -it -v "$PWD":/app -w /app php:8.4-cli sh -c \
"curl -sL https://phel-lang.org/phar -o /tmp/phel.phar && php /tmp/phel.phar run src/main.phel"Persistent phel alias backed by Docker#
Download PHAR once, make phel feel native:
curl -L https://phel-lang.org/phar -o phel.phar
# Add to ~/.zshrc, ~/.bashrc, or run in your shell:
alias phel='docker run --rm -it -v "$PWD":/app -w /app php:8.4-cli php /app/phel.phar'
phel repl
phel run src/main.phel
phel testComposer project with no local PHP#
Use official composer image (ships PHP + Composer):
docker run --rm -it -v "$PWD":/app -w /app composer \
create-project --stability dev phel-lang/cli-skeleton example-app
cd example-app
# Start the REPL
docker run --rm -it -v "$PWD":/app -w /app -p 2345:2345 composer composer repl
Alias for daily use:
alias dcomposer='docker run --rm -it -v "$PWD":/app -w /app composer'
dcomposer composer repl
dcomposer composer test
dcomposer composer dev
-p 2345:2345exposes default nREPL port for host editor integration. Omit if not needed.
Nix#
Reproducible dev environments. Phel is in nixpkgs: see phel on search.nixos.org or the package source.
No Nix yet? Install via Determinate Systems installer or official installer.
Ad-hoc shell#
nix shell nixpkgs#phel
phel repl
Nixpkgs may lag latest. Check
nix eval nixpkgs#phel.version. For newest, use Composer or PHAR.
Project shell.nix#
Pin PHP + Composer for the team:
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
packages = with pkgs; [
php84
php84Packages.composer
];
}
Then nix-shell and use Composer as normal.
Verify install#
Run the doctor:
vendor/bin/phel doctor ; Composer
php phel.phar doctor ; PHAR
phel doctor ; Nix / global
Checks PHP extensions (json, mbstring, readline), writable cache dir, source layout. Tells you exactly what's missing.
Clojure Mental model for the toolchain ›
Mapping from lein/deps.edn:
| Clojure | Phel |
|---|---|
deps.edn / project.clj | composer.json + phel-config.php |
lein new app foo | composer create-project … cli-skeleton foo |
clj / lein repl | composer repl or phel repl |
lein test | composer test or phel test |
uberjar | phel build (compiles to PHP) |
| nREPL | phel nrepl (bencode over TCP) |
Editor integration: nREPL + LSP. See Editor Support.
Upgrading to 0.49#
composer require phel-lang/phel-lang:^0.49
./vendor/bin/phel cache:clear # or: rm -rf .phel/cache
Always clear the cache after upgrading: compiled PHP from earlier installs references renamed core types and fails to load otherwise. Rebuild downstream projects too.
Behaviour changes in 0.49:
- No breaking changes. Existing code compiles as before.
partitionandpartition-allaccept Clojure's extra arities ([n step coll], plus[n step pad coll]forpartition); the[n coll]form is unchanged.- Sorted maps and sorted sets treat
NaNas equal to itself and ordered after every number, matching Clojure'scompare.(count (sorted-set NAN NAN))is now1instead of2. pr/prnprint char literals like\Aas one-char strings ("A").- Multi-arity functions emit fixed-arity
invokeArityNmethods, so build-mode calls with a known arity skip variadic dispatch (roughly 1.5-2x faster per call). - Optional
PhelConfig::withStripSymbolMeta()drops symbol metadata from compiled artifacts (-28% size, -40% cold require). With it on,phel docand(meta ...)over built defs return nil, and toggling forces a full recompile. PhelConfig::withAppModulePaths()scopes Gacela module discovery, sophel list:modulesandphel cache:warmno longer fatal on classes that cannot load standalone. Defaults to the previous whole-root walk.
New in 0.49: 20 new core fns, including every-pred, mapv, filterv, while, distinct?, bounded-count, map-invert, random-sample, the pr/prn/pr-str/prn-str family, the completed atom API (compare-and-set!, swap-vals!, reset-vals!), clojure.set-style relational helpers (select, project, rename, index), and subseq/rsubseq over sorted collections; a new phel\trace namespace (trace, trace-fn, deftrace, dotrace) in the spirit of clojure.tools.trace; and (tap> x) printing out of the box in the REPL. See the 0.49 release notes.
Upgrading to 0.48#
composer require phel-lang/phel-lang:^0.48
./vendor/bin/phel cache:clear # or: rm -rf .phel/cache
Always clear the cache after upgrading: compiled PHP from earlier installs references renamed core types and fails to load otherwise. Rebuild downstream projects too.
Behaviour changes in 0.48:
- No breaking changes. Existing code compiles as before.
- The compiled-code cache key now hashes only the
.phelsource, so a compiler-only upgrade no longer serves stale PHP; the cache index format bumped and invalidates old entries once on first run. phelno longer fatals in read-only / unwritable environments: caches degrade quietly and CLI commands report a clear error instead of aborting when a target file can't be written.- Squaring (
(** x 2)) andreduceover a typed vector now compile to native PHP; startup and emitted code shrink further via constant-slot sharing and leaner location metadata.
New in 0.48: new core fns (trampoline, reductions, subvec, with-open, reduce-kv, gcd, lcm, arity, variadic?, inspect, and dbg); a stepping debugger via (break) that opens a sub-REPL over the captured locals ((continue) or EOF resumes, so non-interactive runs never hang); phel test --coverage=html for a self-contained line-colored coverage report; and phel export stubs that carry native parameter/return types from :tag metadata. See the 0.48 release notes.
Behaviour changes in 0.47:
- No breaking changes. Existing code compiles as before.
phel testnow prints structural diffs (+/-/~) for any collection that differs, not just the first few entries, so assertion failures point straight at the mismatch.phel compileprints folded values to stderr when a form emits no PHP output, making constant folding visible instead of silent.- New projects scaffold with optimization level 2 enabled in
phel-config.php. Existing configs are untouched.
New in 0.47: LSP signature help now covers plain Phel calls like (map f xs) (arity, parameter names, docstring); nREPL eval responses carry per-session *1/*2/*3 value history so Calva and Conjure show the last three results; the REPL's (doc sym) renders function examples under an Example: heading; runtime errors name the .phel location instead of a compiled temp path; and startup is about 30% faster via OPcache re-execution. See the 0.47 release notes.
Behaviour changes in 0.46:
- Breaking: the deprecated
PhelConfigsetX()setters anduseLayout()/useNestedLayout()/useFlatLayout()were removed, along with thesetX()shims onPhelBuildConfig/PhelExportConfig. Use thewith*()methods inphel-config.phpinstead. - A broken
phel-config.phpnow fails with a clear error naming the file and expected structure (exit code 1) instead of an uncaught exception stack trace. phel buildnow exits non-zero when compilation aborts, instead of printing errors while exiting0. CI relying on the old exit code may start failing as intended.- The incremental build cache now cascades recompiles to dependent namespaces when a required namespace changes, preventing stale output reuse.
New in 0.46 (native path): config validation in phel config and phel doctor (relative paths, source/test dirs, optimization levels, types); phel build --timing for per-phase compile durations; phel init scaffolds configs with declare(strict_types=1);; an optional intermediate compile cache via withEnableIntermediateCache(); and a more resilient LSP that stays alive during idle periods and lists symbols from unsaved buffer edits. See the 0.46 release notes.
Behaviour changes in 0.45:
- Breaking: the runtime CLI-args var is now
*argv*(earmuffed), matching*program*and Clojure's*command-line-args*. The oldargvname was removed: replaceargvwith*argv*in scripts that read command-line arguments. - CLI flag renames with deprecated aliases kept:
index --output/-o(was--out),config --format=json(was--json). The old flags still work but warn on stderr. - Overflowing constant int arithmetic (
+/-/*) now folds toBigIntlike the runtime instead offloat. Float printing is consistent acrossstr/print/REPL (integer floats keep.0).
New in 0.45 (warm boot): the PHAR ships phel.core precompiled, cutting cold-start run/test/eval from ~1.2s to ~0.2s; a native-int arithmetic fast path (~1.8-8x per op); shell completion (bash/zsh/fish) plus CLI short aliases (r run, t test, b build, e eval); REPL/nREPL autocompletion of special forms and native symbols; and phel doctor OPcache reporting. See the 0.45 release notes.
Behaviour changes in 0.44:
- Requires
gacela-project/gacela: ^1.15. Editingphel-config.phptakes effect immediately again (the stale merged-config cache is cleared on change). phel testexit codes are stricter: it no longer exits0when nothing ran, bad paths/selectors fail loudly, and--listno longer appends a falseNo tests matched. CI relying on the old lenient codes may start failing as intended.await-all(andpmap, built on it) now return results in input order instead of completion order. Code that tolerated shuffled concurrent results sees deterministic ordering now.- The docs doctest harness (
composer test-docs,tests/doctest/) was removed; user-facing guides now live on phel-lang.org.
New in 0.44 (config tooling + sharper test runner): phel config prints the merged config with provenance, phel test --coverage and --watch, phel build --report, phel init --template=<name>, optimization levels (phel build -O <level>), LSP PHP interop, and a REPL reload workflow ((reload!), (run-tests ...)). See the 0.44 release notes.
Behaviour changes in 0.43:
- A
never/void/null:tagreturn on a value-returning function is now a compile error instead of a load-time fatal (mixed,?T, and union/intersection tags still pass).
New in 0.43 (typed PHP interop): php/callable first-class callables, defstruct ^:php/readonly fields, defenum methods + interfaces, ^:php/override (#[\Override]), and definterface typed class constants. See the 0.43 release notes.
Behaviour changes in 0.42:
- Structs print with a
.separator instead of\(e.g.(my.ns.point 1 2)). Snapshot tests or code that parses struct output must match the new form. str/index-ofreturnsnilfor an empty search string instead of throwing a PHPValueError.- Lexer columns are counted in code points, so error locations in multibyte source point at the right column.
if-let,when-let,if-some,when-firstare now hygienic: a user binding named like the macros' internal temporary no longer collides.
New in 0.42 (richer typed PHP interop, all opt-in):
phel.reflect: read PHP 8 attributes (class-attributes/method-attributes/ ...) and bridge native enums (enum->keyword/keyword->enum/enum-values).defenumnative backed enums anddefexceptionwith an optional parent class.php/refpasses a local by reference intophp/->/php/::and plain PHP calls likepreg_match/sort.hydrate/beanbridge a Phel map and a typed PHP object both ways.- PHP 8 named arguments in
php/new/php/->/php/::via the:&marker, e.g.(php/new \App\Mailer :& :host "smtp"). iterator-seqbuilds a lazy seq over any PHPTraversable.defstruct:phpblocks declare inline PHP magic methods;phel formatandphel.httpJSON bodies / response builders round it out.
See the 0.42 release notes for the full list.
Breaking changes in 0.41:
- Stricter argument errors:
takewith a non-int count,remove/select-keyson a non-seqable,int/long/float/doubleon non-numeric values, andget/assoc/updatewith non-int keys now raise clean Phel errors instead of leaking a PHPTypeError. Code that leaned on silent coercion must pass real values. - Clojure-aligned laziness:
map,filter,remove,concat,distinct, andrepeatedlyno longer realize their head eagerly,mapovernilreturns a lazy seq, andLazySeqno longer dropsnilvalues. Force withdoallorvecwhere you relied on eager evaluation.
Breaking changes in 0.40:
phel agent-install: the.agents/docs tree is now copied by default. The--with-docsflag is gone; use--no-docsto opt out.- Map destructuring with
:keys/:strs/:symsand a non-vector value now reports a shape error instead of silently dropping the binding.
Breaking changes in 0.39 (Clojure-aligned core type renames):
Variable→AtomUuid→UUIDBigInteger→BigIntRational→RatioPhelFuture→FutureExInfoException→ExceptionInfoLazyCons→Cons- Auto-refer: common
Phel\Lang\*types resolve without(:use ...).Interfacesuffix dropped (e.g.(php/instanceof x LazySeq)). User(:use ...)still overrides.
Earlier upgrades (0.37):
PhelConfigsetters replaced by immutablewithX()chain; oldsetX()shims emit deprecation notices. See Configuration.PhelConfig::forProject(ProjectLayout $layout = Flat, string $mainNamespace = ''): layout argument is first,Flatis the default.Phel\Printermoved toPhel\Shared\Printer. Phel sources should(:use Phel.Shared.Printer.Printer); the old path no longer resolves.- Cross-module exceptions +
CodeSnippetmoved toPhel\Shared\Exceptions/Phel\Shared\Parser\ReadModel. - Runtime state (cache, REPL history, error log) now lives under
.phel/. Override viawithPhelDir('...')or thePHEL_DIRenv var.
Next steps#
- Getting Started: first REPL session, project tour.
- Editor Support: Emacs, VS Code, IntelliJ, Vim.
- CLI Commands: every subcommand.
- Configuration:
phel-config.phpoptions.