Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What are Config Files and how do we improve Config? #7388

Closed
kenjis opened this issue Mar 27, 2023 · 47 comments · Fixed by #8563
Closed

What are Config Files and how do we improve Config? #7388

kenjis opened this issue Mar 27, 2023 · 47 comments · Fixed by #8563

Comments

@kenjis
Copy link
Member

kenjis commented Mar 27, 2023

I have questions about Config.

the current Routes file isn't what I would consider a config file
#7380 (comment)

Then, what are Config Files? or what are Config items?

Config items should be immutable or mutable?
We should use array for Config instead of class? What is the advantage of array-config?
App\Config causes infinite loop now (See #7308). How do we solve it?

Checking the environment variables for all properties when instantiating the Config class is too heavy.
This is one of the reasons why CI4 is slower than CI3. I want to improve this too.

@kenjis kenjis mentioned this issue Mar 27, 2023
5 tasks
@kenjis
Copy link
Member Author

kenjis commented Mar 27, 2023

Forbid using the constructor in configs.
#7380 (comment)

What if I want to use a value in the database for a config value?

@lonnieezell
Copy link
Member

I don't think this is a bug, so shouldn't it be discussed in the forums?

@kenjis kenjis added the dev label Mar 28, 2023
@lonnieezell
Copy link
Member

What if I want to use a value in the database for a config value?

Config files are typically loaded before a database would be, so you hit issues there.

@kenjis
Copy link
Member Author

kenjis commented Mar 28, 2023

I don't think this is a bug, so shouldn't it be discussed in the forums?

I don't think most maintainers see the forum.

And the source code is hard to read on the forum, and we can't just link to it and see the code like you can on GitHub. It's hard to have detailed discussions.

@kenjis
Copy link
Member Author

kenjis commented Mar 28, 2023

Config files are typically loaded before a database would be, so you hit issues there.

Database seems working in Config\App.
See #4297 (comment)

@lonnieezell
Copy link
Member

Database seems working in Config\App.

IIRC it will work for some config files but not others. Whichever ones that the database layer might grab will cause an issue, and that's more than just the database config file. I've ran into this in the past and I don't remember which specific ones caused the issue.

What if I want to use a value in the database for a config value?

The I recommending using CodeIgniter's official solution - Settings. :)

I don't think most maintainers see the forum.

They will see it on Slack, though. We've always told people Issues are for bug reports, forum was for feature requests.

@iRedds
Copy link
Collaborator

iRedds commented Mar 28, 2023

What if I want to use a value in the database for a config value?

First, I will give an example of Laravel.

  1. Environment variables are loaded.
  2. Configs are loaded.
  3. ...
  4. ServiceProviders are loaded (ServiceProviders register services in the container via the register() method)
  5. For all loaded ServiceProviders, the boot() method is called.

There is an application ServiceProvider like AppServiceProvider. And in the AppServiceProvider::boot() method, you can add some logic that will definitely be called after the configs and before the request is processed.

I want to say that for the problem you described, there are several ways to solve it.

We need a class that will work in the same way as AppServiceProvider::boot()
Or, for these purposes, we can use the pre_system event, which is called immediately before the request is processed.

@iRedds
Copy link
Collaborator

iRedds commented Mar 28, 2023

I don't think this is a bug, so shouldn't it be discussed in the forums?

From my experience, I can say that the feedback from the CI team on the forum is close to zero. The only maintainer who is active is @kenjis . But as you can see, even he does not rely on the forum as a place for discussion.

@kenjis
Copy link
Member Author

kenjis commented Mar 28, 2023

@iRedds My question is like this if using Laravel.
How do I set the values of Config files from the database data?
Probably I can't?

@kenjis
Copy link
Member Author

kenjis commented Mar 28, 2023

We've always told people Issues are for bug reports, forum was for feature requests.

I think it is better to use GitHub Discussions if you don't like to use GitHub IIssues.

@iRedds
Copy link
Collaborator

iRedds commented Mar 28, 2023

@kenjis I am not using Laravel. I look at its code base to understand how these or those solutions are implemented.
I suppose that changes in configs at runtime in Laravel can be done as follows.

// config myconfig.php
return [
    'key' => 'default',
    'key2' => 'default2'
];

// AppServiceProvider
public function boot(): void
{
    // Here we override the config values, for example, by taking the values from the database.
    config(['myconfig.key' => 'new value']);
    // or for multiple keys
    config(['myconfig' => ['key' => 'new ', 'key2' => 'new 2']]);
}

// using 
Route::get('/', function () {
    dd(config('myconfig')); 
});

Now, by referring to the config, in the route we will receive the following data.

array:2 [▼ // routes\web.php:18
  "key" => "new "
  "key2" => "new 2"
]

@lonnieezell
Copy link
Member

How do I set the values of Config files from the database data?

You can't, they are just simple arrays. You would need to do a separate layer, much like Settings does currently. f

From my experience, I can say that the feedback from the CI team on the forum is close to zero. The only maintainer who is active is @kenjis . But as you can see, even he does not rely on the forum as a place for discussion.

That's why we have a Slack channel for the core team. Discussions could happen there and if there was a need for input on the forums we could be pointed that way with a Slack message. I just feel that if we have been telling users that Issues are for bugs, we should listen to that advice ourselves.

@lonnieezell
Copy link
Member

Back to the original discussion, though:

App\Config causes infinite loop now (See #7308). How do we solve it?

We don't. There is no need for session with a config file, as a config file is intended to hold the settings that configure how the application works and not respond to each user's individual session settings.

We should use array for Config instead of class?

I'm not opposed to going back to this for v5. We obviously cannot do it earlier. It would be lighter than the class-based configuration we currently use, and better for memory as optimized as PHPs arrays are now. I don't know how much the performance gains would be, compared to the database layer, which is our slowest part of the framework now, I believe.

There are a few config classes that provide methods that would have to be moved elsewhere. To be honest, though, I've only ever heard a couple people mentioning it being an issue, so we would need to consider the pros/cons/cost in manpower, and I think it would be good to get a bigger set of opinions on the forums. There's probably several topics we should do that about so we can start actually planning for v5 more.

Checking the environment variables for all properties when instantiating the Config class is too heavy.

IIRC the loading of environment vars only happens once, and would still need to happen with an array-based config. I haven't measured the cost of looping over the class vars in each class, but I would be surprised if it was too onerous. Would be interesting to see benchmarking on that.

@gphg
Copy link

gphg commented Mar 28, 2023

It is possible to cache the database loaded configs?

Edit:
Doesn't seem possible. The cache class is loaded later after Config\App.

@iRedds
Copy link
Collaborator

iRedds commented Mar 28, 2023

We don't. There is no need for session with a config file, as a config file is intended to hold the settings that configure how the application works and not respond to each user's individual session settings.

Sessions as an example.
If the code that refers to the same config is called in the config constructor, then a loop occurs.
And I guess a lot of people guessed to change configs at runtime in the config constructor.

@kenjis
Copy link
Member Author

kenjis commented Mar 28, 2023

There is no need for session with a config file, as a config file is intended to hold the settings that configure how the application works and not respond to each user's individual session settings.

Why? This is my main question. After all, what is a config item and what is not?

This is an example, but I don't think the requirement that if a locale is stored in the session and you want to set the locale based on that is so odd. And App\Config has a setting for locales $supportedLocales.
If you don't set $supportedLocales correctly, the application won't work well.

@lonnieezell
Copy link
Member

Supported locales is a static value. The current locale in the session is a dynamic value. Config files are typically for static values that don't depend on the current running state of the app.

@kenjis
Copy link
Member Author

kenjis commented Mar 29, 2023

What is a static value and what is not?
Probably a static value does not change at runtime.

Then $allowedHostnames is a static value?
What if a site user has its own URL like username.example.com?
It would not be possible to define all values in a Config file.

And even if $supportedLocales is a static value, it would still be difficult to define all locales in a Config file for a site where locales are constantly being added by users.

@kenjis
Copy link
Member Author

kenjis commented Mar 29, 2023

@iRedds Thank you for showing.

I want to do something like this. Then I can see what the value is just by looking at the config file.

// config myconfig.php
$values = db_connect()->query()->...

return [
    'key' => $values['key']',
    'key2' => $values['key2']',
];

If AppServiceProvider is going to change the config values, it might be better that there is no config file.
If there is no config file, I only have to look at AppServiceProvider to see what the config values are.

@lonnieezell
Copy link
Member

Why does a database config solution have to be at the "config" level or even system level? As mentioned, we already provide a solution to this in the Settings library.

@kenjis your examples are correct - and they would typically be handled by some other piece in the app layer, not in the config. I'm not aware of any framework that has dynamic config values like you're discussing here. it seems like most of this would be something that likely gets processed either in an Event, or a Filter (which can modify the request as needed).

@iRedds
Copy link
Collaborator

iRedds commented Mar 29, 2023

@kenjis In your example, you don't see values, only variables. Having access only to the project code, without a database, it will be difficult to understand what data the code operates on.

In addition, it already looks more like some kind of library than a config.

config without config file. It looks like some kind of perversion )

@kenjis
Copy link
Member Author

kenjis commented Jul 13, 2023

IIRC the loading of environment vars only happens once, and would still need to happen with an array-based config. I haven't measured the cost of looping over the class vars in each class, but I would be surprised if it was too onerous. Would be interesting to see benchmarking on that.

Simple benchmark to CI4 default Welcome page on my MacBook Air.

Results:

Requests/sec:    378.43 (default)
Requests/sec:    508.60 (disable to load env vars)
$ php -v
PHP 8.2.8 (cli) (built: Jul  6 2023 11:16:24) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.8, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.8, Copyright (c), by Zend Technologies
$ composer update --no-dev
$ php spark env

CodeIgniter v4.3.6 Command Line Tool - Server Time: 2023-07-13 02:46:04 UTC+00:00

Your environment is currently set as production.
$ symfony server:start
$ wrk -t10 -d5s http://127.0.0.1:8000/
Running 5s test @ http://127.0.0.1:8000/
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    27.40ms   12.70ms 138.30ms   92.92%
    Req/Sec    38.02      8.85    50.00     71.83%
  1903 requests in 5.03s, 32.70MB read
Requests/sec:    378.43
Transfer/sec:      6.50MB

Disable to load environment variables:

--- a/system/Config/BaseConfig.php
+++ b/system/Config/BaseConfig.php
@@ -63,24 +63,24 @@ class BaseConfig
 
         $this->registerProperties();
 
-        $properties  = array_keys(get_object_vars($this));
-        $prefix      = static::class;
-        $slashAt     = strrpos($prefix, '\\');
-        $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1));
-
-        foreach ($properties as $property) {
-            $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
-
-            if ($this instanceof Encryption && $property === 'key') {
-                if (strpos($this->{$property}, 'hex2bin:') === 0) {
-                    // Handle hex2bin prefix
-                    $this->{$property} = hex2bin(substr($this->{$property}, 8));
-                } elseif (strpos($this->{$property}, 'base64:') === 0) {
-                    // Handle base64 prefix
-                    $this->{$property} = base64_decode(substr($this->{$property}, 7), true);
-                }
-            }
-        }
+//         $properties  = array_keys(get_object_vars($this));
+//         $prefix      = static::class;
+//         $slashAt     = strrpos($prefix, '\\');
+//         $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1));
+// 
+//         foreach ($properties as $property) {
+//             $this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
+// 
+//             if ($this instanceof Encryption && $property === 'key') {
+//                 if (strpos($this->{$property}, 'hex2bin:') === 0) {
+//                     // Handle hex2bin prefix
+//                     $this->{$property} = hex2bin(substr($this->{$property}, 8));
+//                 } elseif (strpos($this->{$property}, 'base64:') === 0) {
+//                     // Handle base64 prefix
+//                     $this->{$property} = base64_decode(substr($this->{$property}, 7), true);
+//                 }
+//             }
+//         }
     }
 
     /**
$ wrk -t10 -d5s http://127.0.0.1:8000/
Running 5s test @ http://127.0.0.1:8000/
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    20.86ms   12.72ms 148.69ms   94.62%
    Req/Sec    51.15     11.17    70.00     83.30%
  2555 requests in 5.02s, 43.90MB read
Requests/sec:    508.60
Transfer/sec:      8.74MB

@kenjis
Copy link
Member Author

kenjis commented Jul 13, 2023

My opinion:

  • All Config values are global default values and should not be personalized.
  • Every Config class should be immutable.

@lonnieezell
Copy link
Member

All Config values are global default values and should not be personalized.

How do you propose to handle per-environment values then? Adding them to the code itself is a non-starter. Best practices and security say you should never commit a number of your environment variables to a repo.

It's likely that, if we were doing a major overhaul of the config system that we could refactor the env var use with config files to be faster. Likely by being less "automatic" and doing it something a little more reliant on the environment variables themselves, instead of looping over them at instantiation and assigning them within the class. This would also likely allow us to not have to extend the BaseConfig class anymore.

@iRedds
Copy link
Collaborator

iRedds commented Jul 13, 2023

arrays 😆

@kenjis
Copy link
Member Author

kenjis commented Jul 13, 2023

@lonnieezell My English may not be correct, but I mean the handling per-environment values is not changed.
Environment variables are pulled and they override Config properties when a Config class is instantiated.

What I wanted to say by "not be personalized" is that the Config value is the value for the system, not for the site visitor.

@kenjis
Copy link
Member Author

kenjis commented Jul 13, 2023

The proposal to change to arrays is not yet clear on how to implement it concretely.

Assuming that the Config values are written in an array, how do we pass the values to the class?
And what about compatibility issues? In order to maintain compatibility, both Config classes and array files should be supported, at least for a certain period of time.

And with a Config class, we can jump from the property code to the source class in the IDE.
With arrays, however, we cannot jump to the source of the definition.
Our DX will be worse.

@iRedds
Copy link
Collaborator

iRedds commented Jul 14, 2023

Pass an array instead of a class.
Since we use classes as configs and specify the type hint in class constructors, we need to have an application for testing the framework.

By the way, Laravel has an interesting approach. It is not the config that is passed to the classes (services), but the application instance, which is inherited from the service container class that implements the ArrayAccess interface.
I'll try to show it schematically.

class Some
{
    /**
     * @var Application
     */
    protected $app;

    /**
     * @param Application $app
     */
    public function __construct($app)
    {
        // Here the `config` key is the name of the service in the container.
        $this->app['config']['config_name.config_var'];
    }
}

// And in tests it is enough to create an array.
new Some([
    'config' => ['config_name' => ['config_var' =>'value']]
])

Since our configs are classes, in order to temporarily change the value, you have to use an alternative class, since objects are passed by reference.

d(config(App::class)->baseURL); // config(...)->baseURL string (22) "http://localhost:8080/"
$config = config(App::class);
$config->baseURL = 'sss';
d(config(App::class)->baseURL); // config(...)->baseURL string (3) "sss"

When migrating from 3.x to 4.x, a soft transition was announced, although configs from arrays were replaced with classes.
So v5 can return arrays in the same way.

And with a Config class, we can jump from the property code to the source class in the IDE.

Weak advantage.

@kenjis
Copy link
Member Author

kenjis commented Jul 14, 2023

Laravel approach makes the design worse. All classes depends on the service container (Application).

I don't think a hard transition from classes to arrays is desirable.

By the way, what is the benefit of changing to arrays?
Changing from arrays to classes in v4 and then back to arrays again in v5 is ridiculous unless there is a huge advantage.

@iRedds
Copy link
Collaborator

iRedds commented Jul 15, 2023

By the way, what is the benefit of changing to arrays?

  • I repeat what I said above. That arrays are not passed by reference and changing the value will not affect the state of the config.
  • In an array, you can only assign a value from dotenv to a specific key. In objects, for this, we iterate over all the properties of the class in a loop. (By the way, foreach copies the passed array, which increases memory consumption at the moment.)
  • Arrays do not have a constructor, which means there is no chance of getting an infinite loop, as it can now happen with rash use.
  • You can include another config file in an array.
