[PHP-DEV] Automatically execute Closures returned from primary script

Hi

this thread concerns my PR carrying the same title as this email: cli: Automatically execute Closures returned from primary script by TimWolla · Pull Request #23658 · php/php-src · GitHub, where Gina requested that I ask the mailing list for opinions and/or objections against the feature.

My goal is to enable PHP scripts to act both as “reusable module” (defining classes or functions for use in an application) as well as an executable script at the same time, similarly to Python’s `if __name__ == "__main__"` pattern.

My current PR automatically executes any `Closure` returned by the main PHP script, such that:

     <?php

     function my_json_decode(string $json): mixed {
         return json_decode($json, flags: JSON_THROW_ON_ERROR);
     }

     return static function () {
         var_dump(my_json_decode(file_get_contents('php://stdin')));
     };

would just define `my_json_decode()` when included in an existing script, but print the decoding of the standard input when run as `echo '{"foo": "bar"}' |php my_json_decode.php` by executing the returned Closure, similar to `echo '{"foo": "bar"}' |python3 -m json.tool`.

Another possible solution, that I didn't yet evaluate, might be defining a magic constant `__MAIN__` that is equivalent to `__FILE__` of the script that is executed first.

Best regards
Tim Düsterhus

On Tue, 22 Sept 2026 at 14:19, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

this thread concerns my PR carrying the same title as this email:
cli: Automatically execute Closures returned from primary script by TimWolla · Pull Request #23658 · php/php-src · GitHub, where Gina requested that I
ask the mailing list for opinions and/or objections against the feature.

My goal is to enable PHP scripts to act both as “reusable module”
(defining classes or functions for use in an application) as well as an
executable script at the same time, similarly to Python’s `if __name__
== "__main__"` pattern.

My current PR automatically executes any `Closure` returned by the main
PHP script, such that:

     <?php

     function my_json_decode(string $json): mixed {
         return json_decode($json, flags: JSON_THROW_ON_ERROR);
     }

     return static function () {
         var_dump(my_json_decode(file_get_contents('php://stdin')));
     };

would just define `my_json_decode()` when included in an existing
script, but print the decoding of the standard input when run as `echo
'{"foo": "bar"}' |php my_json_decode.php` by executing the returned
Closure, similar to `echo '{"foo": "bar"}' |python3 -m json.tool`.

Another possible solution, that I didn't yet evaluate, might be defining
a magic constant `__MAIN__` that is equivalent to `__FILE__` of the
script that is executed first.

Best regards
Tim Düsterhus

Hi Tim,

A BIG yes from me.

I currently tend to use the following pattern in executable scripts,
or even index.php files in web applications:

(static function (): void {
    // This is the main entry point.
})();

For example: mago/composer/bin/mago at main · carthage-software/mago · GitHub

It works, but I think returning the closure is cleaner and
communicates the intent more clearly:

Just to confirm my understanding: when the file is included from
another script, the closure would still be returned without being
executed,
so the following would continue to work as it does today:

$callable = require 'script_file.php';
$callable();

Is that correct?

Best regards,
Seifeddine

Hi

On 2026-09-22 17:21, Seifeddine Gmati wrote:

Just to confirm my understanding: when the file is included from
another script, the closure would still be returned without being
executed,
so the following would continue to work as it does today:

$callable = require 'script_file.php';
$callable();

Is that correct?

Yes. The only change is that the PHP CLI SAPI will make use of the return value of the main script rather than ignoring it. No changes for any other SAPI and no changes to `include`/`require`. Please see the tests in the PR, and in particular `sapi/cli/tests/primary_script_closure_004.phpt`.

Best regards
Tim Düsterhus

On Tue, 22 Sept 2026 at 16:29, Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 2026-09-22 17:21, Seifeddine Gmati wrote:
> Just to confirm my understanding: when the file is included from
> another script, the closure would still be returned without being
> executed,
> so the following would continue to work as it does today:
>
> ```php
> $callable = require 'script_file.php';
> $callable();
> ```
>
> Is that correct?

Yes. The only change is that the PHP CLI SAPI will make use of the
return value of the main script rather than ignoring it. No changes for
any other SAPI and no changes to `include`/`require`. Please see the
tests in the PR, and in particular
`sapi/cli/tests/primary_script_closure_004.phpt`.

