Welcome to Admin Junkies, Guest — join our community!

Register or log in to explore all our content and services for free on Admin Junkies.

PHP Web Application Development:: App Setup, Configuration and Bootstrapping.

Tyrsson

Retired Staff
Retired Staff
Joined
Apr 29, 2023
Messages
402
Website
github.com
Credits
1,098
Ok, so here we go.

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.
Preferably you will have a setup similar to this:
Using Git, Github and a local server for development

Ok, so here are the steps.
  1. Fork and clone this repo: https://github.com/Tyrsson/twitch
  2. Create a vhost for the project
  3. Open the directory in Vscode as a project
  4. 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.
  5. Run composer install
As soon as the composer downloads and installs are complete you should have a working build. Just open your browser and navigate to the vhost you setup in step 2.
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();
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.
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.
  1. Kernel (the application class. Named Kernel to prevent confusion with the App namespace, since they would be different).
  2. 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.
  3. It maps Laminas\View\HelperPluginManager to a factory to allow usage of all but one of the default Laminas View helpers.
  4. It maps Laminas\View\Resolver\TemplatePathStack to a factory to allow template path resolution based on configuration.
  5. It maps the Laminas\View\Renderer\PhpRenderer to a factory
  6. It maps Laminas\View\View to a factory
  7. 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.
  8. 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.
Important note:
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
    ) {
    }
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
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:

Log in or register to unlock full forum benefits!

Log in or register to unlock full forum benefits!

Register

Register on Admin Junkies completely free.

Register now
Log in

If you have an account, please log in

Log in
Activity
So far there's no one here

Users who are viewing this thread

Would You Rather #9

  • Start a forum in a popular but highly competitive niche

    Votes: 5 21.7%
  • Initiate a forum within a limited-known niche with zero competition

    Votes: 18 78.3%
Win this space by entering the Website of The Month Contest

Theme editor

Theme customizations

Graphic Backgrounds

Granite Backgrounds