return ['key1' => 'value1', 'key2' => include 'some_config.php'];
return ['key1' => 'value1'] + include 'some_config.php';
  • In objects, for this, we iterate over all the properties of the class in a loop.
  • Arrays don't need a namespace, and forcing third-party library config files to be moved to the config directory will reduce search overhead.
  • With arrays, it is easier to organize the access to the value through the dot notation.
  • There is less overhead when restoring an array from the cache than for objects.

ps: It surprises me what garbage they came up with with configs. More precisely with access to them. Some strange factory.
Instead, there could be a regular service that stores configs in itself.

@kenjis
Copy link
Member Author

kenjis commented Jul 16, 2023

That arrays are not passed by reference and changing the value will not affect the state of the config.

If the Config class is immutable, the problem does not arise.

In an array, you can only assign a value from dotenv to a specific key.

Even if our Config will be arrays, we need to refect the following .env value to the config value.

database.default.database = ci4

Arrays do not have a constructor, which means there is no chance of getting an infinite loop, as it can now happen with rash use.

Yes, exactly.

However, I now believe that using Session in Config\App is a misuse. This means using personalized values in Config.

You can include another config file in an array.

Why do you need to include another config?

A Config class can have a another Config instance in a property.

Arrays don't need a namespace, and forcing third-party library config files to be moved to the config directory will reduce search overhead.

