Cookbook

Practical recipes for common tasks in Phel. Each example is self-contained and ready to use.

Read and Process a CSV File#

Read a CSV file and parse it into a vector of maps, where each map represents a row with column headers as keys.

(ns cookbook\csv-reader)

# Read a CSV file and return a vector of maps
# Each row becomes a map with header names as keys
(defn read-csv [filepath]
  (let [handle (php/fopen filepath "r")]
    (if (not handle)
      (do
        (println (str "Error: cannot open " filepath))
        [])
      (let [headers (php/fgetcsv handle)
            header-keys (for [h :in headers] (keyword h))]
        (loop [rows []]
          (let [line (php/fgetcsv handle)]
            (if (= false line)
              (do
                (php/fclose handle)
                rows)
              (let [row (for [[i k] :pairs header-keys]
                          [k (php/aget line i)])]
                (recur (conj rows (apply hash-map (flatten row))))))))))))

# Example usage:
# Given a file "users.csv" with contents:
#   name,email,role
#   Alice,alice@example.com,admin
#   Bob,bob@example.com,editor

(def users (read-csv "users.csv"))
# => [{:name "Alice" :email "alice@example.com" :role "admin"}
#     {:name "Bob" :email "bob@example.com" :role "editor"}]

# Process the parsed data
(def admin-emails
  (->> users
       (filter |(= "admin" (get $ :role)))
       (map :email)))
# => ["alice@example.com"]

See also: PHP Interop, Data Structures

Build a Simple CLI Tool#

A command-line script that reads arguments, parses simple flags, and produces output.

(ns cookbook\cli-tool)

# Access command-line arguments via PHP's $argv
# When running: vendor/bin/phel run src/cli-tool.phel --name Alice --greeting Hi
(def args (let [argv (php/aget php/$_SERVER "argv")]
            # Skip the first two args (phel binary and script path)
            (for [i :range [2 (php/count argv)]]
              (php/aget argv i))))

# Parse flags into a map of --key value pairs
(defn parse-flags [args]
  (loop [remaining args
         flags {}]
    (if (empty? remaining)
      flags
      (let [current (first remaining)
            rest-args (rest remaining)]
        (if (php/str_starts_with current "--")
          (let [key (keyword (php/substr current 2))
                value (first rest-args)]
            (recur (rest rest-args) (assoc flags key value)))
          (recur rest-args flags))))))

# Build the tool
(defn run []
  (let [flags (parse-flags args)
        name (get flags :name "World")
        greeting (get flags :greeting "Hello")
        repeat-count (php/intval (get flags :repeat "1"))]
    (dotimes [_ repeat-count]
      (println (str greeting ", " name "!")))))

(run)
# Running: vendor/bin/phel run src/cli-tool.phel --name Alice --repeat 3
# Output:
#   Hello, Alice!
#   Hello, Alice!
#   Hello, Alice!

See also: PHP Interop, Control Flow

HTTP Request with cURL#

Make an HTTP GET request using PHP's cURL functions and parse a JSON response.

(ns cookbook\http-client)

# Perform an HTTP GET request and return the response body as a string
(defn http-get [url]
  (let [ch (php/curl_init)]
    (php/curl_setopt ch php/CURLOPT_URL url)
    (php/curl_setopt ch php/CURLOPT_RETURNTRANSFER true)
    (php/curl_setopt ch php/CURLOPT_FOLLOWLOCATION true)
    (php/curl_setopt ch php/CURLOPT_TIMEOUT 30)
    (let [response (php/curl_exec ch)
          error (php/curl_error ch)
          status (php/curl_getinfo ch php/CURLINFO_HTTP_CODE)]
      (php/curl_close ch)
      (if (= false response)
        {:error error :status 0}
        {:body response :status status}))))

# Parse a JSON string into a Phel map
(defn parse-json [json-string]
  (let [decoded (php/json_decode json-string true)]
    (if (nil? decoded)
      {:error (php/json_last_error_msg)}
      decoded)))

# Fetch data from a JSON API
(defn fetch-json [url]
  (let [result (http-get url)]
    (if (get result :error)
      result
      (parse-json (get result :body)))))

# Example: fetch a list of todos from a public API
(def response (fetch-json "https://jsonplaceholder.typicode.com/todos/1"))
# response is a PHP associative array, access with php/aget
(println (str "Title: " (php/aget response "title")))
(println (str "Completed: " (if (php/aget response "completed") "yes" "no")))

