Discovery
Overview
Tempest introduces a unique approach to bootstrapping an application. Instead of requiring manual registration of project code and packages, Tempest automatically scans the codebase and detects the components that should be loaded. This process is called discovery.
Discovery is powered by composer metadata. Every package that depends on Tempest, along with your application's own code, are included in the discovery process. Tempest applies various rules to determine the purpose of different pieces of code. It can analyze file names, attributes, interfaces, return types, and more.
For instance, web routes are discovered based on route attributes:
final readonly class HomeController { #[Get(uri: '/home')] public function __invoke(): View { return view('home.view.php'); } }
Note that Tempest is able to cache discovery information to avoid any performance cost. Enabling this cache in production is highly recommended.
Built-in discovery classes
Most of Tempest's features are built on top of discovery. The following describes which discovery class is associated to which feature.
Tempest\Core\DiscoveryDiscovery
Discovers other discovery classes. This class is run manually by the framework when booted.Tempest\CommandBus\CommandBusDiscovery
Discovers methods with the#[CommandHandler]
attribute and registers them into the command bus.Tempest\Console\Discovery\ConsoleCommandDiscovery
Discovers methods with the#[ConsoleCommand]
attribute and registers them as console commands.Tempest\Console\Discovery\ScheduleDiscovery
Discovers methods with the#[Schedule]
attribute and registers them as scheduled tasks.Tempest\Container\InitializerDiscovery
Discovers classes that implementInitializer
orDynamicInitializer
and registers them in the container.Tempest\Database\MigrationDiscovery
Discovers classes that implementTempest\Database\Migration
and registers them in the migration manager.Tempest\EventBusDiscovery\EventBusDiscovery
Discovers methods with the#[EventHandler]
attribute and registers them in the event bus.Tempest\Router\RouteDiscovery
Discovers route attributes on methods and registers them as controller actions in the router.Tempest\Mapper\MapperDiscovery
Discovers classes that implementTempest\Mapper\Mapper
, which are registered in\Tempest\Mapper\ObjectFactory
Tempest\View\ViewComponentDiscovery
Discovers classes that implementTempest\View\ViewComponent
, as well as view files that contain<x-component>
or namedx-*.view.php
Tempest\Vite\ViteDiscovery
Discovers*.entrypoint.{ts,js,css}
files and register them as entrypoints.
Implementing your own discovery
Discovering code in classes
Tempest will discover classes that implement Tempest\Discovery\Discovery
. You may create one, and implement the discover()
and apply
methods.
The discover()
method accepts a DiscoveryLocation
and a ClassReflector
parameter. You may use the latter to loop through a class' attributes, methods, parameters or anything else.
If you find what you are interested in, you may register it using $this->discoveryItems->add()
. As an example, the following is a simplified version of the event bus discovery:
use Tempest\Discovery\Discovery; use Tempest\Discovery\IsDiscovery; final readonly class EventBusDiscovery implements Discovery { // This provides the default implementation for `Discovery`'s internals use IsDiscovery; public function __construct( // Discovery classes are autowired, // so you can inject all dependencies you need private EventBusConfig $eventBusConfig, ) { } public function discover(DiscoveryLocation $location, ClassReflector $class): void { foreach ($class->getPublicMethods() as $method) { $eventHandler = $method->getAttribute(EventHandler::class); // Extra checks to determine whether // we can actually use the current method as an event handler // … // Finally, we add all discovery-related data into `$this->discoveryItems`: $this->discoveryItems->add($location, [$eventName, $eventHandler, $method]); } } // Next, the `apply` method is called whenever discovery is ready to be // applied into the framework. In this case, we want to loop over all // registered discovery items, and add them to the event bus config. public function apply(): void { foreach ($this->discoveryItems as [$eventName, $eventHandler, $method]) { $this->eventBusConfig->addClassMethodHandler( event: $eventName, handler: $eventHandler, reflectionMethod: $method, ); } } }
Discovering files
In some situations, you may want to not just discover classes, but also files. For instance, view files, front-end entrypoints or SQL migrations are not PHP classes, but still need to be discovered.
In this case, you may implement the additional Tempest\Discovery\DiscoversPath
interface. It will allow a discovery class to discover all paths that aren't classes as well. As an example, below is a simplified version of the Vite discovery:
use Tempest\Discovery\Discovery; use Tempest\Discovery\DiscoversPath; use Tempest\Discovery\IsDiscovery; final class ViteDiscovery implements Discovery, DiscoversPath { use IsDiscovery; public function __construct( private readonly ViteConfig $viteConfig, ) {} // We are not discovering any class, so we return immediately. public function discover(DiscoveryLocation $location, ClassReflector $class): void { return; } // This method is called for every file in registered discovery locations. // We can use the `$path` to determine whether we are interested in it. public function discoverPath(DiscoveryLocation $location, string $path): void { // We are insterested in `.ts`, `.css` and `.js` files only. if (! ends_with($path, ['.ts', '.css', '.js'])) { return; } // These files need to be specifically marked as `.entrypoint`. if (! str($path)->beforeLast('.')->endsWith('.entrypoint')) { return; } $this->discoveryItems->add($location, [$path]); } // When discovery is cached, `discover` and `discoverPath` are not called. // Instead, `discoveryItems` is already fed with serialized data, which // we can use. In this case, we add the paths to the Vite config. public function apply(): void { foreach ($this->discoveryItems as [$path]) { $this->viteConfig->addEntrypoint($path); } } }
Discovery in production
While discovery is a really powerful feature, it also comes with some performance considerations. In production environments, you need to make sure that the discovery workflow is cached. This is done by using the DISCOVERY_CACHE
environment variable:
DISCOVERY_CACHE=true
The most important step is to generate that cache. This is done by running the discovery:generate
, which should be part of your deployment pipeline. Make sure to run it before any other Tempest command.
./tempest discovery:generate ℹ Clearing existing discovery cache… ✓ Discovery cached has been cleared ℹ Generating new discovery cache… (cache strategy used: all) ✓ Cached 1119 items
Discovery for local development
By default, the discovery cache is disabled in a development environment. Depending on your local setup, it is likely that you will not run into noticeable slowdowns. However, for larger projects, you might benefit from enabling a partial discovery cache:
DISCOVERY_CACHE=partial
This caching strategy will only cache discovery for vendor files. For this reason, it is recommended to run discovery:generate
after every composer update:
{ "scripts": { "post-package-update": [ "php tempest discovery:generate" ] } }
Note that, if you've created your project using tempest/app
, you'll have the post-package-update
script already included. You may read the internal documentation about discovery to learn more.