simplify chapter 12

This commit is contained in:
lubiana 2022-05-31 17:48:26 +02:00 committed by Andre Lubian
parent 350f8e18b9
commit f857fa4752

View file

@ -4,197 +4,99 @@
In the last chapter we added some more definitions to our dependencies.php in that definitions
we needed to pass quite a few configuration settings and filesystem strings to the constructors
of the classes. This might work for a small projects, but if we are growing we want to source that out to a more explicit file that holds all the configuration valuse for our project.
of the classes. This might work for a small projects, but if we are growing we want to source that out to a more
explicit file that holds all the configuration values for our project.
As this is not a problem unique to our project there are already a some options available. Some projects use [.env](https://github.com/vlucas/phpdotenv) files, others use [.ini](https://www.php.net/manual/de/function.parse-ini-file.php), there is [yaml](https://www.php.net/manual/de/function.yaml-parse-file.php) as well some frameworks have implemented complex Readers for many configuration file formats that can be used, take a look at the [laminas config component](https://docs.laminas.dev/laminas-config/reader/) for example.
As this is not a problem unique to our project there are already a some options available. Some projects use
[.env](https://github.com/vlucas/phpdotenv) files, others use
[.ini](https://www.php.net/manual/de/function.parse-ini-file.php), there is
[yaml](https://www.php.net/manual/de/function.yaml-parse-file.php) as well some frameworks have implemented complex
Readers for many configuration file formats that can be used, take a look at the
[laminas config component](https://docs.laminas.dev/laminas-config/reader/) for example.
As i am a big fan of writing everything in php, which gives our IDE the chance to autocomplete our code better I am quite happy the PHP8 gives us some tools to achieve easy to use configuration via php. You can take a look at [this blogpost](https://stitcher.io/blog/what-about-config-builders) to read about some considerations on that topic before moving on.
As I am a big fan of writing everything in php, which gives our IDE the chance to autocomplete our code better I am
quite happy that PHP8 gives us some tools to achieve easy to use configuration via php. You can take a look at
[this blogpost](https://stitcher.io/blog/what-about-config-builders) to read about some considerations on that topic
before moving on.
Lets create a 'Settings' class in our './src' Folder:
For the purpose of this Tutorial I will use a simple ValueObject that has all our configuration values as properties.
create a `Configuration.php` class in the `./src` folder:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
final class Settings
final class Configuration
{
public function __construct(
public readonly string $environment,
public readonly string $dependenciesFile,
public readonly string $templateDir,
public readonly string $templateExtension,
public readonly string $environment = 'dev',
public readonly string $routesFile = __DIR__ . '/../config/routes.php',
public readonly string $templateDir = __DIR__ . '/../templates',
public readonly string $templateExtension = '.html',
) {
}
}
```
I am using a new Feature from PHP 8.1 here called [readonly properties](https://stitcher.io/blog/php-81-readonly-properties) to write a small valueobject without the need to write complex getters and setters. The linked article gives a great explanation on how they work.
I am using a new Feature from PHP 8.1 here called
[readonly properties](https://stitcher.io/blog/php-81-readonly-properties) to write a small valueobject without the need
to write complex getters and setters. The linked article gives a great explanation on how they work. You can see, that
I have added working default values for every configuration parameter. In my personal opinion, project should always
have working default values without you needing to set up anything. This greatly improves usability and reduces errors.
When creating an instance of the setting class with my project specific values i will use another
new feature called [named arguments](https://stitcher.io/blog/php-8-named-arguments). There is
a lot of discussion on the topic of named arguments as some argue it creates unclean and
unmaintainable code, but vor simple valueobjects i would argue that they are ok.
We can now update our `container.php` file to use the configuration. Currently, the Mustache_Loader, as well as the
Fastroute-Dispatcher use values that we have defined in our Configuration, lets update those definitions:
here is a small example of creating a settings object using named arguments that I placed in the config folder
under the name settings.php
```php
Dispatcher::class => fn (Configuration $c) => simpleDispatcher(require $c->routesFile),
Mustache_Loader_FilesystemLoader::class => fn (Configuration $c) => new Mustache_Loader_FilesystemLoader(
$c->templateDir,
[
'extension' => $c->templateExtension,
]
),
```
Magically this is all we need to do, as the PHP-DI container knows that all constructor parameters of our configuration
class have default values and can create the needed object on its own.
There is one small problem: If we want to change environment from `dev` to `prod` we would need to update the
configuration class in the src directory. This is something we don't want to do on every deployment. So lets add a file
in our `./config` directory called `settings.php` that returns a Configuration object.
```php
<?php declare(strict_types=1);
use Lubian\NoFramework\Settings;
return new Settings(
environment: 'dev',
dependenciesFile: __DIR__ . '/dependencies.php',
templateDir: __DIR__ . '/../templates',
templateExtension: '.html',
return new \Lubian\NoFramework\Configuration(
environment: 'prod',
);
```
here I am using a new feature called [named arguments](https://stitcher.io/blog/php-8-named-arguments). There is
a lot of discussion on the topic of named arguments as some argue it creates unclean and
unmaintainable code, but for simple value-objects I would argue that they are ok.
But now we need some more code to include that settings Object and make it available in our container. As I don't want to use requires and includes too much in the dependencies configuration we are going to create a Factory class that gives us an Instance of the config file.
Lets define our Interface first. That way we can later switch to another implementation that creates our Settings object.
src/Factory/SettingsProvider.php:
We now need to add a line to our container to use the `settings.php` file to create the Configuration-object:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Settings;
interface SettingsProvider
{
public function getSettings(): Settings;
}
\Lubian\NoFramework\Configuration::class => fn () => require __DIR__ . '/settings.php',
```
And write a simple implementation that uses our settings.php to provide our App with the Settingsobject:
One small oversight to fix is in the registration of our error-handler in the bootstrap-file. There we read the
environment with the getenv-method. Lets change the line:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Settings;
final class FileSystemSettingsProvider implements SettingsProvider
{
public function __construct(
private string $filePath
) {
}
public function getSettings(): Settings
{
return require $this->filePath;
}
}
$environment = getenv('ENVIRONMENT') ?: 'dev';
```
If we later want to use yaml or ini files for our Settings we can easily write a different provider to read those files
and craft a settings object from them.
As we have now created a completely new Namespace and Folder and our SettingsProvider is all alone we could add another
factory for our Container because everyone should have a Friend :)
to:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Psr\Container\ContainerInterface;
interface ContainerProvider
{
public function getContainer(): ContainerInterface;
}
$config = require __DIR__ . '/../config/settings.php';
assert($config instanceof \Lubian\NoFramework\Configuration);
$environment = $config->environment;
```
And a simple implementation that uses our new Settingsprovider to build the container:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use DI\ContainerBuilder;
use Lubian\NoFramework\Settings;
use Psr\Container\ContainerInterface;
final class SettingsContainerProvider implements ContainerProvider
{
public function __construct(
private SettingsProvider $settingsProvider,
) {
}
public function getContainer(): ContainerInterface
{
$builder = new ContainerBuilder;
$settings = $this->settingsProvider->getSettings();
$dependencies = require $settings->dependenciesFile;
$dependencies[Settings::class] = fn () => $settings;
$builder->addDefinitions($dependencies);
return $builder->build();
}
}
```
For this to work we need to change our dependencies.php file to just return the array of definitions:
And here we can instantly use the Settings object to create our template engine.
```php
<?php declare(strict_types=1);
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Service\Time\Now;
use Lubian\NoFramework\Service\Time\SystemClockNow;
use Lubian\NoFramework\Settings;
use Lubian\NoFramework\Template\MustacheRenderer;
use Lubian\NoFramework\Template\Renderer;
use Mustache_Engine as ME;
use Mustache_Loader_FilesystemLoader as MLF;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
return [
ResponseInterface::class => fn (ResponseFactory $rf) => $rf->createResponse(),
ServerRequestInterface::class => fn (ServerRequestFactory $rf) => $rf::fromGlobals(),
Now::class => fn (SystemClockNow $n) => $n,
Renderer::class => fn (Mustache_Engine $e) => new MustacheRenderer($e),
MLF::class => fn (Settings $s) => new MLF($s->templateDir, ['extension' => $s->templateExtension]),
ME::class => fn (MLF $mfl) => new ME(['loader' => $mfl]),
];
```
Now we can change our Bootstrap.php file to use the new Factories for the creation of the Initial Objects:
```php
...
error_reporting(E_ALL);
$settingsProvider = new FileSystemSettingsProvider(__DIR__ . '/../config/settings.php');
$container = (new SettingsContainerProvider($settingsProvider))->getContainer();
$settings = $settingsProvider->getSettings();
$whoops = new Run;
if ($settings->environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $e): void {
error_log('Error: ' . $e->getMessage(), $e->getCode());
echo 'An Error happened';
});
}
$whoops->register();
...
```
Check if everything still works, run your code quality checks and commit the changes before moving on the the next chapter.
Check if everything still works, run your code quality checks and commit the changes before moving on the next chapter.
You might notice that phpstan throws an error as there is a documented violation missing. You can either regenerate the
baseline, or simply remove that line from the `phpstan-baseline.neon` file.
[<< previous](11-templating.md) | [next >>](13-refactoring.md)