What do you mean? "forcing third-party library config files to be moved to the config directory"
Where is "the config directory"?
Why will it reduce search overhead?

With arrays, it is easier to organize the access to the value through the dot notation.

What is the benefit of it?

There is less overhead when restoring an array from the cache than for objects.

Have you measured it?

@iRedds
Copy link
Collaborator

iRedds commented Jul 16, 2023

If the Config class is immutable, the problem does not arise.

This is logical. But here a problem arises. If we need to change one or more values in the config for some one-time functionality, then we are forced to add an alternative config.

Even if our Config will be arrays, we need to refect the following .env value to the config value.

Due to the fact that we are using classes, the keys in .env must follow the scheme class.property[.key[.key]]
In the case of arrays, this is not necessary and the name of the key in .env can be anything.
Below is an example of what the structure of the database config might look like (I think such a structure is convenient) and a compact view of .env.

return [
    'default.connection' => env('DB.connection', 'default'),
    'driver' => [
        'mysql'  => \DB\Mysql\Driver::class,
        'oracle' => \DB\Oracle\Driver::class,
    ],
    'connections' => [
        'default'  => env('foo'),
        'another_connection' => env('bar'),
    ]
];
DB.connection = default
foo = oracle://user:pass@host:port/database?option_name=option_value&failover=another_connection
bar = mysql://user:pass@host:port/database?option_name=option_value

