tobento/app

Base application for creating any PHP project.

1.0.7 2023-08-31 15:45 UTC

This package is auto-updated.

Last update: 2024-10-30 01:48:28 UTC


README

Base application for creating any PHP project.
You might get started with the App Skeleton where you will find a list of available app bundles.

Table of Contents

Getting Started

Add the latest version of the app project running this command.

composer require tobento/app

Requirements

  • PHP 8.0 or greater

Documentation

App Lifecycle

First, create the app from the provided app factory.
Next, register any boot you want within your app.
Finally, run your app.

use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Adding boots
$app->boot(\Tobento\App\Boot\App::class);
$app->boot(\Tobento\App\Boot\ErrorHandling::class);

// Run the app
$app->run();

App Factory

use Tobento\App\AppFactory;
use Tobento\App\AppFactoryInterface;
use Tobento\App\AppInterface;
use Tobento\Service\Resolver\ResolverFactoryInterface;
use Tobento\Service\Booting\BooterInterface;
use Tobento\Service\Dir\DirsInterface;

$appFactory = new AppFactory();

var_dump($appFactory instanceof AppFactoryInterface);
// bool(true)

$app = $appFactory->createApp(
    resolverFactory: null, // null|ResolverFactoryInterface
    booter: null, // null|BooterInterface
    dirs: null, // null|DirsInterface
);

var_dump($app instanceof AppInterface);
// bool(true)

Parameters explanation

App Resolving

PSR-11

use Tobento\App\AppFactory;

class Foo {}

$app = (new AppFactory())->createApp();

var_dump($app->has(Bar::class));
// bool(false)

var_dump($app->get(Foo::class));
// object(Foo)#2 (0) { }

Autowiring

The app resolves any dependencies by autowiring, except build-in parameters needs a definition to be resolved.

On union types parameter, the first resolvable parameter gets used if not set by definiton.

Definitions

use Tobento\App\AppFactory;
use Tobento\Service\Resolver\DefinitionInterface;

class Foo
{
    public function __construct(
        protected string $name
    ) {} 
}

$app = (new AppFactory())->createApp();

$definition = $app->set(Foo::class)->construct('name');

var_dump($definition instanceof DefinitionInterface);
// bool(true)

Check out the Resolver Definitions to learn more about definitions in general.

Make

use Tobento\App\AppFactory;

class Foo
{
    public function __construct(
        private Bar $bar,
        private string $name
    ) {} 
}

class Bar {}

$app = (new AppFactory())->createApp();

$foo = $app->make(Foo::class, ['name' => 'value']);

Check out Resolver Make to learn more about it.

Call

use Tobento\App\AppFactory;

class Foo
{
    public function index(Bar $bar, string $name): string
    {
        return $name;
    } 
}

class Bar {}

$app = (new AppFactory())->createApp();

$name = $app->call([Foo::class, 'index'], ['name' => 'value']);

var_dump($name);
// string(5) "value"

Check out Resolver Call to learn more about it.

On

use Tobento\App\AppFactory;

class AdminUser {}
class GuestUser {}

$app = (new AppFactory())->createApp();

$app->on(AdminUser::class, GuestUser::class);

$user = $app->get(AdminUser::class);

var_dump($user);
// object(GuestUser)#16 (0) { }

Check out Resolver On to learn more about it.

App Booting

You may call the booting method to boot the registered boots so as having access to its functionality, otherwise it gets called on the run method.

use Tobento\App\AppFactory;
use Tobento\App\Boot;

interface ServiceInterface {}
class Service implements ServiceInterface {}

class ServiceBoot extends Boot
{
    public function boot()
    {
        $this->app->set(ServiceInterface::class, Service::class);
    }
}

$app = (new AppFactory())->createApp();

$app->boot(ServiceBoot::class);

var_dump($app->has(ServiceInterface::class));
// bool(false)

// Do the booting.
$app->booting();

var_dump($app->has(ServiceInterface::class));
// bool(true)

// Run the app
$app->run();

App Directories

You may add directories for its later usage.

use Tobento\App\AppFactory;
use Tobento\Service\Dir\DirsInterface;

$app = (new AppFactory())->createApp();

$app->dirs()
    ->dir(dir: 'path/to/config', name: 'config', group: 'config')
    ->dir(dir: 'path/to/view', name: 'view');
    
var_dump($app->dir(name: 'view'));
// string(13) "path/to/view/"

var_dump($app->dirs() instanceof DirsInterface);
// bool(true)

Check out the Dir Service to learn more about dirs in general.

App Clock

use Tobento\App\AppFactory;
use Psr\Clock\ClockInterface;

$app = (new AppFactory())->createApp();

// Get the app clock:
var_dump($app->clock() instanceof ClockInterface);
// bool(true)

// or:
var_dump($app->get(ClockInterface::class) instanceof ClockInterface);
// bool(true)

App Macros

use Tobento\App\AppFactory;

$app = (new AppFactory())->createApp();

$app->addMacro('lowercase', function(string $string): string {
    return strtolower($string);
});

var_dump($app->lowercase('Lorem'));
// string(5) "lorem"

Check out the Macro Service to learn more about macros in general.

Available Boots

App Boot

The app boot does the following:

  • boots Config Boot and Functions Boot
  • loads app config file if exist
  • sets app environment based on app config
  • adds specific config directory for environment
  • sets timezone based on app config
  • boots the specified boots from app config
