This guide maps common PHP patterns to their Phel equivalents. If you already know PHP, you can use this page as a quick reference to start writing Phel productively. Each section shows familiar PHP code alongside the idiomatic Phel way of doing the same thing.
Variables and Constants#
In PHP, variables are mutable by default. In Phel, bindings are immutable -- you create new values instead of changing existing ones.
// PHP
$x = 42;
$x = 100; // Reassignment is fine
const TAX_RATE = 0.21;
define('APP_NAME', 'MyApp');
function example() {
$local = 10;
$local = 20; // Can mutate freely
}# Phel
(def x 42) # Global binding, cannot be redefined
(def tax-rate 0.21) # Constants are just defs
(def app-name "MyApp")
# Local bindings with let
(let [local 10
other (+ local 5)]
(+ local other)) # Evaluates to 25
# local and other do not exist outside the let block
Key difference: def and let bindings are immutable. You don't modify a value -- you create a new one. See Global and Local Bindings for more details.
If you need mutable state, Phel provides explicit variables:
(def counter (var 0))
(swap! counter inc) # counter is now 1
(deref counter) # Evaluates to 1Functions#
PHP functions map naturally to Phel's defn. The last expression in a Phel function is its return value -- no return statement needed.
// PHP
function add(int $a, int $b): int {
return $a + $b;
}
$double = fn($x) => $x * 2;
function greet(string $name = "World"): string {
return "Hello, $name!";
}
function sum(...$numbers): int {
return array_sum($numbers);
}# Phel
(defn add [a b]
(+ a b))
(def double |(* $ 2)) # Short anonymous function
# Multi-arity for default parameters
(defn greet
([] (greet "World"))
([name] (str "Hello, " name "!")))
(greet) # => "Hello, World!"
(greet "Phel") # => "Hello, Phel!"
# Variadic functions with &
(defn sum [& numbers]
(reduce + 0 numbers))
(sum 1 2 3 4) # => 10
The short anonymous function syntax | replaces PHP's arrow functions. Use $ for a single parameter, or $1, $2, etc. for multiple parameters:
|(+ $1 $2) # Same as fn($a, $b) => $a + $b
|(str "Hi " $) # Same as fn($x) => "Hi " . $x
See Functions and Recursion for the full reference.
Arrays to Vectors and Maps#
PHP uses a single array type for both indexed and associative arrays. Phel separates these into distinct immutable data structures.
Indexed arrays become vectors#
// PHP
$numbers = [1, 2, 3];
$numbers[] = 4; // Append
$first = $numbers[0]; // Access by index# Phel
(def numbers [1 2 3])
(def updated (conj numbers 4)) # => [1 2 3 4], numbers is unchanged
(get numbers 0) # => 1
(first numbers) # => 1Associative arrays become maps#
// PHP
$user = ['name' => 'Alice', 'age' => 30];
$user['email'] = 'alice@example.com'; // Add key
$name = $user['name']; // Access
unset($user['age']); // Remove key# Phel
(def user {:name "Alice" :age 30})
(def with-email (assoc user :email "alice@example.com"))
(get user :name) # => "Alice"
(:name user) # => "Alice" (keywords are functions!)
(dissoc user :age) # => {:name "Alice"}Quick reference#
| PHP | Phel | Notes |
|---|---|---|
$arr[] = $val | (conj vec val) | Returns new vector |
$arr['k'] = $v | (assoc map :k v) | Returns new map |
$arr['k'] | (get map :k) or (:k map) | |
unset($arr['k']) | (dissoc map :k) | Returns new map |
count($arr) | (count coll) | Works on all collections |
in_array($v, $arr) | `(some | (= $ v) coll)` |
array_key_exists | (contains? map :k) |
The critical difference: all operations return new collections. The original is never modified. See Data Structures for the full reference.
Control Flow#
if / else#
// PHP
if ($age >= 18) {
$status = 'adult';
} else {
$status = 'minor';
}# Phel
(def status (if (>= age 18) "adult" "minor"))when (if without else)#
// PHP
if ($debug) {
echo "Debug mode on";
log("enabled");
}# Phel
(when debug
(println "Debug mode on")
(log "enabled"))
when returns nil when the condition is false. Use it for side-effects or when you do not need an else branch.
switch becomes case#
// PHP
switch ($code) {
case 200: $msg = 'OK'; break;
case 404: $msg = 'Not Found'; break;
default: $msg = 'Unknown';
}# Phel
(def msg
(case code
200 "OK"
404 "Not Found")) # Returns nil if no matchmatch becomes cond#
// PHP
$label = match(true) {
$temp <= 0 => 'freezing',
$temp <= 20 => 'cold',
$temp <= 30 => 'warm',
default => 'hot',
};# Phel
(def label
(cond
(<= temp 0) "freezing"
(<= temp 20) "cold"
(<= temp 30) "warm"
:else "hot"))Truthiness difference#
This is a common gotcha for PHP developers:
// PHP falsy values: false, null, 0, "", "0", [], 0.0
if (0) { /* NOT reached */ }
if ("") { /* NOT reached */ }
if ([]) { /* NOT reached */ }# Phel: ONLY false and nil are falsy
(if 0 "truthy" "falsy") # => "truthy"
(if "" "truthy" "falsy") # => "truthy"
(if [] "truthy" "falsy") # => "truthy"
See Control Flow and Truth and Boolean Operations for more.
Loops#
Phel favors higher-order functions over explicit loops. Most PHP loops translate into map, filter, or reduce.
foreach#
// PHP
foreach ($items as $item) {
echo $item;
}
foreach ($map as $key => $value) {
echo "$key: $value";
}# Phel - side-effects only (returns nil)
(foreach [item items]
(println item))
(foreach [k v my-map]
(println (str k ": " v)))
# Phel - building a new collection (prefer this)
(for [item :in items] (process item))for loop#
// PHP
for ($i = 0; $i < 10; $i++) {
echo $i;
}# Phel - using loop/recur
(loop [i 0]
(when (< i 10)
(println i)
(recur (inc i))))
# Phel - using for comprehension (when building a collection)
(for [i :range [0 10]] i) # => [0 1 2 3 4 5 6 7 8 9]array_map, array_filter, array_reduce#
// PHP
$doubled = array_map(fn($x) => $x * 2, $numbers);
$evens = array_filter($numbers, fn($x) => $x % 2 === 0);
$sum = array_reduce($numbers, fn($carry, $x) => $carry + $x, 0);# Phel
(def doubled (map |(* $ 2) numbers))
(def evens (filter even? numbers))
(def sum (reduce + 0 numbers))
Notice how Phel's argument order differs from PHP: the function comes before the collection. This makes composition and threading natural.
Strings#
// PHP
$full = $first . " " . $last;
$len = strlen($greeting);
$formatted = sprintf("Hello, %s! You are %d.", $name, $age);
$upper = strtoupper($str);
$contains = str_contains($haystack, $needle);# Phel
(def full (str first " " last))
(def len (php/strlen greeting))
(def formatted (format "Hello, %s! You are %d." name age))
(def upper (php/strtoupper str))
(def contains (php/str_contains haystack needle))
Any PHP string function can be called with the php/ prefix. Phel provides str for concatenation and format for sprintf-style formatting. See PHP Interop for the full interop reference.
Classes and Objects#
Phel is not object-oriented, but it provides full interop with PHP's object system.
Creating objects#
// PHP
$now = new DateTime();
$date = new DateTimeImmutable('2024-01-15');# Phel
(ns my\module
(:use \DateTime)
(:use \DateTimeImmutable))
(def now (php/new DateTime))
(def date (php/new DateTimeImmutable "2024-01-15"))Calling methods and accessing properties#
// PHP
$formatted = $date->format('Y-m-d');
$timestamp = $date->getTimestamp();
$obj->name;
// Chaining
$result = (new DateTimeImmutable('2024-01-15'))
->modify('+1 month')
->format('Y-m-d');# Phel
(def formatted (php/-> date (format "Y-m-d")))
(def timestamp (php/-> date (getTimestamp)))
(php/-> obj name)
# Chaining
(def result
(php/-> (php/new DateTimeImmutable "2024-01-15")
(modify "+1 month")
(format "Y-m-d")))Static methods and constants#
// PHP
$atom = DateTimeImmutable::ATOM;
$parsed = DateTimeImmutable::createFromFormat('Y-m-d', '2024-03-22');# Phel
(def atom (php/:: DateTimeImmutable ATOM))
(def parsed (php/:: DateTimeImmutable (createFromFormat "Y-m-d" "2024-03-22")))
For data modeling, Phel uses structs and maps instead of classes:
(defstruct user [name email role])
(def alice (user "Alice" "alice@example.com" :admin))
(get alice :name) # => "Alice"
(assoc alice :role :editor) # => new struct with role changed
See PHP Interop for the complete reference.
Error Handling#
// PHP
try {
$result = riskyOperation();
} catch (InvalidArgumentException $e) {
$result = "Invalid: " . $e->getMessage();
} catch (RuntimeException $e) {
$result = "Runtime error";
} finally {
cleanup();
}
throw new RuntimeException("Something went wrong");# Phel
(def result
(try
(risky-operation)
(catch \InvalidArgumentException e
(str "Invalid: " (php/-> e (getMessage))))
(catch \RuntimeException e
"Runtime error")
(finally
(cleanup))))
(throw (php/new \RuntimeException "Something went wrong"))
The structure is similar to PHP's try/catch but expressed as a single form. See the exceptions section in Control Flow for more details.
Common Patterns#
Here are practical examples of real PHP code converted to idiomatic Phel.
Processing a list of users#
// PHP
$users = [
['name' => 'Alice', 'active' => true, 'age' => 30],
['name' => 'Bob', 'active' => false, 'age' => 25],
['name' => 'Charlie', 'active' => true, 'age' => 35],
];
$activeNames = array_map(
fn($u) => $u['name'],
array_filter($users, fn($u) => $u['active'])
);
// ['Alice', 'Charlie']# Phel
(def users
[{:name "Alice" :active true :age 30}
{:name "Bob" :active false :age 25}
{:name "Charlie" :active true :age 35}])
(def active-names
(->> users
(filter :active)
(map :name)))
# => ["Alice" "Charlie"]Building an API response#
// PHP
function jsonResponse(array $data, int $status = 200): array {
return [
'status' => $status,
'body' => json_encode($data),
'headers' => ['Content-Type' => 'application/json'],
];
}
$response = jsonResponse(['user' => 'Alice', 'role' => 'admin']);# Phel
(defn json-response
([data] (json-response data 200))
([data status]
{:status status
:body (php/json_encode (to-php-array data))
:headers {:content-type "application/json"}}))
(def response (json-response {:user "Alice" :role "admin"}))Working with dates#
// PHP
$now = new DateTimeImmutable();
$nextWeek = $now->modify('+7 days');
$formatted = $nextWeek->format('Y-m-d');
$isWeekend = in_array($now->format('N'), ['6', '7']);# Phel
(ns my\dates
(:use \DateTimeImmutable))
(def now (php/new DateTimeImmutable))
(def next-week (php/-> now (modify "+7 days")))
(def formatted (php/-> next-week (format "Y-m-d")))
(def weekend?
(let [day-of-week (php/-> now (format "N"))]
(or (= day-of-week "6") (= day-of-week "7"))))Reading a config file#
// PHP
$config = json_decode(file_get_contents('config.json'), true);
$dbHost = $config['database']['host'] ?? 'localhost';
$dbPort = $config['database']['port'] ?? 3306;# Phel
(def config
(let [raw (php/file_get_contents "config.json")]
(php/json_decode raw true)))
(def db-host (or (php/aget-in config ["database" "host"]) "localhost"))
(def db-port (or (php/aget-in config ["database" "port"]) 3306))Key Mindset Shifts#
Moving from PHP to Phel involves a few conceptual shifts:
- Data is immutable -- you do not modify data in place, you transform it into new values. The original is always preserved.
- Functions are values -- pass them as arguments, return them from other functions, store them in collections.
- Prefix notation -- the operator always comes first:
(+ 1 2)not1 + 2. This is consistent for everything, including function calls. - No return statement -- the last expression in a function body is its return value.
- No semicolons, no curly braces -- just parentheses. Indentation conveys structure visually; parentheses convey it to the compiler.
- Truthiness -- only
falseandnilare falsy.0,"", and[]are all truthy. This catches many PHP developers off guard at first. - Everything is an expression --
if,let,case, andcondall return values. There are no statements. - Thread-last (
->>) replaces method chaining -- instead of$arr->filter()->map()->sort(), use(->> coll (filter pred) (map f) (sort)).