Best regards
Tim Düsterhus

Hi Tim,

That makes sense, thanks.

Another approach worth considering is the one used by HHVM/Hack, where
an entry point is identified using an attribute:

<<__EntryPoint>>
function main(): void {
    echo "Hello world!\n";
}

The only documentation reference I could find is here:

Hack prefixes built-in and special attributes with `__`. The PHP
equivalent might look something like this:

#[EntryPoint]
function main(): void {
    echo "Hello world!\n";
}

A function carrying this attribute in the main script would be
executed automatically. This makes the entry point explicit and avoids
assigning special meaning to the script's return value.

There would be a few semantic questions to resolve:

1. Should top-level code in the main script execute before the
entry-point function?
2. Which signatures should be accepted? Possibilities might include
`()`, `(array $argv)`, or `(int $argc, array $argv)`, with a return
type of `void`, `int`, or `never`.
3. What should happen if multiple functions are marked as entry
points? A compile-time error seems reasonable.

I still like the returned-closure proposal, but I think this
alternative is worth considering.

Best regards,
Seifeddine

On Tue, Sep 22, 2026 at 7:20 AM Tim Düsterhus <tim@bastelstu.be> wrote:

this thread concerns my PR carrying the same title as this email:
cli: Automatically execute Closures returned from primary script by TimWolla · Pull Request #23658 · php/php-src · GitHub, where Gina requested that I
ask the mailing list for opinions and/or objections against the feature.

Why is this PR scoped to the CLI SAPI? Shouldn't it apply equally to
any SAPI's top-level script?

On 22 September 2026 20:43:11 BST, Seifeddine Gmati <azjezz@carthage.software> wrote:

Another approach worth considering is the one used by HHVM/Hack, where
an entry point is identified using an attribute ... The PHP
equivalent might look something like this:

#[EntryPoint]
function main(): void {
   echo "Hello world!\n";
}

I think I like this version better - as well as being more explicit, it means the entry point doesn't have to be at the very end of the file.

I don't think I've ever written a file (or wanted to) that was both an include and an entry point; but I have written files which include both function and class definitions and code intended for direct execution. Often, the procedural code is just something like "run_script($argv);" so adding an attribute would feel natural.

2. Which signatures should be accepted? Possibilities might include
`()`, `(array $argv)`, or `(int $argc, array $argv)`, with a return
type of `void`, `int`, or `never`.

I think this is a good question regardless of the mechanism for marking the function. One complication would be Levi's suggestion that this could be useful in web SAPIs as well, where $argv wouldn't mean anything (and request data is in super-globals). Maybe a specific #[CliEntryPoint] would provide $argv, and a generic #[EntryPoint] would not provide anything?

Rowan Tommins
[IMSoP]

On Wed, Sep 23, 2026, at 8:11 AM, Rowan Tommins [IMSoP] wrote:

On 22 September 2026 20:43:11 BST, Seifeddine Gmati
<azjezz@carthage.software> wrote:

Another approach worth considering is the one used by HHVM/Hack, where
an entry point is identified using an attribute ... The PHP
equivalent might look something like this:

#[EntryPoint]
function main(): void {
   echo "Hello world!\n";
}

I think I like this version better - as well as being more explicit, it
means the entry point doesn't have to be at the very end of the file.

I don't think I've ever written a file (or wanted to) that was both an
include and an entry point; but I have written files which include both
function and class definitions and code intended for direct execution.
Often, the procedural code is just something like "run_script($argv);"
so adding an attribute would feel natural.

I agree with Rowan. I have never, ever needed to do Python-style "library and self-execution" in PHP. The way the language is structured just doesn't make it necessary. And auto-executing a specific return type just feels hacky an unexpected. I'd be -1 on the original proposal here.

But I do, typically, have some sort of "get the heck out of the global namespace" function. Automating *that* in a more robust and supported way I could get behind.

2. Which signatures should be accepted? Possibilities might include
`()`, `(array $argv)`, or `(int $argc, array $argv)`, with a return
type of `void`, `int`, or `never`.

I think this is a good question regardless of the mechanism for marking
the function. One complication would be Levi's suggestion that this
could be useful in web SAPIs as well, where $argv wouldn't mean
anything (and request data is in super-globals). Maybe a specific
#[CliEntryPoint] would provide $argv, and a generic #[EntryPoint] would
not provide anything?

