Skip to main content

PHP Interop

8 min read
On this page

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.

ShorthandExpands 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 constant

Class 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")) ; verbose
PHP 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.

FunctionDirectionExampleResult
to-php-arrayPhel vector/map to PHP array(to-php-array [1 2 3])<PHP-Array [1, 2, 3]>
phel->phpdeep Phel to PHP (nested)(phel->php {:a 1 :b 2})<PHP-Array [a:1, b:2]>
php->pheldeep PHP to Phel (nested)(php->phel (php/array 1 2 3))[1 2 3]
php-array-to-mapPHP 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 nil
PHP 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/aget for PHP arrays (mutable)
  • Use get for 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 nil
PHP 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 file
PHP 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:

FunctionReads
class-attributesattributes on a class
method-attributesattributes on a method
property-attributesattributes on a property
;; #[Tag('x')] class Thing {}
(reflect/class-attributes \Demo\Thing)
; => [{:name "Demo\\Tag" :args {0 "x"}}]

Enum bridge:

FunctionDoes
enum-valuesall cases as keywords
enum->keywordone case to its keyword
keyword->enumkeyword 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::Spades

Catching 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#