Globals and constants#
Access PHP superglobals with php/ prefix and get:
(get php/$_SERVER "key") ; $_SERVER['key']
(get php/$GLOBALS "argv") ; $GLOBALS['argv']
PHP define constants accessed via php/CONSTANT_NAME:
(php/define "MY_SETTING" "My value") ; Calls PHP define('MY_SETTING', 'My value');
php/MY_SETTING ; => "My value"PHP Coming from PHP? ›
The php/ prefix gives you direct access to PHP's global scope:
// PHP
$_SERVER['key']
$GLOBALS['argv']
MY_SETTING
// Phel
(get php/$_SERVER "key")
(get php/$GLOBALS "argv")
php/MY_SETTING
Note: Use Phel's immutable data structures when possible. Only use PHP arrays when you need to interop with PHP libraries that expect them.
Calling PHP functions#
Add php/ prefix to any PHP function name:
(php/strlen "test") ; => 4
(php/date "l") ; => "Monday" (or whatever the current day is)PHP Coming from PHP? ›
Any PHP function can be called by adding the php/ prefix:
// PHP
strlen("test");
date("l");
array_map($fn, $array);
// Phel
(php/strlen "test")
(php/date "l")
(php/array_map fn array)
However, Phel provides functional equivalents for many operations. For example, use (count "test") instead of (php/strlen "test") when working with Phel data structures.
Namespaced PHP functions use full path after php/. Three equivalent forms accepted (last two are backslash-free):
(php/Foo\Bar\baz) ; classic backslash form
(php/Foo.Bar/baz) ; dot-separated, slash before fn name
(php/Foo.Bar.baz) ; fully dot-separated
(php/Amp.trapSignal [(php/:: SIGINT) (php/:: SIGTERM)])
Capture into a Phel alias:
(def trap-signal php/\Amp.trapSignal)
(trap-signal [2 15])Interop shorthands#
Terse forms that expand to verbose php/*. Use whichever reads better.
| Shorthand | Expands to |
|---|---|
(ClassName. args) | (php/new ClassName args) |
(new ClassName args) | (php/new ClassName args) |
(.method obj args) | (php/-> obj (method args)) |
(.-field obj) | (php/-> obj field) |
(ClassName/method args) | (php/:: ClassName (method args)) |
ClassName/MEMBER | (php/:: ClassName MEMBER) |
(ns my.module
(:use DateTimeImmutable DateInterval))
(DateTimeImmutable. "2026-04-20") ; constructor (preferred)
(.format (DateTimeImmutable.) "Y-m-d") ; instance method
(.-s (DateInterval. "PT30S")) ; property
(DateTimeImmutable/createFromFormat "Y-m-d" "2026-04-20") ; static method
DateTimeImmutable/ATOM ; static constantClass instantiation#
Three equivalent forms - prefer ClassName. for imported classes:
(ns my.module
(:use DateTime DateTimeImmutable))
(DateTime.) ; => DateTime instance (ClassName. shorthand)
(DateTime. "now") ; => DateTime instance with arg
(new DateTime) ; also valid
(php/new DateTime) ; also valid
(php/new "\\DateTimeImmutable") ; instantiate from string (dynamic)PHP Coming from PHP? ›
// PHP
new DateTime();
new DateTime("now");
new \DateTimeImmutable();
// Phel - preferred shorthand
(DateTime.)
(DateTime. "now")
(DateTimeImmutable.)
Import classes with :use to use the short ClassName. form without repeating the namespace.
Method and property call#
(php/-> object (methodname expr*))
(php/-> object property)
Calls method or accesses property. Both methodname and property must be symbols, not evaluated values.
Chain multiple in one php/->. Each element evaluates on result of previous, enabling fluent chains or nested property access.
(ns my.module
(:use DateInterval)
(:use DateTimeImmutable)
(:use stdClass))
(def di (DateInterval. "PT30S"))
(.format di "%s seconds") ; => "30 seconds" (.method shorthand)
(php/-> di (format "%s seconds")) ; same, verbose form
(.-s di) ; => 30 (.-prop shorthand)
;; Chain multiple calls:
;; (new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d")
(-> (DateTimeImmutable. "2024-03-10")
(.modify "+1 day")
(.format "Y-m-d"))
;; php/-> also works and is required for chains mixing methods and properties:
(php/-> user profile (getDisplayName))
;; Nested property access:
(def address (stdClass.))
(def user (stdClass.))
(php/oset (php/-> address city) "Berlin")
(php/oset (php/-> user address) address)
(php/-> user address city) ; => "Berlin"PHP Coming from PHP? ›
The php/-> operator is similar to PHP's -> but allows chaining in a more functional style:
// PHP
$di->format("%s seconds");
$di->s;
(new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d");
$user->profile->getDisplayName();
// Phel - shorthand forms
(.format di "%s seconds")
(.-s di)
(-> (DateTimeImmutable. "2024-03-10") (.modify "+1 day") (.format "Y-m-d"))
(php/-> user profile (getDisplayName)) ; mixed chains need php/->
Method calls: (.method obj args) shorthand or (php/-> obj (method args)). Property access: (.-prop obj) or (php/-> obj prop). Mixed chains (method + property in one expression) use php/-> directly.
Clojure Coming from Clojure? ›
The php/-> operator is inspired by Clojure's thread-first macro ->, but specifically designed for PHP object method chaining.
Static method and property#
(php/:: class (methodname expr*))
(php/:: class property)
Same as above, but static.
(ns my.module
(:use DateTimeImmutable))
DateTimeImmutable/ATOM ; => "Y-m-d\TH:i:sP" (shorthand)
(php/:: DateTimeImmutable ATOM) ; verbose form
(DateTimeImmutable/createFromFormat "Y-m-d" "2020-03-22") ; shorthand
(php/:: DateTimeImmutable (createFromFormat "Y-m-d" "2020-03-22")) ; verbosePHP Coming from PHP? ›
The php/:: operator is equivalent to PHP's :: for static method and property access:
// PHP
DateTimeImmutable::ATOM;
DateTimeImmutable::createFromFormat("Y-m-d", "2020-03-22");
// Phel - shorthand forms
DateTimeImmutable/ATOM
(DateTimeImmutable/createFromFormat "Y-m-d" "2020-03-22")
Named arguments#
PHP 8 named arguments are passed after a :& marker as :key value pairs. Works in php/new, php/->, and php/::. Keyword keys map to the PHP parameter names; order is then irrelevant.
(let [dt (php/:: \DateTime
(createFromFormat :& :format "Y-m-d" :datetime "2026-06-06"))]
(php/-> dt (format "Y-m-d"))) ; => "2026-06-06"PHP Coming from PHP? ›
// PHP
\DateTime::createFromFormat(format: "Y-m-d", datetime: "2026-06-06");
new \App\Mailer(host: "smtp", port: 587);
;; Phel
(php/:: \DateTime (createFromFormat :& :format "Y-m-d" :datetime "2026-06-06"))
(php/new \App\Mailer :& :host "smtp" :port 587)
By-reference arguments#
Some PHP functions write through a &$ref parameter (preg_match, sort, ...). Wrap a local binding in php/ref to pass it by reference; the local must be let-bound (a top-level def is not a PHP variable).
(let [subject "order-42"
matches (php/array)]
(php/preg_match "/(\d+)/" subject (php/ref matches))
(php/aget matches 1)) ; => "42"
php/ref also works inside php/-> / php/:: calls.
Set object properties#
(php/oset (php/-> object property) value)
(php/oset (php/:: class property) value)
Set value on class/object property.
(def x (stdclass.))
(php/oset (php/-> x name) "foo")PHP Coming from PHP? ›
php/oset is the Phel equivalent of PHP's property assignment:
// PHP
$x = new stdClass();
$x->name = "foo";
// Phel
(def x (stdclass.))
(php/oset (php/-> x name) "foo")
Note: This mutates the PHP object. When possible, use Phel's immutable data structures instead.
Type conversions#
Phel values and PHP values cross the boundary automatically for scalars (int, float, string, bool, nil). Collections differ: Phel uses immutable vectors/maps, PHP uses arrays. Convert explicitly when a library needs one or the other.
| Function | Direction | Example | Result |
|---|---|---|---|
to-php-array | Phel vector/map to PHP array | (to-php-array [1 2 3]) | <PHP-Array [1, 2, 3]> |
phel->php | deep Phel to PHP (nested) | (phel->php {:a 1 :b 2}) | <PHP-Array [a:1, b:2]> |
php->phel | deep PHP to Phel (nested) | (php->phel (php/array 1 2 3)) | [1 2 3] |
php-array-to-map | PHP array to Phel map | (php-array-to-map #php {"a" 1 "b" 2}) | {"a" 1, "b" 2} |
(to-php-array [1 2 3]) ; => <PHP-Array [1, 2, 3]>
(php->phel (php/array 1 2 3)) ; => [1 2 3]
(php-array-to-map #php {"a" 1}) ; => {"a" 1}
(phel->php {:a 1}) ; => <PHP-Array [a:1]>
Use #php [...] and #php {...} reader macros to write PHP array literals directly.
Checking types#
php/instanceof tests an object against a PHP class or interface:
(php/instanceof (php/new \DateTime) \DateTimeInterface) ; => true
For Phel's own values use the core predicates (int?, string?, map?, vector?, ...).
PHP functions as values#
A php/-prefixed function is a first-class value. Bind it, pass it, or spread arguments into it with apply:
(let [upcase php/strtoupper]
(map upcase ["a" "b"])) ; => @["A" "B"]
(apply php/max [3 7 2]) ; => 7
Capture a namespaced PHP function into a Phel alias the same way:
(def trap-signal php/\Amp.trapSignal)
(trap-signal [2 15])Magic methods on structs#
A defstruct is a real PHP class, so it can expose magic methods (__invoke, __toString, __get, ...) through an inline :php block. See Structs for the full form.
(defstruct money [cents]
:php
(__toString [this] (str "$" (/ (get this :cents) 100))))
(php/strval (money 500)) ; => "$5"Get PHP array value#
(php/aget arr index)
Equivalent: arr[index] ?? null.
(php/aget ["a" "b" "c"] 0) ; Evaluates to "a"
(php/aget (php/array "a" "b" "c") 1) ; Evaluates to "b"
(php/aget (php/array "a" "b" "c") 5) ; Evaluates to nilPHP Coming from PHP? ›
php/aget safely accesses PHP array elements:
// PHP
$arr[0] ?? null;
$arr[1] ?? null;
$arr[5] ?? null; // Returns null
// Phel
(php/aget arr 0)
(php/aget arr 1)
(php/aget arr 5) ; Returns nil
Important distinction:
- Use
php/agetfor PHP arrays (mutable) - Use
getfor Phel data structures (immutable vectors, maps)
Get nested PHP array value#
(php/aget-in arr path)
Resolves nested values via a sequence of keys/indexes. path is a sequential collection (e.g. vector). Missing step returns nil.
(def users
#php {"users"
#php {0 #php {"name" "Alice"}
1 #php {"name" "Bob"}}})
(php/aget-in users ["users" 1 "name"]) ; Evaluates to "Bob"
(php/aget-in
#php {"meta" #php {"status" "ok"}}
["meta" "status"]) ; Evaluates to "ok"
(php/aget-in
#php {"meta" #php {"status" "ok"}}
["meta" "missing"]) ; Evaluates to nilPHP Coming from PHP? ›
php/aget-in provides safe nested array access:
// PHP - manual nested access with null coalescing
$users['users'][1]['name'] ?? null;
$data['meta']['status'] ?? null;
$data['meta']['missing'] ?? null;
// Phel - clean path-based access
(php/aget-in users ["users" 1 "name"])
(php/aget-in data ["meta" "status"])
(php/aget-in data ["meta" "missing"]) ; Returns nil safely
This is similar to Phel's get-in for immutable data structures, but specifically for PHP arrays.
Set PHP array value#
(php/aset arr index value)
Equivalent: arr[index] = value.
PHP Coming from PHP? ›
php/aset mutates a PHP array in place:
// PHP
$arr[0] = "value";
// Phel
(php/aset arr 0 "value")
Important: This mutates the array. For immutable operations, use Phel's assoc on Phel data structures instead.
Set nested PHP array value#
(php/aset-in arr path value)
Creates or updates nested entries. Missing intermediate arrays are created.
(def data (php/array))
(php/aset-in data ["user" "profile" "name"] "Charlie")
(php/aget-in data ["user" "profile" "name"]) ; Evaluates to "Charlie"
;; Equivalent to $data['user']['profile']['name'] = 'Charlie';PHP Coming from PHP? ›
php/aset-in creates nested structures automatically:
// PHP - manual nested array creation
$data = [];
$data['user']['profile']['name'] = 'Charlie';
// Phel - automatic path creation
(def data (php/array))
(php/aset-in data ["user" "profile" "name"] "Charlie")
This is the mutable counterpart to Phel's assoc-in for immutable data structures.
Append PHP array value#
(php/apush arr value)
Equivalent: arr[] = value.
PHP Coming from PHP? ›
php/apush appends to a PHP array:
// PHP
$arr[] = "new value";
// Phel
(php/apush arr "new value")
For immutable operations, use conj on Phel vectors instead.
Unset PHP array value#
(php/aunset arr index)
Equivalent: unset(arr[index]).
PHP Coming from PHP? ›
php/aunset removes an element from a PHP array:
// PHP
unset($arr[0]);
// Phel
(php/aunset arr 0)
For immutable operations, use dissoc on Phel maps instead.
Unset nested PHP array value#
(php/aunset-in arr path)
Removes nested entry. Parent arrays remain untouched even if empty after.
(def data #php {"user" #php {"profile" #php {"name" "Dora"}}})
(php/aunset-in data ["user" "profile" "name"])
(php/aget-in data ["user" "profile" "name"]) ; Evaluates to nil
;; Equivalent to unset($data['user']['profile']['name']);PHP Coming from PHP? ›
php/aunset-in removes nested array elements:
// PHP
unset($data['user']['profile']['name']);
// Phel
(php/aunset-in data ["user" "profile" "name"])
Parent arrays remain intact even if they become empty after the unset.
__DIR__, __FILE__, *file*#
PHP magic constants __DIR__ and __FILE__ work but expand at PHP compile, pointing to the generated PHP file under .phel/cache.
For the original Phel source path, use *file* (absolute path of current Phel file). Combine with php/dirname for the source dir.
(println __DIR__) ; Directory name of the generated PHP file
(println __FILE__) ; Filename of the generated PHP file
(println (php/dirname *file*)) ; Directory of the original Phel file
(println *file*) ; Absolute path of the original filePHP Coming from PHP? ›
Important distinction:
// PHP magic constants
__DIR__ // Points to .phel/cache directory (generated PHP)
__FILE__ // Points to cached .php file
// Phel special var
*file* // Points to your actual .phel source file
Use *file* when you need to reference the original Phel source location, such as for loading resources relative to your source code.
Map to typed object and back#
hydrate and bean bridge a Phel map and a typed PHP object both ways: hydrate rebuilds an instance from a map (skipping the constructor, like an ORM rehydrating an entity), and bean reads an object's public properties back into a map with keyword keys.
;; class App\Point { public int $x; public int $y; }
(def p (hydrate "App\\Point" {:x 1 :y 2})) ; => App\Point instance
(bean p) ; => {:x 1 :y 2}
To read PHP 8 attributes and bridge native enums, see phel.reflect
(class-attributes, enum->keyword, ...) in the
API reference.
Native enums and exceptions#
defenum compiles to a native PHP backed enum (e.g. for Doctrine/Symfony columns), plus a Name? predicate. The enum is a real PHP type: consume it from PHP, reference it by full name (\my\ns\Status), or bridge cases to keywords with phel.reflect (see Reflection).
(defenum Status :active "active" :inactive "inactive")
;; emits: enum Status: string { case active = "active"; case inactive = "inactive"; }
defexception defines an exception extending a chosen parent, so framework catch blocks match it by type:
(defexception NotFound \RuntimeException)
(try
(throw (NotFound "missing"))
(catch \RuntimeException e (php/-> e (getMessage)))) ; => "missing"Reflection: attributes and enums#
phel.reflect reads PHP 8 attributes and bridges native enums (including defenum output) to keywords and back. Pass classes/enums by full name.
(ns my-app
(:require phel\reflect :as reflect))
Attributes come back as {:name :args} maps:
| Function | Reads |
|---|---|
class-attributes | attributes on a class |
method-attributes | attributes on a method |
property-attributes | attributes on a property |
;; #[Tag('x')] class Thing {}
(reflect/class-attributes \Demo\Thing)
; => [{:name "Demo\\Tag" :args {0 "x"}}]
Enum bridge:
| Function | Does |
|---|---|
enum-values | all cases as keywords |
enum->keyword | one case to its keyword |
keyword->enum | keyword back to the case |
;; enum Suit: string { case Hearts = 'H'; case Spades = 'S'; }
(reflect/enum-values \Demo\Suit) ; => [:Hearts :Spades]
(reflect/enum->keyword (php/:: \Demo\Suit Hearts)) ; => :Hearts
(reflect/keyword->enum \Demo\Suit :Spades) ; => Suit::SpadesCatching PHP exceptions#
PHP functions and methods throw native exceptions, and they cross the interop boundary unchanged. Catch them with try/catch, matching on the PHP class name. Catch \Throwable to handle anything.
(try
(php/intdiv 1 0)
(catch \DivisionByZeroError e
(php/-> e (getMessage))))
; => "Division by zero"
The .method shorthand and a finally clause work too:
(try
(risky-php-call)
(catch \Throwable e
(.getMessage e))
(finally
(cleanup)))
For Phel's own exceptions, ex-info, and re-throwing, see Error Handling.
Calling Phel from PHP#
Useful for integrating Phel into existing PHP apps. Load the Phel namespace after autoload.php.
Example: using-exported-phel-function.php
<?php
use Phel\Phel;
use PhelGenerated\CliSkeleton\Modules\AdderModule;
$projectRootDir = dirname(__DIR__);
require $projectRootDir . '/vendor/autoload.php';
Phel::run($projectRootDir, 'cli-skeleton.modules.adder-module');
$adder = new AdderModule();
$result = $adder->adder(1, 2, 3);
echo 'Result = ' . $result . PHP_EOL;
Two ways: manually, or via the export command.
Manually#
PhelCallerTrait calls any Phel function from a PHP class. Inject the trait, call callPhel.
<?php
use Phel\Interop\PhelCallerTrait;
class MyExistingClass {
use PhelCallerTrait;
public function myExistingMethod(...$arguments) {
return $this->callPhel(
'my.phel.namespace',
'phel-function-name',
...$arguments
);
}
}Using the export command#
phel export generates a wrapper class for all Phel functions marked export.
Set the withExportFromDirectories, withExportNamespacePrefix, and withExportTargetDirectory options in phel-config.php first: see Configuration.
Mark a function exported with metadata:
(defn my-function
{:export true}
[a b]
(+ a b))
phel export then generates a wrapper class in the target dir (here src/PhelGenerated). Use it from PHP to call Phel functions.
Typed and annotated output#
When the generated PHP must satisfy a framework's type expectations, opt-in metadata (^{:tag T}, ^{:php/attr [...]}, ^{:php/doc "..."}, ^:php/readonly, and more) enriches it; untagged forms are unchanged. For the full metadata table and a Doctrine-entity defstruct example, see Typed PHP from Phel definitions.
Next steps#
- Error Handling:
try,catch,finally,ex-info. - Configuration:
withExport*options forphel export. - PHP API reference: every
php/*builtin. - Rosetta Stone: PHP and Phel side by side, interop included.