Since we have multiple SAPIs, would it make sense to do something like #[EntryPoint('cli')], #[EntryPoint('frankenphp')], etc? With no SAPI name indicating "all", and each SAPI implying a different set of arguments?

That might even offer a way to get super-global data passed into a function without referencing the super-globals. (And, crazy crazy thought, a way to register the run function for FrankenPHP worker mode? Dunno if that makes sense, just spitballing.)

--Larry Garfield

Hi

On 2026-09-22 23:12, Levi Morrison wrote:

this thread concerns my PR carrying the same title as this email:
cli: Automatically execute Closures returned from primary script by TimWolla · Pull Request #23658 · php/php-src · GitHub, where Gina requested that I
ask the mailing list for opinions and/or objections against the feature.

Why is this PR scoped to the CLI SAPI? Shouldn't it apply equally to
any SAPI's top-level script?

The “file is both a library and executable” use case is only really a thing for a single-file deployment, because otherwise you could just separate the executable bit into a different file. I believe that making a library do something useful when executed directly is something that would work for the CLI SAPI. In web contexts, where the front-controller pattern is already well-established and where you generally have an allow-list of legal entrypoints (mostly just your front-controller) for security reasons I don't see it working well.

Best regards
Tim Düsterhus

Hi

On 2026-09-22 15:17, Tim Düsterhus wrote:

My current PR automatically executes any `Closure` returned by the main PHP script, such that:

Based on the replies it has become clear that the proposed Closure approach has several questions to solve and possibly needs prior design (e.g. defining a request or context object that would be passed as a parameter) to not paint ourselves into a corner. This requires much more thought than I'm willing to spend right now, given I wanted to solve a specific use case of mine.

I'm therefore abandoning the Closure idea and will instead look into …

Another possible solution, that I didn't yet evaluate, might be defining a magic constant `__MAIN__` that is equivalent to `__FILE__` of the script that is executed first.

… defining a __MAIN__ constant, which should hopefully have much fewer design considerations and might also benefit PHP applications that don't leverage the front-controller approach to protect internal files from direct access (e.g. as a native variant of phpBB’s IN_PHPBB constant).

Best regards
Tim Düsterhus

On Wed, Sep 23, 2026 at 7:44 AM Tim Düsterhus <tim@bastelstu.be> wrote:

Hi

On 2026-09-22 23:12, Levi Morrison wrote:
>> this thread concerns my PR carrying the same title as this email:
>> cli: Automatically execute Closures returned from primary script by TimWolla · Pull Request #23658 · php/php-src · GitHub, where Gina requested that I
>> ask the mailing list for opinions and/or objections against the
>> feature.
>
> Why is this PR scoped to the CLI SAPI? Shouldn't it apply equally to
> any SAPI's top-level script?

The “file is both a library and executable” use case is only really a
thing for a single-file deployment, because otherwise you could just
separate the executable bit into a different file. I believe that making
a library do something useful when executed directly is something that
would work for the CLI SAPI. In web contexts, where the front-controller
pattern is already well-established and where you generally have an
allow-list of legal entrypoints (mostly just your front-controller) for
security reasons I don't see it working well.

Best regards
Tim Düsterhus

Perhaps you have not seen this pattern in Symfony:

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $context) {
    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

It uses (in my opinion) horrible autoloader magic to re-include the
file to make it work. But nonetheless, this is partly why I asked why
you scoped it to just CLI.

I don't really like top-level code, so in some respects it might be
nice to have an annotation for some function to be called
automatically. But I would need to think about this more if I would
actually support such a proposal, as I don't particularly like
annotations either. Perhaps status quo is better.

On Wed, Sep 23, 2026, 15:52 Tim Düsterhus <tim@bastelstu.be> wrote:

Another possible solution, that I didn’t yet evaluate, might be
defining a magic constant __MAIN__ that is equivalent to __FILE__
of the script that is executed first.

… defining a MAIN constant, which should hopefully have much fewer
design considerations and might also benefit PHP applications that don’t
leverage the front-controller approach to protect internal files from
direct access (e.g. as a native variant of phpBB’s IN_PHPBB constant).

I think already this would work with realpath($_SERVER['SCRIPT_FILENAME'])

–
Alex