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

Implement config object #13

Closed
wants to merge 2 commits into from
Closed

Conversation

flangofas
Copy link
Collaborator

@flangofas flangofas commented Apr 30, 2020

This is the implementation of #6

It can be accessed by the Application or Command Object.

Use cases:

$config = $app->getConfig();
$config = $command->getConfig();

//Default params:
$params = [
    'bootstrapDir'   => 'app',
    'commandsDir'    => 'Application/Commands',
    'indexDir'	     => 'public',
    'indexFile'	     => 'index.php',
    'rootDir'	     => 'Absolute path of the project\'s root directory',
    'sourceDir'	     => 'src',
];

//Retrieve value
$config['commandsDir'];
//OR
$config->get('commandsDir');

Additional params can be stored as follows:

$config['newParam'] = 'value';
//OR
$config->set('newParam') = 'value';

Supported formats

  • JSON
  • PHP
  • Yaml

@flangofas flangofas requested review from l0gicgate and ABGEO April 30, 2020 17:07
Copy link
Member

@l0gicgate l0gicgate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a preliminary review, before writing all the tests let's nail down the initial mechanisms.

Thanks for this, great work!

Comment on lines +35 to +41
public function setConfig(string $configDir): void
{
$app = $this->getApplication();
if ($app instanceof Application) {
$app->setConfig($configDir);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should throw here to indicate to the user that the parent Application does not support this in case this command is being used out of context. Same for the getConfig() method

* @param string $key
* @return mixed|null
*/
public function get(string $key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should omit from using ArrayAccess and use named getters/setters:

  • getBootstrapDir(): string
  • getCommandsDir(): ?string
  • getIndexDir(): string
  • getIndexFile(): string
  • getSourceDir(): string

Same for setters, here we return the Config object so we can chain set calls:

  • setBootstrapDir(string $dir): Config
    ...

I think that this object should be immutable but extensible. So maybe an interface is necessary. Another point I would like to make is that in no instance should getters return null, as this will create downstream errors. These should all be validated, required parameters. The only parameter that could be null is commandsDir.

So instead of instantiating the configuration file with an array I would do something like:

public function __construct(
    string $sourceDir,
    string $bootstrapDir,
    string $indexDir,
    string $indexFile,
    ?string $commandsDir = null,
)

Then you could have a static instantiator:

public static function fromArray(array $params): Config
{
    // ... do array validation to ensure params contain required keys ...
   return new self(...);
}

public static function fromEnvironment(): Config
{
    // ... do array validation to ensure params contain required keys ...
   return new self(...);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

src/Config/ConfigResolver.php Show resolved Hide resolved
src/Exception/ConfigNotFoundException.php Outdated Show resolved Hide resolved
tests/ApplicationTest.php Show resolved Hide resolved
if ($format === self::FORMAT_PHP) {
$array = require($this->file);
if (is_array($array) === false) {
throw new InvalidArgumentException("The file $this->file should return an array");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's wrap $this->file with {$this->file}

$string = (string) file_get_contents($this->file);
$array = json_decode($string, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException("The file $this->file should be a valid JSON");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's wrap $this->file with {$this->file}


public function loadConfig(): ?Config
{
$this->locateConfig();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should do:

$this->file = $this->locateConfig();

In the locateConfig(): string method we can instead return early from the proposed switch statement.

Comment on lines +65 to +69
if (is_null($this->file)) {
throw new ConfigNotFoundException(
'Please create config file, supported formats: ' . implode(',', $formats)
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this if statement can be removed since we would reach this from not returning early. The ConfigurationNotFoundException still stands though.

{
$this->locateConfig();

return $this->loadConfigByFormat();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ENV variables takes precedence over all other formats here what we should try first is $this->attemptCreatingConfigFromEnv(): Config which would call Config::fromEnvironment() (this method should throw something like CannotCreateConfigFromEnvironmentException. If we catch this, then we proceed to try and locate PHP config then JSON then YAML

Here we should do:

try {
    return $this->attemptCreatingConfigFromEnv();
catch (CannotCreateConfigFromEnvironmentException $e) {
    return $this->attemptCreatingConfigFromSupportedFormats();
}

return $this->config;
}

public function setConfig(string $configDir): void
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it weird, that getConfig returns a Config object while setConfig takes a string? I would suggest to name the method resolveAndLoadConfig which in turn calls setConfig(Config $config).

If so, the same holds for \Slim\Console\Command\AbstractCommand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @adriansuter

*/
private $default = [
'bootstrapDir' => DIRECTORY_SEPARATOR . 'app',
'commandsDir' => DIRECTORY_SEPARATOR . 'Application/Commands',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a DIRECTORY_SEPARATOR between Application and Commands too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the default we do. For user passed input that’ll be up to them!

* @param string $key
* @return mixed|null
*/
public function get(string $key)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

$this->dir = $dir;
}

public function loadConfig(): ?Config
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can basically instantiate a ConfigResolver and then load the config multiple times? Why should we?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolver should only have one public method called resolve which either returns a Config object or throws.

Copy link
Collaborator Author

@flangofas flangofas May 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adriansuter @l0gicgate
Should we disallow to load the config again when it is already loaded via ConfigResolver?

*/
private $default = [
'bootstrapDir' => DIRECTORY_SEPARATOR . 'app',
'commandsDir' => DIRECTORY_SEPARATOR . 'Application/Commands',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use DIRECTORY_SEPARATOR between Application and Commands too.
And I think src/Commands will be a more semantic place for user commands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at how a DDD project is normally structured I would say that Application/Commands is better since you’ll typically only have only 3 root level folders being:

  • Application
  • Domain
  • Infrastructure

Could even go one step further and do Application/Console/Commands since in there you would technically want to separate your Console and Web layers.

@@ -48,4 +55,14 @@ public function doRun(InputInterface $input, OutputInterface $output): int

return parent::doRun($input, $output);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment block.

{
return $this->config;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment block.

@@ -17,5 +17,11 @@ if (file_exists(__DIR__ . '/../../../autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
}

$app = new Application();
//Config SHOULD be placed by init command in the project's root directory
$configDir = dirname(dirname(__DIR__));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where we use Slim Console as a dependency in project, dirname(dirname(__DIR__)) will return the correct project root, but what if we use Console as a global dependency? It will return ~/.composer. Can we make this part dynamic?

* @param array<mixed> $params
* @return void
*/
public function setAll(array $params): void
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make it fluent and return self.

);
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment block.

private $file = null;

/** @var string */
private $dir;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a more descriptive name, for example configDirectory.

private const CONFIG_FILENAME = 'slim-console.config';

/** @var string|null */
private $file = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a more descriptive name, for example configFile.


public function loadConfig(): ?Config
{
$this->locateConfig();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle the exception?

$string = (string) file_get_contents($this->file);
$array = json_decode($string, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException("The file $this->file should be a valid JSON");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we provide more information to the user, for example, using json_last_error_msg()?

@l0gicgate l0gicgate mentioned this pull request May 1, 2020
3 tasks
@l0gicgate
Copy link
Member

@flangofas I created a new draft in #14. I think it's probably an easier starting point since you essentially would have to entirely rewrite this from the reviewers' directions.

Please don't take offense. Thank you for this initial contribution it gave us a good base to see where we wanted to start at least so it wasn't all done for nothing.

@l0gicgate l0gicgate closed this May 1, 2020
@flangofas
Copy link
Collaborator Author

flangofas commented May 1, 2020

@l0gicgate No offense taken, I agree. The decisions were not made and it wasn't quite clear how it is going to be implemented, I should've asked more questions in the first place.

@flangofas flangofas deleted the add-config-object branch May 2, 2020 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants