Declaration Gating
Tier 1 · Story 11Complete
PHP developers often wrap declarations in existence checks so a file can be loaded more than once without fatal redeclaration errors — for example if (!\function_exists(...)) { function ... } or if (!\class_exists(...)) { class ... }. Tyhp supports the same pattern. What Tyhp adds is compile-time validation: when you use that idiom to gate a declaration, the gate argument must name that exact declaration, using a fully-qualified name.
Note
This page is about declaration gates in Tyhp source — wrapping a class, function, interface, trait, or enum so it is only declared when it does not already exist. That is separate from using \class_exists($name) / \function_exists($name) as type guards on string variables (see Type Narrowing and Guards), and separate from PHP version gates (declare(php=…) / #[\Tyhp\Php]) and extension gates (declare(ext="…")) that keep or drop declarations while binding (see PHP Version Gating). Package signatures for the same PHP pattern use fallback function and fallback const in .tyhpdef (see Fallback functions).
The Pattern
A declaration gate is a top-level if with a negated existence call and a single matching declaration in the then-branch (no else):
<?tyhp
namespace App\Payments;
if (!\function_exists('\App\Payments\formatMoney')) {
function formatMoney(int $cents): string {
return '$' . number_format($cents / 100, 2);
}
}
if (!\class_exists('\App\Payments\Money')) {
class Money {
public function __construct(public readonly int $cents): void {}
}
}
The matching helpers are \function_exists, \class_exists, \enum_exists, \interface_exists, and \trait_exists. The helper must match the kind of declaration inside the block.
The Name Must Be Exact
Inside a namespace, an unqualified or wrong name is not enough. The gate must check for the same symbol the block declares. Tyhp accepts:
- A string literal with the fully-qualified name (leading
\ optional), e.g. '\App\Payments\formatMoney'
__NAMESPACE__ . '\formatMoney' — concatenating the current namespace with '\Name'
In the global namespace only, a short name such as 'Foo' or '\Foo' is also accepted.
These are rejected (compiler error TYHP4213):
'formatMoney' — not namespaced when the declaration lives in a namespace
'\formatMoney' — wrong namespace (global instead of App\Payments)
'' — empty
'someOtherName' — a different symbol than the one being declared
<?tyhp
namespace App\Payments;
// ERROR TYHP4213 — unqualified
if (!\function_exists('formatMoney')) {
function formatMoney(int $cents): string { return ''; }
}
// ERROR TYHP4213 — wrong namespace
if (!\function_exists('\formatMoney')) {
function formatMoney(int $cents): string { return ''; }
}
// OK — explicit FQN
if (!\function_exists('\App\Payments\formatMoney')) {
function formatMoney(int $cents): string { return ''; }
}
// OK — __NAMESPACE__ concat
if (!\function_exists(__NAMESPACE__ . '\formatMoney')) {
function formatMoney(int $cents): string { return ''; }
}
Note
nameof(...) inside a declaration gate is not validated yet and is not treated as a movable gate. Prefer an FQN string or __NAMESPACE__ . '\Name' for now.
When the Gate Is Correct
If the gate is valid (matching helper, single matching declaration, and a correct FQN / __NAMESPACE__ argument):
- The checker accepts it — no TYHP4213
- The emitter moves the entire
if together with the declaration to the normal destination for that kind of symbol (classes/interfaces/traits/enums to their PSR-4 file; namespace functions to _functions.php)
- The gate argument is always rewritten to
__NAMESPACE__ . '\Name' in the PHP output (even if the Tyhp source used an explicit FQN), so an output namespacePrefix cannot break the runtime check
- Gated functions are placed at the end of
_functions.php, after ungated function declarations, so they are evaluated last
- The source file is not treated as an entry point solely because of the gate
For example, Tyhp source that checks '\App\Payments\formatMoney' emits:
namespace App\Payments;
if (!\function_exists(__NAMESPACE__ . '\formatMoney')) {
function formatMoney(int $cents): string {
return '$' . number_format($cents / 100, 2);
}
}
Tip
This is why declaration gates are useful for shared helpers that may be loaded from more than one path: Tyhp still emits them as normal declarations, wrapped in the same existence check you wrote — rewritten to __NAMESPACE__ so it tracks the emitted namespace.
Fallback Declarations
fallback in front of a function, class, interface, trait, enum, or const declaration is shorthand for the matching existence gate. The declaration is only declared when nothing has declared that name already:
<?tyhp
namespace App\Payments;
fallback function formatMoney(int $cents): string {
return '$' . \number_format($cents / 100, 2);
}
This emits the same code as the hand-written gate above:
namespace App\Payments;
if (!\function_exists(__NAMESPACE__ . '\formatMoney')) {
function formatMoney(int $cents): string
{
return '$' . \number_format($cents / 100, 2);
}
}
fallback combines with deprecated, obsolete, and async. It does not combine with partial, omit, or extern. Because fallback is contextual, a function named fallback still works.
A fallback const uses \defined and \define, because PHP has no conditional const:
<?tyhp
fallback const int LIMIT = 13;
if (!\defined('LIMIT')) {
\define('LIMIT', 13);
}
In a namespace the name is written as __NAMESPACE__ . '\LIMIT'; in the global namespace it has no leading backslash, because \define('\LIMIT', …) would register a constant that plain LIMIT cannot find.
A fallback declaration can sit inside declare(php=…) and declare(ext=…) blocks. The version and extension checks wrap the existence check. See PHP Version Gating.
When the Gate Is Incorrect
If the shape looks like a declaration gate (negated *_exists wrapping a single declaration of the matching kind) but the argument does not name that declaration:
- The checker reports TYHP4213 — Declaration existence gate must check for the expected fully-qualified name
- The build fails (the error blocks emit)
- The splitter does not treat the
if as a movable declaration gate
Other if wrappers that are not this idiom (wrong helper kind, else branch, multiple statements in the then-body, or a non-existence condition) are ordinary control flow. They stay as entry-point / root code and are not rewritten as declaration gates.
Warning
A mismatched gate is not “close enough.” Checking 'formatMoney' while declaring App\Payments\formatMoney is a compile error in Tyhp, even though PHP might appear to work at runtime in some setups.
Common Mistakes
Danger
Don't use the short name inside a namespace ('demo'). Use '\Current\Namespace\demo' or __NAMESPACE__ . '\demo'.
Danger
Don't gate one symbol while declaring another (function_exists('foo') wrapping function bar()). The names must match.
Danger
Don't mix helpers and declaration kinds (class_exists around a function, or function_exists around a class). Those are not treated as declaration gates.