Ok, so here we go.
Apps you will need to follow along.
Using Git, Github and a local server for development
Ok, so here are the steps.
If you have a working build then your good to go. If not just post the issue you are having as a new topic and reference this post and I will help you sort it out.
Moving along.... .. .
Create a new branch from master/main, this will allow you to reset the application to a known working build in case you break it.
So first let's take a look at the application entry point.
Open /your-path/your-project/public/index.php
You will see this:
So, yeah, that is all that is required to run the application.
For those that are not new to programming but may not be using a PSR 11 implementation. The above code really provides a great example of why we should use a ContainerInterface implementation.
So, I'm sure you are probably wondering where the configuration takes place... It happens right here:
Wait, too simple you say? Well, great libraries make that possible and for this application we are using the following primary dependencies of:
The two packages we are covering are config-aggregator and service manager, which is provided as a dependency (service manager) of laminas-view. If I had to describe this combination with a single word that word would be. Powerful.
So, let's take a look at how this works in a little more depth.
Open: /your-path/your-project/config/container.php
The file contains:
So, the reason this works is due to how the container works. Since the config is merged, and all Service -> Factory relationships are defined in the configuration, and are now registered with the container, if we call a service from the container via $serviceInstance = $container->get(ServiceName::class) we will get an instance of that service returned as long as the factory can create an instance and return it. If not we should expect a Laminas\ServiceManager\Exception\ServiceNotCreatedException to be thrown.
So let's take a look at the actual config. The location is mentioned above
Open: /your-path/your-project/config/config.php
In the previous code we use two providers. ArrayProvider and PhpFileProvider. If you are using the PhpFileProvider, please note that the file should still return an array i.e.
I know, you're thinking... I've yet to see a service mapping right?
This is where the magic starts happening.
Open /your-path/your-project/module/App/src/ConfigProvider.php
This ConfigProvider holds all of the service mapping required to initialize the application in the required state. So what does it do exactly. It sets up service to factory mapping for the following services.
The HelperPluginManager expects its configuration, by default, to be provided by a top level config key ['view_helpers'] which we provide. You will most likely notice it shares the same top level config keys with the service manager. The reason for this is that the HelperPluginManager is a specialized instance of the ServiceManager that requires that all services that it manages be of a certain Type.
So, with all of that covered. How does the application use it? For that lets look at the Kernel class, its factory and of course, their usage.
If you are reading this critically, as you should be, you will have noticed that in index.php we did not require the kernel class and then use the keyword "new" to create a instance of the Kernel class. Instead we asked the container for an instance. Why? The reason for that is because the factory is aware of the dependencies required to create a Kernel instance. Furthermore, since those dependencies, are themselves registered with the container the container can create instances as needed ready for injection into the Kernel class. To see it in action let's look at the Kernel _construct() method.
To reduce the amount of boiler plate code I am using constructor promotion here. If you need more info just google that and the php manual will explain it better than I can . Basically it lets you declare the class property when you declare the constructor argument (parameter). If the arg/param has a visibility modifier then it will "promote" that argument to a class property. Since we have all of our wiring in place for this. We can do this:
Index.php
This works because the ServiceManager calls its factory based on the identifier, which means, when we call get on the container it calls the factories __invoke method:
Open /your-path/your-project/module/src/Factory/KernelFactory.php
At this point in the index.php file we have a Kernel instance initialized so that we can ->run() the application.
In the next installment we will look into each service a little deeper and why we have chosen that particular component for the job it needs to perform. We will also look at which requirements from the outline prompted me to choose the component that I did so we have an idea of what we should be asking and why.
Apps you will need to follow along.
- Local server (articles will reference using Apache), and Php >= 8.0 (possibly MySQL/MariaDb at some point in the future).
- Git, Github account, Composer (global install will be referenced)
- Editor, articles will reference VSCode, it's free, and it rocks.
Using Git, Github and a local server for development
Ok, so here are the steps.
- Fork and clone this repo: https://github.com/Tyrsson/twitch
- Create a vhost for the project
- Open the directory in Vscode as a project
- Open the terminal window in vscode. You will want to open the project folder you just cloned from github. The terminal window in vscode will open to that directory.
- Run composer install
If you have a working build then your good to go. If not just post the issue you are having as a new topic and reference this post and I will help you sort it out.
Moving along.... .. .
Create a new branch from master/main, this will allow you to reset the application to a known working build in case you break it.
So first let's take a look at the application entry point.
Open /your-path/your-project/public/index.php
You will see this:
PHP:
<?php
declare(strict_types=1);
use App\Kernel;
chdir(dirname(__DIR__)); // make sure the php process is running from the correct dir
require 'vendor/autoload.php'; // Using the composer autoloader, its pretty much industry standard now
/**
* Lets be nice and keep the global namespace clean.
* self called anonymous function that will create its own scope
* looks kinda javascrippy does it not?
*/
(function () {
$container = require 'config/container.php';
$app = $container->get(Kernel::class);
$app->run();
})();
So, yeah, that is all that is required to run the application.
For those that are not new to programming but may not be using a PSR 11 implementation. The above code really provides a great example of why we should use a ContainerInterface implementation.
So, I'm sure you are probably wondering where the configuration takes place... It happens right here:
PHP:
$container = require 'config/container.php';
Wait, too simple you say? Well, great libraries make that possible and for this application we are using the following primary dependencies of:
JSON:
"require": {
"laminas/laminas-diactoros": "^3.2",
"webinertia/webinertia-utils": "^0.0.10",
"laminas/laminas-view": "^2.30",
"laminas/laminas-config-aggregator": "^1.13"
},
The two packages we are covering are config-aggregator and service manager, which is provided as a dependency (service manager) of laminas-view. If I had to describe this combination with a single word that word would be. Powerful.
So, let's take a look at how this works in a little more depth.
Open: /your-path/your-project/config/container.php
The file contains:
PHP:
<?php
declare(strict_types=1);
use Laminas\ServiceManager\ServiceManager;
// we gotta have this so throw require at it
$config = require __DIR__ . '/config.php';
// build container
$serviceManager = new ServiceManager($config);
/**
* This creates a application service known as config
* so that within our factories we can do this:
* $config = $container->get('config');
* Since merging happens prior to initializing the container, the application factories
* have access to all configuration at this point, and also, all services.
* This is like literally the 5th in the call stack, that's pretty darn early
* */
$serviceManager->setService('config', $config);
// return container
return $serviceManager;
So, the reason this works is due to how the container works. Since the config is merged, and all Service -> Factory relationships are defined in the configuration, and are now registered with the container, if we call a service from the container via $serviceInstance = $container->get(ServiceName::class) we will get an instance of that service returned as long as the factory can create an instance and return it. If not we should expect a Laminas\ServiceManager\Exception\ServiceNotCreatedException to be thrown.
So let's take a look at the actual config. The location is mentioned above
Open: /your-path/your-project/config/config.php
PHP:
<?php
declare(strict_types=1);
use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
/**
* This is VERY important.
* The reason is that the service manager expects certain keys to be present at certain levels
* by doing this as we are we do not have to key juggle, nor do we have to add a new method just to return a
* specific key of the array. Its just simpler.
*/
$configProvider = new App\ConfigProvider();
$aggregator = new ConfigAggregator([
// Default App module config
//App\ConfigProvider::class,
new ArrayProvider($configProvider->getDependencyConfig()),
// Load application config in a pre-defined order in such a way that local settings
// overwrite global settings. (Loaded as first to last):
// - `global.php`
// - `*.global.php`
// - `local.php`
// - `*.local.php`
/**
* include the settings files from /data/app. They are stored there because they will be modified
* loading in this order allows for any development mode settings to override them
* without having to change the base values
*/
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
// we want runtime settings that intersect config options to override so they are honored after merge
new PhpFileProvider(realpath(__DIR__ . '/../') . '/data/app/settings/{,*}.php'),
// Load development config if it exists
new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
]);
// return the merged config
return $aggregator->getMergedConfig();
PHP:
// .....
return [// some service manager structured config];
I know, you're thinking... I've yet to see a service mapping right?
This is where the magic starts happening.
Open /your-path/your-project/module/App/src/ConfigProvider.php
PHP:
<?php
declare(strict_types=1);
namespace App;
use App\View\Helper\Config;
use App\View\Helper\Factory\ConfigFactory;
use App\View\Helper\MenuHelper;
use App\View\Helper\Factory\MenuHelperFactory;
use Laminas\View;
use Psr\Http\Message\ServerRequestInterface;
final class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencyConfig(),
];
}
public function getDependencyConfig(): array
{
return [
'factories' => [
Kernel::class => Factory\KernelFactory::class,
ServerRequestInterface::class => Factory\RequestFactory::class,
View\HelperPluginManager::class => Factory\HelperPluginManagerFactory::class,
View\Resolver\TemplatePathStack::class => Factory\TemplatePathStackFactory::class,
View\Renderer\PhpRenderer::class => Factory\PhpRendererfactory::class,
View\View::class => Factory\ViewFactory::class,
],
'view_helpers' => [
'aliases' => [
'config' => Config::class,
'menu' => MenuHelper::class,
],
'factories' => [
Config::class => ConfigFactory::class,
MenuHelper::class => MenuHelperFactory::class,
],
],
'view_manager' => [
'base_path' => '/',
'display_not_found_reason' => true,
'display_exceptions' => true,
'doctype' => 'HTML5',
'default_template_suffix' => 'phtml', // this can be set to php, tpl etc
'not_found_template' => 'error/404',
'exception_template' => 'error/index',
'template_path_stack' => [
__DIR__ . '/../view',
],
],
];
}
}
This ConfigProvider holds all of the service mapping required to initialize the application in the required state. So what does it do exactly. It sets up service to factory mapping for the following services.
- Kernel (the application class. Named Kernel to prevent confusion with the App namespace, since they would be different).
- It maps Psr\Http\Message\ServerRequestInterface => App\Factory\RequestFactory which creates and returns an instance of Laminas\Diactoros\ServerRequest. Internally it uses Laminas\Diactoros\ServerRequestFactory::fromGlobals() to accomplish it work.
- It maps Laminas\View\HelperPluginManager to a factory to allow usage of all but one of the default Laminas View helpers.
- It maps Laminas\View\Resolver\TemplatePathStack to a factory to allow template path resolution based on configuration.
- It maps the Laminas\View\Renderer\PhpRenderer to a factory
- It maps Laminas\View\View to a factory
- It sets up the HelperPluginManager config which creates two aliases (more on this later) and it provides two helpers for the View layer. Config and Menu (more on these later as well). Basically the reason for the aliases is to allow the method overloading via PhpRenderer to call the helpers by a short name via is scope and context i.e. in a template file $this->menu(...);. I covered this in the template deep dive articles.
- It provides a top level config key of ['view_manager'] which some of the helpers expects to be present if you are to provide their default values via configuration, such as the base_path and doctype. It also holds the keys for error and 404 templates (explained in a future iteration) and the base template_path_stack key which is a relative path to our templates for this module.
The HelperPluginManager expects its configuration, by default, to be provided by a top level config key ['view_helpers'] which we provide. You will most likely notice it shares the same top level config keys with the service manager. The reason for this is that the HelperPluginManager is a specialized instance of the ServiceManager that requires that all services that it manages be of a certain Type.
So, with all of that covered. How does the application use it? For that lets look at the Kernel class, its factory and of course, their usage.
If you are reading this critically, as you should be, you will have noticed that in index.php we did not require the kernel class and then use the keyword "new" to create a instance of the Kernel class. Instead we asked the container for an instance. Why? The reason for that is because the factory is aware of the dependencies required to create a Kernel instance. Furthermore, since those dependencies, are themselves registered with the container the container can create instances as needed ready for injection into the Kernel class. To see it in action let's look at the Kernel _construct() method.
PHP:
public function __construct(
private ServerRequest $request,
private View $view,
private array $config
) {
}
Index.php
PHP:
// This returns a configured container instance that is aware of all of our service mappings
$container = require 'config/container.php';
// This returns us a instance of the Kernel class ready for use
This works because the ServiceManager calls its factory based on the identifier, which means, when we call get on the container it calls the factories __invoke method:
Open /your-path/your-project/module/src/Factory/KernelFactory.php
PHP:
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Kernel;
use Laminas\View\View;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
final class KernelFactory
{
public function __invoke(ContainerInterface $container): Kernel
{
// return our instance, injected with our initialized dependencies, created by their respective factories
return new Kernel(
$container->get(ServerRequestInterface::class), // calls the RequestFactory
$container->get(View::class), // calls the View::class factory
$container->get('config') // remember the 'config' service we created when we built the container, this is its usage
);
}
}
At this point in the index.php file we have a Kernel instance initialized so that we can ->run() the application.
In the next installment we will look into each service a little deeper and why we have chosen that particular component for the job it needs to perform. We will also look at which requirements from the outline prompted me to choose the component that I did so we have an idea of what we should be asking and why.
Last edited: