You will learn
- Call any PHP function with
php/and pass it around as a value - Create objects with
ClassName., call methods with.method, and read properties with.-field - Use static methods and constants with
ClassName/member - Convert between PHP arrays and Phel collections with
to-array,phel->php, andphp->phel - Catch PHP exceptions and turn them into data
Read first
Phel compiles to PHP, so everything you know from PHP is still there: str_pad, DateTimeImmutable, json_encode, Composer packages. This module shows how to call that code from Phel. The one thing to watch is the border between the two worlds: PHP arrays are mutable, Phel collections are not, and you convert when data crosses.
Calling PHP functions
Put php/ in front of any PHP function name. PHP constants use the same prefix. In PHP you would write str_pad("7", 3, "0", STR_PAD_LEFT):
(php/str_pad "7" 3 "0" php/STR_PAD_LEFT) ; => "007"
php/PHP_INT_MAX ; => 9223372036854775807Exercise 1 Predict easy
What does each line return?
(php/strtoupper "phel")
(php/str_repeat "ab" 3)
(php/max 3 7 2)
Solution
(php/strtoupper "phel") ; => "PHEL"
(php/str_repeat "ab" 3) ; => "ababab"
(php/max 3 7 2) ; => 7
Same functions, same arguments, same results as in PHP. Only the call moves inside the parentheses.
Exercise 2 Fill in easy
A php/ function is a value, like any Phel function. Fill in the blank so each name starts with a capital letter:
(map ___ ["ada" "grace"]) ; => ("Ada" "Grace")
Hint
PHP has a function that uppercases the first character of a string.
Solution
(map php/ucfirst ["ada" "grace"]) ; => ("Ada" "Grace")
You pass php/ucfirst to map the same way you pass inc or str. No fn wrapper needed.
Learn more: PHP functions as values
Objects and methods
Add a dot after a class name to create an object: (DateTimeImmutable. "2024-03-10") is new DateTimeImmutable("2024-03-10"). Call a method with .method, and chain calls with ->:
;; (new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d")
(-> (DateTimeImmutable. "2024-03-10")
(.modify "+1 day")
(.format "Y-m-d")) ; => "2024-03-11"Exercise 3 Predict easy
DateTimeImmutable never changes. .modify returns a new object. What does this return?
(let [start (DateTimeImmutable. "2024-03-10")
later (.modify start "+1 day")]
[(.format start "Y-m-d") (.format later "Y-m-d")])
Solution
(let [start (DateTimeImmutable. "2024-03-10")
later (.modify start "+1 day")]
[(.format start "Y-m-d") (.format later "Y-m-d")])
; => ["2024-03-10" "2024-03-11"]
start keeps its value, like a Phel vector after conj. That is why DateTimeImmutable feels natural in Phel.
Exercise 4 Write medium
Write add-days. It takes a date string in Y-m-d format and a number of days, and returns the new date as a Y-m-d string.
(add-days "2024-03-10" 1) ; => "2024-03-11"
(add-days "2024-02-28" 2) ; => "2024-03-01"
Hint
.modify accepts strings such as "+2 days". Build that string with str, then chain with ->.
Solution
(defn add-days [date n]
(-> (DateTimeImmutable. date)
(.modify (str "+" n " days"))
(.format "Y-m-d")))
(add-days "2024-03-10" 1) ; => "2024-03-11"
(add-days "2024-02-28" 2) ; => "2024-03-01"
PHP handles the calendar (2024 is a leap year). Phel only builds the string and chains the calls.
Exercise 5 Fix medium
due-date should not touch the date it receives. But after the call, created has moved too. Fix it.
(defn due-date [start]
(.modify start "+30 days"))
(let [created (DateTime. "2024-01-01")
due (due-date created)]
[(.format created "Y-m-d") (.format due "Y-m-d")])
; => ["2024-01-31" "2024-01-31"], expected ["2024-01-01" "2024-01-31"]
Hint
The bug is not in due-date. Look at the class.
Solution
(defn due-date [start]
(.modify start "+30 days"))
(let [created (DateTimeImmutable. "2024-01-01")
due (due-date created)]
[(.format created "Y-m-d") (.format due "Y-m-d")])
; => ["2024-01-01" "2024-01-31"]
DateTime is mutable: .modify changes the object in place and returns it, so both names point to the same object. Prefer DateTimeImmutable in Phel code, for the same reason Phel collections are immutable.
Exercise 6 Write medium
.diff returns a DateInterval object. Its public property days holds the number of days. Read a property with .-field. Write days-between:
(days-between "2024-01-01" "2024-03-01") ; => 60
Hint
(.-days interval) is $interval->days in PHP.
Solution
(defn days-between [from to]
(.-days (.diff (DateTimeImmutable. from) (DateTimeImmutable. to))))
(days-between "2024-01-01" "2024-03-01") ; => 60
.diff is a method call, .-days is a property read. The dash tells them apart.
Static methods and class constants
ClassName/member calls a static method or reads a class constant, like ClassName::member in PHP:
DateTimeImmutable/ATOM ; => "Y-m-d\\TH:i:sP"Exercise 7 Fill in medium
Parse a European date with the static method createFromFormat and return it as Y-m-d. Fill in the blanks:
(-> (DateTimeImmutable/___ "d/m/Y" "22/03/2020")
(___ "Y-m-d"))
; => "2020-03-22"
Hint
The first blank is the static method name. The second is the method that turns a date into a string.
Solution
(-> (DateTimeImmutable/createFromFormat "d/m/Y" "22/03/2020")
(.format "Y-m-d"))
; => "2020-03-22"
In PHP: DateTimeImmutable::createFromFormat("d/m/Y", "22/03/2020")->format("Y-m-d").
PHP arrays and Phel collections
Numbers, strings, booleans, and nil cross between PHP and Phel unchanged. Collections do not. A Phel vector is not a PHP array, so convert at the border:
to-array: a Phel vector or map to a PHP array (one level).phel->php: Phel data to PHP arrays, nested.php->phel: PHP arrays to Phel vectors and maps, nested.
(php/implode ", " (to-array ["a" "b"])) ; => "a, b"
(php->phel (php/explode "," "a,b,c")) ; => ["a" "b" "c"]Exercise 8 Predict medium
php/explode returns a PHP array. What do these return?
(vector? (php/explode "," "red,green"))
(vector? (php->phel (php/explode "," "red,green")))
(count (php/explode "," "red,green"))
Hint
Core functions such as count and map can read a PHP array. That does not make it a vector.
Solution
(vector? (php/explode "," "red,green")) ; => false
(vector? (php->phel (php/explode "," "red,green"))) ; => true
(count (php/explode "," "red,green")) ; => 2
A PHP array prints as <PHP-Array ["red", "green"]>. Convert it with php->phel as soon as it enters your code, so the rest of your functions work with Phel data.
Exercise 9 Fix medium
This fails with implode(): Argument #2 ($array) must be of type ?array, Phel\Lang\Collections\Vector\PersistentVector given. Fix it.
(defn join-tags [tags]
(php/implode ", " tags))
(join-tags ["php" "lisp"]) ; expected "php, lisp"
Hint
The error tells you the type PHP wanted and the type it got.
Solution
(defn join-tags [tags]
(php/implode ", " (to-array tags)))
(join-tags ["php" "lisp"]) ; => "php, lisp"
PHP functions typed array need a real PHP array. For this task Phel has its own tool too: (phel.string/join ", " tags), but converting is the general fix for any PHP function.
Exercise 10 Write medium
ArrayObject is a mutable PHP object that wraps an array. Write append-all: wrap xs in an ArrayObject, .append every item of ys, then return a Phel vector.
(append-all [1 2] [3 4]) ; => [1 2 3 4]
Hint
(ArrayObject. php-array) takes a PHP array, so convert xs first. Use doseq for the side effect. .getArrayCopy gives you a PHP array back.
Solution
(defn append-all [xs ys]
(let [box (ArrayObject. (to-array xs))]
(doseq [y ys]
(.append box y))
(php->phel (.getArrayCopy box))))
(append-all [1 2] [3 4]) ; => [1 2 3 4]
The mutation stays inside the function. Callers give a vector and get a vector back. In real code (into xs ys) does this; the point here is the round trip.
JSON
php/json_encode expects PHP arrays, so convert with phel->php first. For decoding, pass true to get arrays instead of objects, then convert with php->phel:
(php->phel (php/json_decode "{\"a\":1,\"b\":[1,2]}" true))
; => {"a" 1, "b" [1 2]}Exercise 11 Predict medium
What does this return? Is it what you want?
(php/json_encode {:name "Ada"})
Hint
json_encode sees a PHP object, not an array.
Solution
(php/json_encode {:name "Ada"}) ; => "{}"
(php/json_encode (phel->php {:name "Ada"})) ; => "{\"name\":\"Ada\"}"
A Phel map is an object with no public properties, so PHP encodes it as {}. No error, but the data is wrong. Convert with phel->php; keyword keys become strings. Phel also ships a phel.json module with encode and decode that do the conversion for you.
Exercise 12 Write hard
Write order-total. It takes a JSON string with a list of items and returns the sum of price * qty.
(order-total "{\"items\":[{\"price\":5,\"qty\":2},{\"price\":3,\"qty\":1}]}")
; => 13
Hint
Decode into PHP arrays, convert with php->phel, then use the tools from Sequences. The keys are strings, not keywords.
Solution
(defn order-total [json]
(let [order (php->phel (php/json_decode json true))]
(->> (get order "items")
(map #(* (get % "price") (get % "qty")))
(reduce +))))
(order-total "{\"items\":[{\"price\":5,\"qty\":2},{\"price\":3,\"qty\":1}]}")
; => 13
Convert once at the border, then write plain Phel. A common mistake is (get % :price): JSON keys arrive as strings, so it returns nil.
Catching PHP exceptions
PHP exceptions reach Phel unchanged. Catch them by class name with a leading backslash:
(try
(php/intdiv 1 0)
(catch \DivisionByZeroError e
(.getMessage e)))
; => "Division by zero"Exercise 13 Write hard
(DateTimeImmutable. "tomorrow-ish") throws. Write parse-date that never throws: it returns {:ok "Y-m-d"} for a valid date and {:error "Invalid date: ..."} otherwise.
(parse-date "2024-03-10") ; => {:ok "2024-03-10"}
(parse-date "tomorrow-ish") ; => {:error "Invalid date: tomorrow-ish"}
Hint
Catch \Exception. The exact class changed between PHP versions, and every one of them extends Exception.
Solution
(defn parse-date [s]
(try
{:ok (.format (DateTimeImmutable. s) "Y-m-d")}
(catch \Exception e
{:error (str "Invalid date: " s)})))
(parse-date "2024-03-10") ; => {:ok "2024-03-10"}
(parse-date "tomorrow-ish") ; => {:error "Invalid date: tomorrow-ish"}
The PHP exception stops at the border and becomes data, the same pattern as in State and Errors. The rest of your code checks :ok or :error and never needs a try.
Learn more: Catch PHP exceptions
What you can do now
- You can call PHP functions, constants, and classes from Phel without a wrapper
- You can chain method calls on PHP objects with
-> - You know that Phel vectors and maps are not PHP arrays, and you convert at the boundary
- You can encode and decode JSON through PHP and get Phel data back
- You can catch a PHP exception and return an error as a map