You can include another config file in an array.

Why do you need to include another config?

Don't know. Maybe I'm a pervert) For example, logically separate parts of the config.

What do you mean? "forcing third-party library config files to be moved to the config directory"
Where is "the config directory"? Why will it reduce search overhead?

Application config directory app/Config/. Libraries (to work with CI) must place their configs in the appropriate directory (i.e. copy it there through the publisher) app/Config/my_library.php.
Referring to the config('my_library') config, we will search in the appropriate path.

public function load(string $conf) 
{
    return include $this->path . $conf . '.php';   
}

And here is the link how we load the configs now in CI.

protected static function locateClass(array $options, string $name): ?string

Dot notation is simpler and shorter.

config(Config::class)->property['key'];
// vs
config('config.property.key')

There is less overhead when restoring an array from the cache than for objects.

Have you measured it?

No, I didn't measure. This assumption is based on the fact that the array does not need to implement the __set_state() method to work after being restored from the cache.

@kenjis
Copy link
Member Author

kenjis commented Jul 18, 2023

If we need to change one or more values in the config for some one-time functionality, then we are forced to add an alternative config.

If it changes at runtime, it is not a Config value. We should remove it from the Config file.
And we should have a setter method for it.
Impossible?

@iRedds
Copy link
Collaborator

iRedds commented Jul 18, 2023

Honestly, I do not know. For example, to create a link to a subdomain, we now need an alternative config, otherwise the link will be generated to the current domain. Or the base_url() helper resets the value of indexPage.
Can we remove indexPage and baseURL from configs?

@kenjis
Copy link
Member Author

kenjis commented Jul 19, 2023