# Example: fetch multiple items and process them
(defn fetch-todos [limit]
  (let [data (fetch-json (str "https://jsonplaceholder.typicode.com/todos?_limit=" limit))]
    (for [i :range [0 (php/count data)]]
      (let [todo (php/aget data i)]
        {:id (php/aget todo "id")
         :title (php/aget todo "title")
         :completed (php/aget todo "completed")}))))

(def todos (fetch-todos 5))
(def completed-count (count (filter :completed todos)))
(println (str completed-count " of " (count todos) " todos completed"))

See also: PHP Interop

Generate HTML#

Use Phel's html module to generate HTML markup with nested elements, attributes, and dynamic content.

(ns cookbook\html-generator
  (:require phel\html :refer [html doctype raw-string]))

# Generate a simple page layout
(defn page [title & body]
  (html
    (doctype :html5)
    [:html {:lang "en"}
      [:head
        [:meta {:charset "UTF-8"}]
        [:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}]
        [:title title]
        [:style (raw-string "
          body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
          .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
          .badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
          .active { background: #d4edda; color: #155724; }
          .inactive { background: #f8d7da; color: #721c24; }
        ")]]
      [:body
        [:h1 title]
        body]]))

# Generate a user card component
(defn user-card [user]
  [:div {:class "card"}
    [:h3 (get user :name)]
    [:p (str "Email: " (get user :email))]
    [:span {:class [:badge (if (get user :active) "active" "inactive")]}
      (if (get user :active) "Active" "Inactive")]])

# Generate a navigation bar
(defn nav [links]
  [:nav
    [:ul {:style {:list-style "none" :display "flex" :gap "1rem" :padding "0"}}
      (for [link :in links]
        [:li [:a {:href (get link :url)} (get link :label)]])]])

# Build a complete page with dynamic content
(def users
  [{:name "Alice" :email "alice@example.com" :active true}
   {:name "Bob" :email "bob@example.com" :active false}
   {:name "Charlie" :email "charlie@example.com" :active true}])

(def links
  [{:label "Home" :url "/"}
   {:label "Users" :url "/users"}
   {:label "About" :url "/about"}])

(def output
  (page "User Directory"
    (nav links)
    [:p (str "Total users: " (count users))]
    (for [user :in users]
      (user-card user))))

(println output)

See also: HTML Rendering

Working with Dates#

Use PHP's DateTime classes via Phel interop to create, format, and compare dates.

(ns cookbook\dates
  (:use \DateTimeImmutable)
  (:use \DateInterval)
  (:use \DateTimeZone))

# Create dates
(def now (php/new DateTimeImmutable))
(def specific-date (php/new DateTimeImmutable "2024-06-15"))
(def from-format
  (php/:: DateTimeImmutable (createFromFormat "d/m/Y" "25/12/2024")))

# Format dates
(println (php/-> now (format "Y-m-d H:i:s")))       # 2024-03-10 14:30:00
(println (php/-> now (format "l, F j, Y")))          # Sunday, March 10, 2024
(println (php/-> specific-date (format "D, M j")))   # Sat, Jun 15

# Date arithmetic — add and subtract intervals
(def tomorrow
  (php/-> now (modify "+1 day")))
(def next-week
  (php/-> now (modify "+7 days")))
(def three-months-later
  (php/-> now (add (php/new DateInterval "P3M"))))

(println (str "Tomorrow: " (php/-> tomorrow (format "Y-m-d"))))
(println (str "Next week: " (php/-> next-week (format "Y-m-d"))))
(println (str "In 3 months: " (php/-> three-months-later (format "Y-m-d"))))

# Compare dates
(defn date-before? [a b]
  (< (php/-> a (getTimestamp)) (php/-> b (getTimestamp))))

(defn date-after? [a b]
  (> (php/-> a (getTimestamp)) (php/-> b (getTimestamp))))

(println (str "Tomorrow is after today: " (date-after? tomorrow now)))  # true

# Calculate the difference between two dates
(defn days-between [date1 date2]
  (let [interval (php/-> date1 (diff date2))]
    (php/-> interval days)))

(def start (php/new DateTimeImmutable "2024-01-01"))
(def end (php/new DateTimeImmutable "2024-12-31"))
(println (str "Days in 2024: " (days-between start end)))  # 365

# Work with time zones
(def utc-now (php/new DateTimeImmutable "now" (php/new DateTimeZone "UTC")))
(def tokyo-now
  (php/-> utc-now (setTimezone (php/new DateTimeZone "Asia/Tokyo"))))

(println (str "UTC:   " (php/-> utc-now (format "H:i:s"))))
(println (str "Tokyo: " (php/-> tokyo-now (format "H:i:s"))))

# Utility: human-readable relative time
(defn time-ago [date]
  (let [seconds (- (php/-> (php/new DateTimeImmutable) (getTimestamp))
                   (php/-> date (getTimestamp)))]
    (cond
      (< seconds 60) "just now"
      (< seconds 3600) (str (php/intval (/ seconds 60)) " minutes ago")
      (< seconds 86400) (str (php/intval (/ seconds 3600)) " hours ago")
      :else (str (php/intval (/ seconds 86400)) " days ago"))))

See also: PHP Interop

File System Operations#

Read files, write files, list directories, and check file existence using PHP interop.

(ns cookbook\filesystem)

# Read entire file contents
(defn read-file [path]
  (let [contents (php/file_get_contents path)]
    (if (= false contents)
      nil
      contents)))

# Write content to a file (creates or overwrites)
(defn write-file [path content]
  (let [result (php/file_put_contents path content)]
    (if (= false result)
      (do (println (str "Error: could not write to " path)) false)
      true)))

# Append content to a file
(defn append-file [path content]
  (let [result (php/file_put_contents path content php/FILE_APPEND)]
    (if (= false result)
      (do (println (str "Error: could not append to " path)) false)
      true)))

# Check if a file or directory exists
(defn exists? [path]
  (php/file_exists path))

(defn file? [path]
  (php/is_file path))

(defn directory? [path]
  (php/is_dir path))

# List directory contents, excluding . and ..
(defn list-dir [path]
  (if (not (directory? path))
    []
    (let [entries (php/scandir path)]
      (for [i :range [0 (php/count entries)]
            :let [entry (php/aget entries i)]
            :when (and (not= entry ".") (not= entry ".."))]
        entry))))

# List files matching a pattern
(defn glob-files [pattern]
  (let [matches (php/glob pattern)]
    (if (= false matches)
      []
      (for [i :range [0 (php/count matches)]]
        (php/aget matches i)))))

# Get file info
(defn file-info [path]
  (if (not (exists? path))
    nil
    {:path path
     :size (php/filesize path)
     :modified (php/filemtime path)
     :readable (php/is_readable path)
     :writable (php/is_writable path)}))

# Create directory recursively
(defn mkdir [path]
  (when (not (exists? path))
    (php/mkdir path 0755 true)))

# Example usage
(write-file "output/example.txt" "Hello from Phel!\n")
(append-file "output/example.txt" "Another line.\n")

(when (exists? "output/example.txt")
  (println (read-file "output/example.txt")))

# List all .phel files in a directory
(def phel-files (glob-files "src/**/*.phel"))
(foreach [f phel-files]
  (println (str "Found: " f)))

# Get info about each file
(def file-report
  (->> phel-files
       (map file-info)
       (sort-by :size)
       (reverse)))

See also: PHP Interop

Data Transformation Pipeline#

Take raw data, filter it, transform it, and group it using Phel's threading macros and collection functions.

(ns cookbook\data-pipeline)

# Sample dataset: a vector of user maps
(def users
  [{:name "Alice"   :age 32 :role "engineer" :active true}
   {:name "Bob"     :age 28 :role "designer" :active false}
   {:name "Charlie" :age 45 :role "engineer" :active true}
   {:name "Diana"   :age 35 :role "manager"  :active true}
   {:name "Eve"     :age 29 :role "designer" :active true}
   {:name "Frank"   :age 52 :role "manager"  :active false}
   {:name "Grace"   :age 38 :role "engineer" :active true}])

# Pipeline: get active users, uppercase names, sort by age, group by role
(def result
  (->> users
       (filter :active)                              # keep only active users
       (map |(assoc $ :name (php/strtoupper (get $ :name))))  # uppercase names
       (sort-by :age)                                # sort by age ascending
       (group-by :role)))                            # group into a map by role

# result =>
# {"engineer" [{:name "ALICE"   :age 32 ...}
#              {:name "GRACE"   :age 38 ...}
#              {:name "CHARLIE" :age 45 ...}]
#  "manager"  [{:name "DIANA"   :age 35 ...}]
#  "designer" [{:name "EVE"     :age 29 ...}]}

# Print a summary report
(foreach [role members result]
  (println (str "== " (php/strtoupper role) " (" (count members) ") =="))
  (foreach [m members]
    (println (str "  " (get m :name) " (age " (get m :age) ")"))))

# More pipeline examples:

# Average age of active users
(def avg-age
  (let [active (filter :active users)
        total-age (reduce + 0 (map :age active))]
    (/ total-age (count active))))
(println (str "Average age of active users: " avg-age))

# Find the oldest user per role
(def oldest-per-role
  (->> users
       (group-by :role)
       (map (fn [[role members]]
              [role (get (last (sort-by :age members)) :name)]))
       (apply hash-map (flatten $&))))

# Count users by status
(def status-counts
  {:active (count (filter :active users))
   :inactive (count (filter |(not (get $ :active)) users))})
(println (str "Active: " (get status-counts :active)
              ", Inactive: " (get status-counts :inactive)))

# Extract unique roles
(def roles
  (->> users
       (map :role)
       (into #{})))
(println (str "Roles: " roles))

See also: Data Structures, Control Flow

Simple Key-Value Store#

Build a persistent key-value store backed by a JSON file, with functions for get, put, delete, and listing keys.

(ns cookbook\kv-store)

# Path to the JSON storage file
(def default-store-path "data/store.json")

# Load the store from disk, returning a Phel map
(defn store-load [path]
  (if (not (php/file_exists path))
    {}
    (let [contents (php/file_get_contents path)]
      (if (or (= false contents) (= "" contents))
        {}
        (let [decoded (php/json_decode contents true)]
          (if (nil? decoded)
            {}
            # Convert PHP associative array to Phel map
            (for [[k v] :pairs decoded :reduce [m {}]]
              (assoc m k v))))))))

# Save the store to disk as JSON
(defn store-save [path data]
  (let [dir (php/dirname path)]
    (when (not (php/is_dir dir))
      (php/mkdir dir 0755 true))
    # Convert Phel map to PHP array for json_encode
    (let [php-arr (php/array)]
      (foreach [k v data]
        (php/aset php-arr k v))
      (php/file_put_contents
        path
        (php/json_encode php-arr php/JSON_PRETTY_PRINT)))))

# Get a value by key, with an optional default
(defn store-get
  ([key] (store-get default-store-path key nil))
  ([key default] (store-get default-store-path key default))
  ([path key default]
    (get (store-load path) key default)))

# Put a key-value pair into the store
(defn store-put
  ([key value] (store-put default-store-path key value))
  ([path key value]
    (let [data (store-load path)
          updated (assoc data key value)]
      (store-save path updated)
      updated)))

# Delete a key from the store
(defn store-delete
  ([key] (store-delete default-store-path key))
  ([path key]
    (let [data (store-load path)
          updated (dissoc data key)]
      (store-save path updated)
      updated)))

# List all keys in the store
(defn store-keys
  ([] (store-keys default-store-path))
  ([path] (keys (store-load path))))

# Check if a key exists
(defn store-has?
  ([key] (store-has? default-store-path key))
  ([path key] (contains? (store-load path) key)))

# Example usage
(store-put "user:1" "Alice")
(store-put "user:2" "Bob")
(store-put "config:theme" "dark")

(println (str "User 1: " (store-get "user:1")))           # Alice
(println (str "User 3: " (store-get "user:3" "unknown"))) # unknown
(println (str "Keys: " (store-keys)))                      # ("user:1" "user:2" "config:theme")

(store-delete "user:2")
(println (str "Has user:2? " (store-has? "user:2")))       # false

# Bulk operations using Phel's functional tools
(defn store-put-many [pairs]
  (let [path default-store-path
        data (store-load path)
        updated (reduce (fn [acc [k v]] (assoc acc k v)) data pairs)]
    (store-save path updated)
    updated))

(store-put-many [["lang" "phel"] ["version" "0.29"] ["status" "awesome"]])
(println (str "All keys: " (store-keys)))

See also: Data Structures, PHP Interop