tobento/apps

Multiple apps support. Each app will run in its own application.

Maintainers

Package info

github.com/tobento-ch/apps

Homepage

pkg:composer/tobento/apps

Transparency log

Statistics

Installs: 54

Dependents: 7

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.1 2026-07-31 16:51 UTC

This package is auto-updated.

Last update: 2026-07-31 16:53:39 UTC


README

Multiple apps support. Each app will run in its own application.

Table of Contents

Getting Started

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

composer require tobento/apps

Requirements

  • PHP 8.4 or greater

Documentation

App

Check out the App Skeleton if you are using the skeleton.

You may also check out the App to learn more about the app in general.

Creating A New App

To create a new app, simply extend the AppBoot::class and define the constants as you need it.

use Tobento\Apps\AppBoot;

class Backend extends AppBoot
{
    /**
     * Specify your app boots:
     */
    protected const APP_BOOT = [
        //\Tobento\App\Console\Boot\Console::class,
        //\Tobento\App\User\Web\Boot\UserWeb::class,
    ];
    
    /**
     * Set a unique app id. Must be lowercase and
     * only contain [a-z0-9-] characters.
     * Furthermore, do not set ids with two dashes such as 'foo--bar'
     * as supapps id will be separated by two dashes.
     */
    protected const APP_ID = 'backend';

    /**
     * You may set a slug for the routing e.g. example.com/slug/
     * Or you may set the slug to an empty string e.g. example.com/
     */
    protected const SLUG = 'admin';
    
    /**
     * You may set a domains for the routing e.g. ['api.example.com']
     * In addition, you may set the slug to an empty string,
     * otherwise it gets appended e.g. api.example.com/slug
     */
    protected const DOMAINS = [];
    
    /**
     * You may set a migration to be installed on booting e.g Migration::class
     */
    protected const MIGRATION = '';
}

Allow Sub Apps

If your app supports sub apps, set the supportsSubapps property to true.

use Tobento\Apps\AppBoot;

class DomainFoo extends AppBoot
{
    /**
     * Specify your app boots:
     */
    protected const APP_BOOT = [
        Backend::class,
        Frontend::class,
    ];
    
    /**
     * Set a unique app id. Must be lowercase and
     * only contain [a-z0-9-] characters.
     * Furthermore, do not set ids with two dashes such as 'foo--bar'
     * as supapps id will be separated by two dashes.
     */
    protected const APP_ID = 'domain-foo';

    /**
     * You may set a slug for the routing e.g. example.com/slug/
     * Or you may set the slug to an empty string e.g. example.com/
     */
    protected const SLUG = '';
    
    /**
     * You may set a domains for the routing e.g. ['api.example.com']
     * In addition, you may set the slug to an empty string,
     * otherwise it gets appended e.g. api.example.com/slug
     */
    protected const DOMAINS = ['example.com'];    
    
    /**
     * @var bool
     */
    protected bool $supportsSubapps = true;
}

Booting Apps

After creating your apps, you will need to boot your apps:

use Tobento\App\AppFactory;

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

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots:
$app->boot(DomainFoo::class);
$app->boot(DomainBar::class);
$app->boot(Backend::class);

// Adding app specific boots:
$app->booting();
$app->get(Backend::class)->addBoot(BackendSpecificBoot::class);

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

Example Using Apps Within A Boot

use Tobento\App\Boot;

class Blog extends Boot
{
    public const BOOT = [
        Backend::class,
        Frontend::class,
    ];
    
    public function boot(Backend $backend, Frontend $frontend): void
    {
        $backend->addBoot(BlogBackend::class);
        $frontend->addBoot(BlogFrontend::class);
    }
}

Next, boot your Blog boot:

use Tobento\App\AppFactory;

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

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots:
$app->boot(Blog::class);

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

Apps Config

The configuration for the apps is located in the app/config/apps.php file at the default App Skeleton config location.

Directory Structure

The directory structure will be like:

your-project/
  app/ #root app
    config/
    src/
    ...
  apps/
    backend/
      config/
      views/
      ...
  public/
    apps/
      backend/
        assets/
    assets/
    index.php
  vendor/

Sharing Configurations

You may share configurations between apps using the app:root or app:parent directory to point to the same directory:

In each app config/database.php:

'defaults' => [
    'pdo' => 'mysql',
    'storage' => 'file',
    'shared:storage' => 'shared:file',
],
    
'databases' => [
    'shared:file' => [
        'factory' => \Tobento\Service\Database\Storage\StorageDatabaseFactory::class,
        'config' => [
            'storage' => \Tobento\Service\Storage\JsonFileStorage::class,
            'dir' => directory('app:parent').'storage/database/file/',
        ],
    ],
],

And within your app:

use Tobento\Service\Database\DatabasesInterface;

$storageDatabase = $app->get(DatabasesInterface::class)->default('shared:storage');

// or
$fileDatabase = $app->get(DatabasesInterface::class)->get('shared:file');

Accessing Apps

You may access apps from within another app by using the AppsInterface::class to retrieve the desired app.

use Tobento\Apps\AppBoot;
use Tobento\Apps\AppsInterface;
use Tobento\Service\Routing\RouterInterface;

// Boot the app if it has not booted yet:
$app->booting();

// Get the apps:
$apps = $app->get(AppsInterface::class);

// Get any desired app:
var_dump($apps->get('frontend') instanceof AppBoot);
// bool(true)