In my opinion, indexPage is not changed even if we use a subdomain.
And baseURL is the base URL for one main site. So they never change at runtime.

If you want to create a link to a subdomain (not the current domain), the subdomain URL should be passed to helpers.

But the current site_url() needs an alternate Config\App object...

@iRedds
Copy link
Collaborator

iRedds commented Jul 19, 2023

// base_url() 
$config = clone config(App::class);  // We clone the object because the line below would change the global state.
// ...
$config->indexPage = '';

But these are only special cases and the conversation was about the benefits of the array.

@iRedds
Copy link
Collaborator

iRedds commented Jul 19, 2023

You can include another config file in an array.

Why do you need to include another config?

Don't know. Maybe I'm a pervert) For example, logically separate parts of the config.

By the way, in Yii2 config includes logical configs, for example, database configuration.
https://github.com/yiisoft/yii2-app-basic/blob/master/config/web.php

@kenjis
Copy link
Member Author

kenjis commented Jul 20, 2023

Yii2 seems to have global config files for web and console, so it includes database configuration.

@iRedds
Copy link
Collaborator

iRedds commented Jul 25, 2023

My point is that compound configs are possible and used.
It also makes it possible to use generated configs (for example, for dynamic modules).

@kenjis
Copy link
Member Author

kenjis commented Jul 25, 2023

We don't use global config file or global config object, so it seems we don't need compound configs.

What do you mean by "generated configs"?
To copy a new config file to app/Config, or add config items to the existing config files
when adding a package?

@iRedds
Copy link
Collaborator

iRedds commented Jul 25, 2023

When installing/removing dynamic modules, a config is generated by scanning the directory with modules. The resulting config in the autoload config constructor is merged with the namespaces property. And I don't know any other ways to connect such modules.

Our configuration classes include logic in the form of class methods. That is, these are no longer configs (that is, key = value), but configuration scripts.
For example, the Modules config, which contains a method. Or a database config, by calling its method we create a database connection. What's going on here? "Who do I have to [...] to get off this boat?"(c) E. Ripley =)

We don't use global config file or global config object, so it seems we don't need compound configs.

In fact, we are already loading the main configs and storing them in a global object just to start the application.

@neznaika0
Copy link
Contributor

neznaika0 commented Aug 14, 2023

Did you find a solution in the discussion? I agree that the config should remain non-editable.

But, for changes like $locale, we can have several values: static from Config, dynamic in Request, Cookies, Session. When working with localization, the developer needs to decide for himself what value to use at the moment.

$supportedLocales similarly, the developer needs to decide what to choose: a default value from Config or an array from the database (by installing in the filter or another service as CustomLocales::getList())

For env, we will have to make duplicate properties and get them first instead of the initial config.

This means that for this behavior, you need to additionally have values from config. Right?

@kenjis
Copy link
Member Author

kenjis commented Aug 14, 2023

My suggestion is to make all Config classes readonly classes. This way there will be no confusion about what devs can do. Those that need to change values will need a way to change the values to the actual class, not the Config class.

However, to actually do so would require PHP 8.2 or above, and this would be a breaking change.
So it may make some confusion.

For env, we will have to make duplicate properties and get them first instead of the initial config.

Env variables can override the config values. But it changes the value when instantiating the Config class.
So even if the Config class is a readonly class, it is no problem.
Registrars also work the same way.

@neznaika0
Copy link
Contributor

I also thought about readonly, but I often get to these BC with my desires.

However, to actually do so would require PHP 8.2 or above, and this would be a breaking change.

Yes, we need to start using new versions. Heh, is there an option to somehow jump into the 4.5/5.0 branch?

@kenjis
Copy link
Member Author

kenjis commented Oct 24, 2023

If we really make Config classes readonly, we will have the problem of not being able to write tests easily.

@kenjis
Copy link
Member Author

kenjis commented Nov 4, 2023

The Contextual Settings in Settings library is a misuse of Config, or it makes Config confusing.
https://github.com/codeigniter4/settings#contextual-settings

@kenjis
Copy link
Member Author

kenjis commented Feb 20, 2024

I sent a PR to update the user guide: #8563

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants