A lightweight, embeddable scripting language for PHP applications.
📖 Documentation complète : docs-kodiscript.dickode.net
composer require issadicko/kodi-scriptuse KodiScript\KodiScript;
// Simple evaluation
$result = KodiScript::eval('2 + 3 * 4');
echo $result; // 14
// With variables
$result = KodiScript::run('greeting + ", " + name + "!"', [
'greeting' => 'Hello',
'name' => 'World',
]);
echo $result->value; // "Hello, World!"
// Capture output
$result = KodiScript::run('
let items = ["apple", "banana", "cherry"]
for (item in items) {
print(item)
}
');
print_r($result->output); // ["apple", "banana", "cherry"]- Variables:
let x = 10 - Functions:
fn(a, b) { return a + b } - Control Flow:
if,else,for-in,while - Data Types: numbers, strings, booleans, arrays, objects
- Null Safety:
?.(safe access),?:(elvis operator) - 50+ Native Functions: string, math, random, type, array, JSON, encoding, crypto, date/time
KodiScript is designed to be extensible. You can add your own native functions.
$result = KodiScript::builder('greet("PHP")')
->registerFunction('greet', fn($name) => "Hello, $name!")
->execute();
echo $result->value; // "Hello, PHP!"class ScriptService
{
public function __construct(
private UserRepository $userRepo,
private NotificationService $notifications
) {}
public function execute(string $script, array $context): ScriptResult
{
return KodiScript::builder($script)
->withVariables($context)
->registerFunction('fetchUser', fn($id) => $this->userRepo->find($id))
->registerFunction('sendEmail', fn($to, $subject, $body) =>
$this->notifications->sendEmail($to, $subject, $body))
->registerFunction('calculatePrice', fn($qty, $price, $discount = 0) =>
$qty * $price * (1 - $discount / 100))
->execute();
}
}Evaluates a script and returns the result value. Throws on error.
Runs a script with optional variables and returns ScriptResult.
Creates a builder for advanced configuration.
| Category | Functions |
|---|---|
| String | toString, toNumber, length, substring, toUpperCase, toLowerCase, trim, split, join, replace, contains, startsWith, endsWith, indexOf |
| Math | abs, floor, ceil, round, min, max, pow, sqrt, sin, cos, tan, log, log10, exp |
| Random | random, randomInt, randomUUID |
| Type | typeOf, isNull, isNumber, isString, isBool |
| Array | size, first, last, reverse, slice, sort, sortBy, unique, flatten, push, concat, range, sum, avg, has |
| Array (higher-order) | map, filter, reduce, find, findIndex, some, every, flatMap |
| Array (lists) | chunk, compact, pluck, zip, fromEntries, difference, intersection, union |
| Array (by key) | groupBy, countBy, indexBy, uniqueBy, sumBy, minBy, maxBy, partition |
| JSON | jsonParse, jsonStringify |
| Encoding | base64Encode, base64Decode, urlEncode, urlDecode |
| Crypto | md5, sha1, sha256 |
| Date/Time | now, date, time, datetime, timestamp, formatDate, parseDate, isDate, toISO, year, month, day, hour, minute, second, dayOfWeek, addDays, addHours, addMinutes, addSeconds, addMonths, addYears, diffDays, diffHours, diffMinutes, diffMonths, diffYears, age, startOfDay, endOfDay, startOfMonth, endOfMonth, daysInMonth, isWeekend |
All natives also work as methods: items.groupBy("type"), "2026-10-19".addDays(1).
A date argument is either a number (milliseconds since 1970-01-01T00:00:00Z, fraction truncated toward zero) or ISO text, trimmed:
YYYY-MM-DD(midnight UTC);YYYY-MM-DDTHH:mm,YYYY-MM-DDTHH:mm:ss,YYYY-MM-DDTHH:mm:ss.S…(1 to 9 fraction digits, kept to milliseconds by truncation); a space may replace theT;- a timed form may end with
Zor an offset+HH:mm/-HH:mm/+HHmm. Without a zone the time is UTC.
Fields are validated (2026-02-29 is not a date). Anything else (null, an unreadable string, a boolean, a list, an object) is unreadable, and a date function given an unreadable date returns null instead of raising. A count that is not a number (addDays("2026-10-19", "deux")) still raises.
Every calendar computation (year, month, day, hour, dayOfWeek, startOfDay, addMonths, …) is done in UTC, whatever the server's default time zone. dayOfWeek is 0 for Sunday.
formatDate(date, [format]) (default YYYY-MM-DD) and parseDate(text, format) share these tokens; any other character is copied (formatting) or must match exactly (parsing):
| Token | Meaning |
|---|---|
YYYY |
4-digit year |
YY |
last 2 digits of the year (formatting only; read as two literal characters by parseDate) |
MM DD |
month 01-12, day 01-31 |
HH mm ss |
hour 00-23, minute, second |
SSS |
millisecond 000-999 |
parseDate reads exactly the width of each token in digits, requires the whole text to be consumed, and returns null for an out-of-range value (31/02/2026). Missing fields default to January 1st, 00:00:00.000, and to the year 1970 when there is no YYYY.
toISO(date) returns YYYY-MM-DDTHH:mm:ss.SSSZ. addMonths and addYears clamp to the last day of the target month (addMonths("2026-01-31", 1) is 2026-02-28). diffDays, diffHours and diffMinutes are whole units from the first date to the second, truncated toward zero (negative when the second is earlier). diffMonths counts whole calendar months (diffMonths("2026-01-31", "2026-02-28") is 0), diffYears is that count divided by 12, and age(birth, [at]) is diffYears(birth, at) with at defaulting to now.
Numbers are read up to ±8 640 000 000 000 000 ms (year 275760); beyond that a date is unreadable.
A list argument may be null, which behaves as an empty list; any other non-list value raises. A path is a string of segments separated by .: on an object a segment is a key, on a list a zero-based index written in digits, anything else gives null; "" designates the element itself.
Two values are equal when they have the same type and the same canonical form, so 1 equals 1.0 but not "1", and {a: 1, b: 2} equals {b: 2, a: 1}.
chunk(list, size),compact(list),pluck(list, path),zip(list1, list2, …),fromEntries([[key, value], …]).difference(a, b),intersection(a, b),union(list1, list2, …).groupBy,countBy,indexBy,uniqueBy,sumBy,minBy,maxBy,partition, called asgroupBy(list, key), wherekeyis a path string or a functionfn(item, index). The group key ofgroupBy,countByandindexByis the canonical text of the key value (nullgives"null").sumByskipsnullkeys and raises on a non-number;minByandmaxByreturn the item (the first on ties) and skipnullkeys.partitionputs in the first list the items whose key is notnulland notfalse, as in the other SDKs.
PHP arrays serve both lists and objects: an array is a list when array_is_list() is true, so an object whose keys are 0, 1, 2… in order reads as a list. An empty object produced by groupBy, countBy, indexBy or fromEntries is an empty \stdClass, which prints as {}.
| Language | Package |
|---|---|
| Kotlin | Maven Central |
| Go | pkg.go.dev |
| Dart | pub.dev |
| TypeScript | npm |
MIT