brick / varexporter

A powerful alternative to var_export(), which can export closures and objects without __set_state()

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow custom exporters for enhanced features

boesing opened this issue · comments

Hey there,

I am thinking about adding support to laminas/laminas-config-aggregator to actually export self calling closures.
The main idea is to allow some kind of string which is then interpreted by the exporter in a way that it replaces that string with a self calling function:

putenv('FOO=bar');
$data = ['bar' => '%env(FOO)%'];

$exported = VarExporter::export($data, VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES);

This would probably result in:

$exported = 'return [\'bar\' => (fn () => getenv(\'FOO\'))()];';

So once "importing" this data, it will always execute the callable so that the environment variable is always loaded from the environment at the time the file is being required.

As of now, this is not possible as there is no extension point of this component.
Do you think, this component would be the proper extension point to introduce such a feature or should we try to somehow work-around it in the laminas component instead?
As far as I understand #15, this component is to create data which can then be used without this component so this would actually still hold that statement I guess.

I'd really love to see this feature in here so that we can add a custom string exporter which would do something like:

final class MyStringExporter
{
     public function __construct(
          private readonly BrickVarExporterStringExporter $exporter,
     ) {}

     public function export(string $string): string
     {
           if (!str_starts_with($string, '%env(') || !str_ends_with($string, ')%') {
                return $this->exporter->export($string);
           }

           return 'whatever';
     }
}

But not sure if you want this kind of complexity so see this more as a question rather than a demand :-)
Thanks for your time.

Max

Another solution would be to introduce post-config processors that are called after the cache loading for laminas/laminas-config-aggregator.

Performance wise that processor would need to array_walk_recursive over the whole config for every request. So I see your point of having that part done once during cache generation time.

$envVarsPostCachedConfigProcessor = function (array $config)  {
    array_walk_recursive(
        $config,
        function (&$value) {
            // Replace '%env(EXAMPLE)%' with value of getenv('EXAMPLE')
            if (is_string($value) && preg_match('{%env\(([a-zA-Z_][a-zA-Z0-9_]*)\)%}', $value, $envVars) === 1) {
                $value = getenv($envVars[1]);
            }
        }
    );

    return $config;
};

Hi! While I understand the use case and adding an extension point does not look too difficult, I'm wondering what prevents you from creating the fn() => getenv('FOO') closure in your config, and export that?

Basically:

$data = ['bar' => fn() => getenv('FOO')];

$exported = VarExporter::export($data, VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES);

You could have even have %env(FOO)% in your config, and preprocess it with a recursive code similar to the one proposed by @bcremer before exporting it?

That wont be self calling when loaded.
Having a closure is easy but I want a self-calling closure which calls itself once "executed"/required.

In my example in the initial code, there would be "bar='bar'" in my $data.

So the way we use this component is to dump a large array to the fs which is then required each request. So I do want the require to then load the data via getenv.

Only on mobile and thus not sure if I could point out the use case correct.

So what is not possible is to have a self calling closure as it would be - self calling and thus the $data would already contain the env var rather than fetching it when using require on the dumped array file.

Ah sorry, I know what you mean now. Indeed there is no way to do that at the moment. We could introduce an interface to allow you to bypass any exporter (I don't think there is a point in doing a custom export for scalars other than string, but maybe for objects it can make sense to bring your own exporter?)

I'm thinking of something like:

interface CustomExporter
{
    public function export(mixed $value): ?string;
}

If export() returns null, then brick's exporter would be used for the value.

Two potential pain points come to mind:

  • although not required, the custom exporter should ideally respect VarExporter's current identation level if it returns multiline values
  • brick/varexporter is not feature complete yet: object identity is not preserved if an object is exported twice, and circular references are not currently exportable; if these are implemented at some point, I'm wondering how the proposed feature would affect them.

I think, once we are going for that change, I think having dedicated exporters registered for each type would make more sense.
you do already have dedicated methods and thus, that would make sense to me.
Each exporter could be nullable in the constructor and if none is passed, u always use urs and if not, Id say let the consumers handle it. If there is a need to invoke bricks internal exporter, I guess that would be up to the users.
At least that is how I would probably do it.


regarding the object identity - I am unsure if that is even needed or even possible and thus I think lets see when it comes to that point, wdyt?

Oh and ofc, you can pass the indentation Level as you do with ur own logic. I would keep that signatures in the interfaces tbh.

I have a similar use case, writing Drupal settings, where i want arbitrary expressions referencing vars like $app_root . '/../private'.

I don't like the habit of magic strings like '%env' or '@ service' though, and have bad feelings with encouraging that. There are better ways, even in yaml (see custom tags).

Also i don't like to put too much magic into the exporter. If magic is needed, put it into a preprocess step, and don't touch the semantics of the export data.

With that opinionated approach, i created MR #32.

Eager to know what you think and if this suits your use cases too.