use Tobento\App\AppFactory;

$app = (new AppFactory())->createApp();

$app->boot(\Tobento\App\Boot\App::class);

$app->run();

Config Boot

The config boot does the following:

  • implements the config interface
  • adds config macro

Check out the Config Service to learn more about it in general.

use Tobento\App\AppFactory;
use Tobento\Service\Config\ConfigInterface;

$app = (new AppFactory())->createApp();

$app->dirs()->dir(
    dir: 'path/to/config',
    name: 'config',
    group: 'config'
);
    
$app->boot(\Tobento\App\Boot\Config::class);

$app->booting();

// using interface
$value = $app->get(ConfigInterface::class)->get(
    key: 'app.key',
    default: 'default',
    locale: 'de'
);

// using macro:
$value = $app->config('app.key', 'default');

var_dump($value);
// string(7) "default"

$app->run();

Functions Boot

The functions boot does the following:

  • registers app functions
  • provides register method
  • adds app functions macro

Check out the Helper Function Service to learn more about it in general.

use Tobento\App\AppFactory;
use Tobento\App\AppInterface;

use function Tobento\App\{app, directory, config};

$app = (new AppFactory())->createApp();
    
$app->boot(\Tobento\App\Boot\Functions::class);
$app->booting();

// App function:
var_dump(app() instanceof AppInterface);
// bool(true)

// Directory function:
$app->dirs()->dir('dir/to/foo', 'foo');

var_dump(directory('foo'));
// string(11) "dir/to/foo/"

// Config function:
$app->boot(\Tobento\App\Boot\Config::class);
$app->booting();

var_dump(config(key: 'foo', default: 'foo'));
// string(3) "foo"

// using functions macro:
// $app->functions(__DIR__.'/my-functions.php');

// using boot method:
// $app->get(\Tobento\App\Boot\Functions::class)->register(__DIR__.'/my-functions.php');

$app->run();

Error Handling Boot

The error handling boot does the following:

  • implements the error handling
use Tobento\App\AppFactory;

$app = (new AppFactory())->createApp();

$app->boot(\Tobento\App\Boot\ErrorHandling::class);

$app->run();

Dater Boot

The dater boot does the following:

  • configures DateFormatter with the app.timezone and app.locale config

Check out the Dater Service to learn more about in general.

use Tobento\App\AppFactory;
use Tobento\Service\Dater\DateFormatter;

$app = (new AppFactory())->createApp();

$app->boot(\Tobento\App\Boot\Dater::class);

$app->booting();

$df = $app->get(DateFormatter::class);

var_dump($df->date('now'));
// string(25) "Freitag, 11. Februar 2022"

$app->run();

Handle Boot Errors

You might want to handle errors caused by boots in order to continue running the app:

use Tobento\App\AppFactory;
use Tobento\App\Boot;
use Tobento\App\BootErrorHandlersInterface;
use Tobento\App\BootErrorHandlers;
use Tobento\Service\ErrorHandler\AutowiringThrowableHandlerFactory;

class CausesErrorBoot extends Boot
{
    public function boot(): void
    {
        echo $test();
    }
}

$app = (new AppFactory())->createApp();

$app->set(BootErrorHandlersInterface::class, function() use ($app) {

    $handlers = new BootErrorHandlers(
        new AutowiringThrowableHandlerFactory($app->container())
    );

    $handlers->add(function(Throwable $t): mixed {
        return null;
    });

    return $handlers;
});

$app->boot(\Tobento\App\Boot\ErrorHandling::class);

$app->boot(CausesErrorBoot::class);

$app->run();

Check out the Throwable Handlers to learn more about handlers in general.

Customization

Some boots might implement interfaces on their boot method such as the app http boot. There are several ways to change its implementations. But we need to keep in mind that other boots might have dependent boots defined.

Using the app on method

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Http;
use Psr\Http\Message\ResponseFactoryInterface;

class CustomBoot extends Boot
{
    public function boot(): void
    {
        $this->app->on(
            ResponseFactoryInterface::class,
            \Laminas\Diactoros\ResponseFactory::class
        );
    }
}

$app = (new AppFactory())->createApp();

$app->boot(CustomBoot::class);

$app->run();

Boot extending

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Http;
use Tobento\App\Migration\Boot\Migration;
use Psr\Http\Message\ResponseFactoryInterface;

class CustomHttpBoot extends Http
{
    /**
     * Boot application services.
     *
     * @param Migration $migration
     * @return void
     */
    public function boot(Migration $migration): void
    {
        parent::boot($migration);
        
        // set your custom implementations.
        $this->app->set(
            ResponseFactoryInterface::class,
            \Laminas\Diactoros\ResponseFactory::class
        );
    }
}

$app = (new AppFactory())->createApp();

$app->on(Http::class, CustomHttpBoot::class);

$app->run();

Boot implementation overwriting

use Tobento\App\Boot;
use Tobento\App\Http\Boot\Http;
use Psr\Http\Message\ResponseFactoryInterface;

class CustomBoot extends Boot
{
    public const BOOT = [
        Http::class,
    ];
    
    public function boot(Http $http): void
    {
        // set your custom implementations.
        $this->app->set(
            ResponseFactoryInterface::class,
            \Laminas\Diactoros\ResponseFactory::class
        );
    }
}

$app = (new AppFactory())->createApp();

$app->boot(CustomBoot::class);

$app->run();

Credits