$frontendApp = $apps->get('frontend')->app();
$frontendApp->booting();

// For instance, get all frontend app routes:
$routes = $frontendApp->get(RouterInterface::class)->getRoutes();

Sub Apps

When accessing a sup app, you will need to boot the parent app first, otherwise the sub app will not be found!

use Tobento\Apps\AppsInterface;
use Tobento\Service\Routing\RouterInterface;

// Boot the app if it has not booted yet:
$app->booting();

// Get the apps:
$apps = $app->get(AppsInterface::class);

// Boot parent app:
$apps->get('domain-foo')->app()->booting();

// Get sub app:
$frontendApp = $apps->get('domain-foo--frontend')->app();
$frontendApp->booting();

// For instance, get all frontend app routes:
$routes = $frontendApp->get(RouterInterface::class)->getRoutes();

CAUTION When booting multiple apps, you will need to reboot the main app using the bootingApp method afterwards, otherwise helper functions will use last booted app container for instance.

use Tobento\Apps\AppsInterface;

// Boot the app if it has not booted yet:
$app->booting();

// Get the apps:
$apps = $app->get(AppsInterface::class);

foreach($apps->all() as $appBoot) {
    $application = $appBoot->app();
    $application->booting();
    // do something
}

$apps->bootingApp($app);

App Finder

The AppFinder allows you to locate and boot apps anywhere in an app tree.
It supports both direct lookup and full recursive search.

Purpose

Apps are organized in a tree structure.
Sub-apps are only discoverable after booting their parent.
AppFinder provides a convenient way to locate an app by its ID and return it in a booted and active context.

Container Context Behavior

Booting an app switches the active container.
This affects all helper functions such as:

  • config()
  • trans()

When AppFinder returns an app, that app remains the active context.
This is intentional: callers typically want to work inside the app they just found.

If you want to switch back to the previous app (usually the main app), you must explicitly call:

$apps->bootingApp($mainApp);

This restores the original container context.

findById(string $id): null|AppInterface

Performs a fast lookup among the current app and its direct children.

  • Returns the app in a booted and active state.
  • Does not restore the previous app context.
  • Supports the special virtual identifier "root".

Use this when you know the app is a direct child or the root app.

use Tobento\App\AppInterface;
use Tobento\Apps\AppFinder;

$finder = new AppFinder($app);

$frontendApp = $finder->findById('frontend');

// Now the frontend app is active
$configValue = config('some.setting');

// Switch back to the main app if needed
$apps->bootingApp($app);

findByIdRecursive(string $id): null|AppInterface

Searches the entire app tree using a depth-first traversal.

  • Boots apps only when needed to discover their sub-apps.
  • Avoids booting the same app more than once using a visited list.
  • Returns the found app in a booted and active state.
  • Does not restore the previous app context when returning an app.
  • Restores the previous app context only when returning null.
  • Supports the special virtual identifier "root".

Use this when apps may be nested multiple levels deep.

use Tobento\App\AppInterface;
use Tobento\Apps\AppFinder;

$finder = new AppFinder($app);

$foundApp = $finder->findByIdRecursive('domain-foo--frontend');

// Now the found app is active
$configValue = config('some.setting');

// Switch back to the main app if needed
$apps->bootingApp($app);

Summary

  • Returned apps are booted and active.
  • The main app is not restored automatically when returning an app.
  • You must manually restore the previous app if you want to switch back.
  • Recursive search explores the entire tree using depth-first traversal.
  • "root" is a virtual identifier and is handled explicitly.

Console

The following commands should be run only on the root app console.

Apps List Command

The apps:list command provides an overview of all the apps:

php ap apps:list

Apps Command

With the apps command you can run any command within each apps.

Runs route:list command on all apps:

php ap apps route:list

Runs route:list command on the frontend and backend app only:

php ap apps route:list --aid=frontend --aid=backend

Apps Create Console Command

You may create for each app a console using the apps:create-console command.

php ap apps:create-console

Once created, the console is available at each app directory:

your-project/
  apps/
    backend/
      config/
      views/
      ap #console

Testing

When using the App Testing bundle, you need to return the specific app you want to test on the createApp method:

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;
use Tobento\Apps\AppsInterface;

final class BackendAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        $app = require __DIR__.'/../app/app.php';
        $app->booting();
        
        // Return the app you want to test:
        return $app->get(AppsInterface::class)->get('backend')->app();
    }
}

Using The Tmp App

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;
use Tobento\Apps\AppsInterface;

final class BackendAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        
        // Boot your apps:
        $app->boot(Backend::class);
        $app->booting();
        
        // Get the app you want to test:
        $app = $app->get(AppsInterface::class)->get('backend')->app();
        
        // You may boot additional boots for testing:
        $app->boot(\Tobento\App\Seeding\Boot\Seeding::class);
        
        return $app;
    }
}

Example using a sub app:

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;
use Tobento\Apps\AppsInterface;

final class BackendAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        
        // Boot your apps:
        $app->boot(DomainFoo::class);
        $app->booting();
        
        // Get and boot parent app:
        $app = $app->get(AppsInterface::class)->get('domain-foo')->app();
        $app->booting();
        
        // Get sub app:
        $app = $app->get(AppsInterface::class)->get('domain-foo--backend')->app();
        
        // You may boot additional boots for testing:
        $app->boot(\Tobento\App\Seeding\Boot\Seeding::class);
        
        return $app;
    }
}

Credits