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 — 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).

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.

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.