Tip
Use the Tyhp alternatives for disabled features: declared properties or __get/__set instead of undeclared properties (except on \stdClass), use plus Composer instead of include/require, and \Throwable catch types.
Tier 0 · Story 10Complete
Tyhp is a typed superset of the PHP language. Some PHP features are disabled or restricted in <?tyhp so the compiler can type-check; .php files and <?php blocks are passed through. This page documents disabled features, restricted features, and preserved PHP features — along with the Tyhp alternatives for each restriction.
The following PHP features are disabled or strongly discouraged in Tyhp. Using them produces a compiler diagnostic as noted. For each, a Tyhp alternative is provided.
The eval() function is discouraged in Tyhp. It is a security risk (arbitrary code execution) and makes static analysis impossible — the compiler cannot type-check dynamically generated code. Using eval() produces informational diagnostic TYHP4800 (CheckerEvalUsage), not an error. Set build.allowEval: true in tyhp.json to suppress it.
<?tyhp
// INFO 4800: eval() usage (suppressed when build.allowEval is true)
// eval('echo "Hello";');
// ALTERNATIVE: Write the dynamic code in a PHP file
// and declare it via a tyhpdef file
// Or use closures/callables for dynamic behavior
If you absolutely need eval-like functionality, write that code in a plain PHP file and declare it into your Tyhp project via a tyhpdef file. This keeps the security boundary clear and the eval usage isolated from type-checked code. build.allowEval only suppresses the informational diagnostic; it does not change how eval behaves at runtime.
Short open tags (<?) and echo short tags (<?=) are not supported in Tyhp. Tyhp files must use the <?tyhp opening tag. PHP files in the same project can still use <?php.
// ERROR: Short open tags are not valid in Tyhp
// <? echo 'hello'; ?>
// <?= $variable ?>
// CORRECT: Use <?tyhp for Tyhp files
<?tyhp
echo 'hello';
echo $variable;
Undeclared property writes ($obj->newProp = value where newProp is not declared) are legal only when the receiver’s type is exactly the global engine class \stdClass. Other classes — including subclasses such as class Bag extends \stdClass — must declare the property or implement __set (TYHP4134). #[\AllowDynamicProperties] is not an opt-in (TYHP4126, and writes still TYHP4134).
Undeclared reads still require a declared property or __get; otherwise the access is unresolved.
The (object) cast follows PHP: if the operand is already an object (a concrete class, the builtin object type, or a union of only object types), the cast is identity — the value keeps its type and is not re-wrapped as \stdClass. That does not allow undeclared writes on those other types. If the operand is an array, scalar, null, or mixed (not already an object), the result type is \stdClass, so (object)[] accepts undeclared writes. The emitted PHP is still (object).
<?tyhp
class User {
public string $name;
}
User $user = new User();
$user->name = 'Alice'; // OK: declared property
// ERROR TYHP4134: 'email' is not declared on User
// $user->email = 'alice@example.com';
// OK: declare the property
class UserWithEmail {
public string $name;
public string $email;
}
// OK: __get / __set for a typed bag on a non-stdClass type
class FlexibleUser {
private array $extra = [];
public function __get(string $name): mixed {
return $this->extra[$name] ?? null;
}
public function __set(string $name, mixed $value): void {
$this->extra[$name] = $value;
}
}
\stdClass $bag = new \stdClass();
$bag->email = 'alice@example.com'; // OK: exact \stdClass
\stdClass $fromArray = (object)['email' => 'alice@example.com'];
$fromArray->extra = 1; // OK: (object)[] is \stdClass
class Bag extends \stdClass {}
Bag $subclass = new Bag();
// ERROR TYHP4134: subclasses are not \stdClass
// $subclass->email = 'alice@example.com';
// (object) on an existing object is identity — still User, still TYHP4134
// $user = (object)$user;
// $user->email = 'alice@example.com';
PHP allows a named function (or, in Tyhp, a named method) to be declared inside the body of
another function or method. PHP hoists the nested declaration to the global scope the first time
the enclosing callable runs — it does not close over the enclosing scope like a closure — which
does not fit Tyhp's static, per-file symbol model. Declaring a named function inside another named
function or method produces compiler diagnostic 4802.
<?tyhp
function outer(): void {
// ERROR 4802: named functions cannot be declared inside another function or method
// function nested(): void {}
}
class C {
public function go(): void {
// ERROR 4802: same restriction applies inside methods
// function nested(): void {}
}
}
// ALTERNATIVE: a private method
class Better {
public function go(): void {
$this->nested();
}
private function nested(): void {}
}
// ALTERNATIVE: a closure assigned to a typed local
function alsoBetter(): void {
$nested = function (): void {};
$nested();
}
A named function declared at file/namespace scope inside an if (!\function_exists(...)) guard is
unaffected by this restriction — that pattern does not nest the declaration inside another named
function or method.
PHP 8 requires every catch clause to name an exception type — untyped catch ($e) is not a PHP feature. Tyhp's checker validates the types that are present: they must be \Throwable (or a subtype), not scalars, and not intersection types.
An empty catch body (no statements) is TYHP4121. A comment-only body is still empty. When swallowing is intentional, name the exception and discard it with (void)$e — the same (void) discard used for unused #[\NoDiscard] returns. See PHP Engine Attributes.
<?tyhp
try {
riskyOperation();
} catch (\Throwable $e) {
echo $e->getMessage();
}
try {
riskyOperation();
} catch (\InvalidArgumentException $e) {
echo $e->getMessage();
} catch (\Throwable $e) {
echo $e->getMessage();
}
try {
$rp = new \ReflectionProperty($class, $name);
} catch (\ReflectionException $e) {
(void)$e;
}
Variable variables are prohibited (TYHP4133). There is no __VarName exception. Use variable_exists($name) on a simple variable when you need an existence check.
<?tyhp
string $greeting = 'Hello';
// ERROR TYHP4133: variable variables are prohibited
string $name = 'greeting';
// echo $$name;
// OK: existence check on a simple variable
if (variable_exists($greeting)) {
echo $greeting;
}
The following PHP dynamic features are still available in Tyhp but require type narrowing through type guard functions or the special __ types before use. This ensures the compiler can verify the referenced symbols exist. See the Dynamic Language Features page for full details.
Calling a function through a variable requires __FunctionName type or a \function_exists() guard.
<?tyhp
// ERROR: Plain string for variable function
string $fn = 'strtoupper';
// echo $fn('hello'); // Compiler error!
// OK: Narrowed to __FunctionName
__FunctionName $fn = 'strtoupper';
echo $fn('hello'); // OK
// OK: Using function_exists() guard
string $callback = getCallback();
if (\function_exists($callback)) {
echo $callback('hello'); // OK
}
Dynamic property access ($obj->$prop) and dynamic method calls ($obj->$method()) require the variable to be narrowed to __PropertyName<T> or __MethodName, or to pass \property_exists() or \method_exists() guards.
<?tyhp
class User {
public string $name = 'Alice';
public function greet(): string { return 'Hello!'; }
}
User $user = new User();
// ERROR: Plain string for dynamic access
string $prop = 'name';
// echo $user->$prop; // Compiler error!
// OK: Using property_exists() guard
if (\property_exists($user, $prop)) {
echo $user->$prop; // OK
}
// OK: Using __PropertyName<User>
__PropertyName<User> $field = 'name';
echo $user->$field; // OK
Creating an object from a variable class name requires __ClassName type or a \class_exists() guard.
<?tyhp
// ERROR: Plain string for dynamic instantiation
string $cls = 'App\\Models\\User';
// $obj = new $cls(); // Compiler error!
// OK: Using class_exists() guard
if (\class_exists($cls)) {
$obj = new $cls(); // OK
}
// OK: Using __ClassName
__ClassName $cls = 'App\\Models\\User';
$obj = new $cls(); // OK
include / include_once / require / require_once always produce compiler error TYHP4801 (CheckerIncludeNotAllowed). There is no static-path exception. The emitter injects vendor/autoload.php itself; a user-written require_once __DIR__ . '/vendor/autoload.php' still fails 4801. There is no import keyword — use use plus Composer autoloading.
<?tyhp
// ERROR 4801: include/require are not allowed
string $file = getIncludeFile();
// include $file;
// ERROR 4801: even a static Composer autoload path is rejected
// require_once __DIR__ . '/vendor/autoload.php';
// ALTERNATIVE: `use` the type; Composer (and the emitter's autoload injection) loads it
use App\Models\User;
use App\Services\UserService;
use App\Services\OrderService;
The extract() and compact() functions dynamically create or collect variables, which completely bypasses static analysis. The compiler reports errors TYHP4136 (CheckerExtractProhibited) and TYHP4135 (CheckerCompactProhibited).
<?tyhp
// ERROR TYHP4136: extract() is prohibited
// \extract($data);
// ALTERNATIVE: Destructure explicitly
string $name = $data['name'];
int $age = $data['age'];
// ERROR TYHP4135: compact() is prohibited
// $result = \compact('name', 'age');
// ALTERNATIVE: Build the array explicitly
array $result = ['name' => $name, 'age' => $age];
The global keyword is not unrestricted. Using global $var produces warning TYHP4137 (CheckerGlobalVariableWarning). Prefer importing PHP globals through tyhpdef (see Importing PHP Variables and Constants) or passing values as parameters.
Unlike PHP where type declarations are optional, Tyhp requires types everywhere. All variables, parameters, return types, and properties must have explicit type annotations or be typed by inference from the first assignment.
<?tyhp
// All variables must be typed or inferred
string $name = 'Alice'; // explicit type
$age = 30; // inferred as int from literal
// ERROR: No type and no initializer
// $unknown; // Compiler error: type required
// All function parameters must have types
function greet(string $name): string {
return "Hello, {$name}!";
}
// ERROR: Missing parameter type
// function bad($name): string { ... } // Compiler error!
// ERROR: Missing return type
// function bad2(string $name) { ... } // Compiler error!
// All class properties must have types
class User {
public string $name; // OK: typed
// public $email; // ERROR: missing type
}
All types in Tyhp are non-nullable by default. A variable declared as string $x cannot hold null — it must be explicitly declared as ?string $x (or string|null $x) to allow null. This is a fundamental difference from PHP.
<?tyhp
string $name = 'Alice';
// ERROR: Cannot assign null to non-nullable string
// $name = null; // Compiler error!
// OK: Explicitly nullable
?string $nickname = null;
$nickname = 'Ali'; // OK
$nickname = null; // OK: nullable
// OK: Union with null
string|null $title = null;
The following PHP features work the same in Tyhp, with no extra restrictions.
The following table summarizes the status of each changed feature, why it is restricted, and the Tyhp alternative.
build.allowEval; alternative: write PHP and declare via tyhpdef\stdClass; other types (including extends \stdClass) declare properties or use __get/__set (TYHP4134). (object) converts non-objects to \stdClass and is identity on existing objectsvariable_exists($name) on a simple variablevendor/autoload.php; use use + Composerglobal $var — WARNING TYHP4137 (CheckerGlobalVariableWarning) — not unrestricted\Throwable); untyped catch ($e) is not a PHP 8 featureUse the Tyhp alternatives for disabled features: declared properties or __get/__set instead of undeclared properties (except on \stdClass), use plus Composer instead of include/require, and \Throwable catch types.
For restricted dynamic features, use the type guard functions (\class_exists(), \function_exists(), \method_exists(), \property_exists()) to narrow types before dynamic access. The compiler recognizes these as type guards.
Prefer static access patterns over dynamic ones. Direct property access ($obj->name), method calls ($obj->method()), and class instantiation (new ClassName()) provide full compile-time type checking.
Using eval() without intending to. eval() is an informational diagnostic (TYHP4800), suppressed by build.allowEval. If you need dynamic code execution, isolate it in a plain PHP file and declare the interface via tyhpdef.
Using <? or <?= in Tyhp files. Always use <?tyhp as the opening tag for Tyhp source files.
Using dynamic access patterns without type guards. Variable functions, dynamic properties, dynamic methods, and dynamic class instantiation all require type narrowing first. Variable variables ($$var) are prohibited (TYHP4133).
Omitting type annotations. Unlike PHP, Tyhp requires types on all variables (or inference from first assignment), parameters, return types, and class properties.