Null safety
String and String? are different types. ?->, ?:, !! and smart casts make every null visible, and untyped PHP values must be narrowed before use.
StirPHP is a strictly typed superset of PHP. Write .sphp, let the checker catch the bugs, and ship plain, readable PHP 8.4 with PHPStan-ready PHPDoc.
On the left is what you write; on the right, what sphp build emits. The when is checked for exhaustiveness: drop a branch and the build fails.
namespace Shop
sealed interface Payment
data class Card(val last4: String) : Payment
data class Invoice(val days: Int) : Payment
record Line { name: String; cents: Int; qty: Int = 1 }
function total(lines: List<Line>): Int = lines->sumOf { it->cents * it->qty }
function describe(p: Payment?): String = when (p) {
is Card -> "card ending ${p->last4}"
is Invoice -> "invoice, due in ${p->days} days"
null -> "not paid yet"
}<?php
declare(strict_types=1);
namespace Shop;
require_once __DIR__ . '/stir-runtime.php';
interface Payment
{
}
final readonly class Card implements \Shop\Payment
{
public function __construct(
public string $last4,
) {
}
}
final readonly class Invoice implements \Shop\Payment
{
public function __construct(
public int $days,
) {
}
}
/** @param list<array{name: string, cents: int, qty: int}> $lines */
function total(array $lines): int
{
return \Stir\List_sumOf($lines, fn (array $it): int => $it['cents'] * $it['qty']);
}
function describe(?\Shop\Payment $p): string
{
return match (true) {
$p instanceof \Shop\Card => "card ending {$p->last4}",
$p instanceof \Shop\Invoice => "invoice, due in {$p->days} days",
$p === null => 'not paid yet',
};
}curl -fsSL https://stirphp.github.io/core/install.sh | shirm https://stirphp.github.io/core/install.ps1 | iexCreates sphp.toml and a first src/main.sphp.
Type-checks the project: StirPHP files strictly, your .php files at their PHPStan level.
Writes plain PHP to build/, with source maps. --watch rebuilds on save.