Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions docs/0-getting-started/02-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Tempest won't impose any file structure on you: one of its core features is that
For instance, Tempest is able to differentiate between a controller method and a console command by looking at the code, instead of relying on naming conventions or configuration files.

:::info
This concept is called [discovery](../4-internals/02-discovery), and is one of Tempest's most powerful features.
This concept is called [discovery](../1-essentials/05-discovery), and is one of Tempest's most powerful features.
:::

The following project structures work the same way in Tempest, without requiring any specific configuration:
Expand All @@ -98,9 +98,11 @@ The following project structures work the same way in Tempest, without requiring

## About discovery

Discovery works by scanning your project code, and looking at each file and method individually to determine what that code does. In production environments, [Tempest will cache the discovery process](../4-internals/02-discovery#discovery-in-production), avoiding any performance overhead.
Discovery works by scanning your project code and looking at each file and method individually to determine what that code does. In production environments, [Tempest caches the discovery process](../1-essentials/05-discovery#discovery-in-production), avoiding any performance overhead.

As an example, Tempest is able to determine which methods are controller methods based on their route attributes, such as `#[Get]` or `#[Post]`:
As an example, Tempest is able to determine which methods are controller methods based on their [route attributes](../1-essentials/01-routing.md), or to detect console commands based on methods annotated with {b`#[Tempest\Console\ConsoleCommand]`}:

:::code-group

```php app/BlogPostController.php
use Tempest\Router\Get;
Expand All @@ -119,18 +121,22 @@ final readonly class BlogPostController
}
```

Likewise, it is able to detect console commands based on the `#[ConsoleCommand]` attribute:

```php app/RssSyncCommand.php
use Tempest\Console\HasConsole;
use Tempest\Console\ConsoleCommand;

final readonly class RssSyncCommand
{
use HasConsole;

#[ConsoleCommand('rss:sync')]
public function __invoke(bool $force = false): void
{ /* … */ }
{
// …
}
}
```

:::

:::tip{tabler:link}
Learn more about discovery in the [dedicated documentation](../1-essentials/05-discovery.md).
:::
2 changes: 1 addition & 1 deletion docs/1-essentials/03-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ Tempest uses migrations to create and update databases across different environm

### Writing migrations

Classes implementing the {b`Tempest\Database\DatabaseMigration`} interface and `.sql` files are automatically [discovered](../4-internals/02-discovery) and registered as migrations. These files can be stored anywhere in the application.
Classes implementing the {b`Tempest\Database\DatabaseMigration`} interface and `.sql` files are automatically [discovered](../1-essentials/05-discovery) and registered as migrations. These files can be stored anywhere in the application.

:::code-group

Expand Down
6 changes: 3 additions & 3 deletions docs/1-essentials/04-console-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Learn how to write console commands with a modern, minimal syntax.

## Overview

Tempest leverages [discovery](../4-internals/02-discovery.md) to find class methods tagged with the {b`#[Tempest\Console\ConsoleCommand]`} attribute. Such methods will automatically be available as console commands through the `./tempest` executable.
Tempest leverages [discovery](./05-discovery.md) to find class methods tagged with the {b`#[Tempest\Console\ConsoleCommand]`} attribute. Such methods will automatically be available as console commands through the `./tempest` executable.

Additionally, Tempest supports [console middleware](#middleware), which makes it easier to build some console features.

Expand Down Expand Up @@ -312,7 +312,7 @@ Tempest provides a few built-in middleware that you may use on your console comm

## Scheduling

Console commands—or any public class method—may be scheduled by using the {b`#[Tempest\Console\Schedule]`} attribute, which accepts an {b`Tempest\Console\Scheduler\Interval`} or {b`Tempest\Console\Scheduler\Every`} value. Methods with this attributes are automatically [discovered](../4-internals/02-discovery.md), so there is nothing more to add.
Console commands—or any public class method—may be scheduled by using the {b`#[Tempest\Console\Schedule]`} attribute, which accepts an {b`Tempest\Console\Scheduler\Interval`} or {b`Tempest\Console\Scheduler\Every`} value. Methods with this attributes are automatically [discovered](./05-discovery.md), so there is nothing more to add.

You may read more on the [dedicated chapter](../2-features/11-scheduling.md).

Expand All @@ -331,4 +331,4 @@ $this->console
->assertSee('caution')
->submit()
->assertSuccess();
```
```
2 changes: 1 addition & 1 deletion docs/1-essentials/05-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ final readonly class CacheRepository implements Repository
When you request the `Repository` from the container, Tempest will automatically wrap the original implementation with your decorator. The decorated object (the original `Repository`) is injected into the decorator's constructor.

:::info
Decorators are discovered automatically through Tempest's [discovery](../4-internals/02-discovery.md), so you don't need to manually register them.
Decorators are discovered automatically through Tempest's [discovery](./05-discovery.md), so you don't need to manually register them.
:::

## Proxy loading
Expand Down
211 changes: 211 additions & 0 deletions docs/1-essentials/05-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
title: Discovery
description: "Tempest automatically locates controller actions, event handlers, console commands, and other components of your application, without needing any configuration from you."
---

## 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:

```php app/HomeController.php
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 in production. You can read more about caching in the [development](#discovery-for-local-development) and [production](#discovery-in-production) sections.

:::info
Read the [getting started with discovery](/blog/discovery-explained) guide if you want to know more about the philosophy of discovery and how it works.
:::

## Built-in discovery classes

Most of Tempest's features are built on top of discovery. The following is a non-exhaustive list that describes which discovery class is associated to which feature.

- {b`Tempest\Core\DiscoveryDiscovery`} discovers other discovery classes. This class is run manually by the framework when booted.
- {b`Tempest\CommandBus\CommandBusDiscovery`} discovers methods with the {b`#[Tempest\CommandBus\CommandHandler]`} attribute and registers them into the [command bus](../2-features/10-command-bus.md).
- {b`Tempest\Console\Discovery\ConsoleCommandDiscovery`} discovers methods with the {b`#[Tempest\Console\ConsoleCommand]`} attribute and registers them as [console commands](../1-essentials/04-console-commands.md).
- {b`Tempest\Console\Discovery\ScheduleDiscovery`} discovers methods with the {b`#[Tempest\Console\Schedule]`} attribute and registers them as [scheduled tasks](../2-features/11-scheduling.md).
- {b`Tempest\Container\InitializerDiscovery`} discovers classes that implement {b`\Tempest\Container\Initializer`} or {b`\Tempest\Container\DynamicInitializer`} and registers them as [dependency initializers](./05-container.md#dependency-initializers).
- {b`Tempest\Database\MigrationDiscovery`} discovers classes that implement {b`Tempest\Database\MigratesUp`} or {b`Tempest\Database\MigratesDown`} and registers them as [migrations](./03-database.md#migrations).
- {b`Tempest\EventBusDiscovery\EventBusDiscovery`} discovers methods with the {b`#[Tempest\EventBus\EventHandler]`} attribute and registers them in the [event bus](../2-features/08-events.md).
- {b`Tempest\Router\RouteDiscovery`} discovers route attributes on methods and registers them as [controller actions](./01-routing.md) in the router.
- {b`Tempest\Mapper\MapperDiscovery`} discovers classes that implement {b`Tempest\Mapper\Mapper`} and registers them for [mapping](../2-features/01-mapper.md#mapper-discovery).
- {b`Tempest\Mapper\CasterDiscovery`} discovers classes that implement {b`Tempest\Mapper\DynamicCaster`} and registers them as [casters](../2-features/01-mapper.md#casters-and-serializers).
- {b`Tempest\Mapper\SerializerDiscovery`} discovers classes that implement {b`Tempest\Mapper\DynamicSerializer`} and registers them as [serializers](../2-features/01-mapper.md#casters-and-serializers).
- {b`Tempest\View\ViewComponentDiscovery`} discovers `x-*.view.php` files and registers them as [view components](../1-essentials/02-views.md#view-components).
- {b`Tempest\Vite\ViteDiscovery`} discovers `*.entrypoint.{ts,js,css}` files and register them as [entrypoints](../2-features/02-asset-bundling.md#entrypoints).
- {b`Tempest\Auth\AccessControl\PolicyDiscovery`} discovers methods annotated with the {b`#[Tempest\Auth\AccessControl\Policy]`} attribute and registers them as [access control policies](../2-features/04-authentication.md#access-control).

## Implementing your own discovery

### Discovering code in classes

Tempest discovers classes that implement {b`Tempest\Discovery\Discovery`}, which requires implementing the `discover()` and `apply()` methods. The {b`Tempest\Discovery\IsDiscovery`} trait provides the rest of the implementation.

The `discover()` method accepts a {b`Tempest\Core\DiscoveryLocation`} and a {b`Tempest\Reflection\ClassReflector`} parameter. The reflector can be used to loop through a class' attributes, methods, parameters or anything else. If the class matches your expectations, you may register it using `$this->discoveryItems->add()`.

As an example, the following is a simplified version of the event bus discovery:

```php EventBusDiscovery.php
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

It is possible to discover files instead of classes. 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 {b`\Tempest\Discovery\DiscoversPath`} interface. It requires a `discoverPath()` method that accepts a {b`Tempest\Core\DiscoveryLocation`} and a string path.

The example below shows a simplified version of the Vite entrypoint discovery:

```php ViteDiscovery.php
use Tempest\Discovery\Discovery;
use Tempest\Discovery\DiscoversPath;
use Tempest\Discovery\IsDiscovery;
use Tempest\Support\Str;

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 (! Str\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

Discovery is a really powerful feature, but it comes with performance considerations. At its core, it loops through all files in your project, including vendors. For this reason, discovery information is automatically cached in production environments.

Caching is done by running the `discovery:generate` command, which should be part of your deployment pipeline before any other Tempest command.

```console ">_ ./tempest discovery:generate --no-interaction"
Clearing discovery cache <dim>.....................................</dim> <strong>2025-12-30 15:51:46</strong>
Clearing discovery cache <dim>.....................................</dim> <strong>DONE</strong>
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>2025-12-30 15:51:46</strong>
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>DONE</strong>
```

## Discovery for local development

During development, discovery is enabled without a cache. Depending on the size of your project, you may benefit from enabling the partial cache strategy:

```env .env
{:hl-property:DISCOVERY_CACHE:}={:hl-keyword:partial:}
```

This strategy only caches discovery for vendor files. For this reason, it is recommended to run `discovery:generate` after every composer update:

```json composer.json
{
"scripts": {
"post-package-update": [
"php tempest discovery:generate"
]
}
}
```

:::info
If your project was created using {`tempest/app`}, the `post-package-update` script is already included.
:::

## Excluding files and classes from discovery

If needed, you can always exclude discovered files and classes by providing a discovery config file:

```php app/discovery.config.php
use Tempest\Core\DiscoveryConfig;

return new DiscoveryConfig()
->skipClasses(GlobalHiddenDiscovery::class)
->skipPaths(__DIR__ . '/../../Fixtures/GlobalHiddenPathDiscovery.php');
```
2 changes: 1 addition & 1 deletion docs/1-essentials/06-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Even though the framework is designed to use as little configuration as possible

## Configuration files

Files ending with `*.config.php` are recognized by Tempest's [discovery](../4-internals/02-discovery) as configuration objects, and will be registered as [singletons](./01-container#singletons) in the container.
Files ending with `*.config.php` are recognized by Tempest's [discovery](../1-essentials/05-discovery) as configuration objects, and will be registered as [singletons](./01-container#singletons) in the container.

```php app/postgres.config.php
use Tempest\Database\Config\PostgresConfig;
Expand Down
2 changes: 1 addition & 1 deletion docs/2-features/11-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ final readonly class SetLocaleMiddleware implements HttpMiddleware

## Defining translation messages

Translation messages are usually stored in translation files. Tempest automatically [discovers](../4-internals/02-discovery.md) YAML and JSON translation files that use the `<name>.<locale>.{yaml,json}` naming format, where `<name>` may be any string, and `<locale>` must be an [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code.
Translation messages are usually stored in translation files. Tempest automatically [discovers](../1-essentials/05-discovery.md) YAML and JSON translation files that use the `<name>.<locale>.{yaml,json}` naming format, where `<name>` may be any string, and `<locale>` must be an [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code.

For instance, you may store translation files in a `lang` directory:

Expand Down
2 changes: 1 addition & 1 deletion docs/2-features/11-scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: 'Tempest provides a modern and convenient way of scheduling tasks,

## Overview

Dealing with repeating, scheduled tasks is as simple as adding the {`#[Tempest\Console\Schedule]`} attribute to any class method. As with console commands, [discovery](../4-internals/02-discovery.md) takes care of finding these methods and registering them.
Dealing with repeating, scheduled tasks is as simple as adding the {`#[Tempest\Console\Schedule]`} attribute to any class method. As with console commands, [discovery](../1-essentials/05-discovery.md) takes care of finding these methods and registering them.

## Using the scheduler

Expand Down
2 changes: 1 addition & 1 deletion docs/2-features/14-exception-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Tempest comes with its own exception handler, which provides a simple way to cat

When an exception is thrown, it will be caught and piped through the registered exception processors. By default, the only registered exception processor, {b`Tempest\Core\LogExceptionProcessor`}, will simply log the exception.

Of course, you may create your own exception processors. This is done by creating a class that implements the {`Tempest\Core\ExceptionProcessor`} interface. Classes implementing this interface are automatically [discovered](../4-internals/02-discovery.md), so you don't need to register them manually.
Of course, you may create your own exception processors. This is done by creating a class that implements the {`Tempest\Core\ExceptionProcessor`} interface. Classes implementing this interface are automatically [discovered](../1-essentials/05-discovery.md), so you don't need to register them manually.

## Reporting exceptions

Expand Down
2 changes: 1 addition & 1 deletion docs/3-packages/02-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ ConsoleApplication::boot()->run();

## Registering commands

`tempest/console` relies on [discovery](../4-internals/02-discovery.md) to find and register console commands. That means you don't have to register any commands manually, and any method within your codebase using the `{php}#[ConsoleCommand]` attribute will automatically be discovered by your console application.
`tempest/console` relies on [discovery](../1-essentials/05-discovery.md) to find and register console commands. That means you don't have to register any commands manually, and any method within your codebase using the `{php}#[ConsoleCommand]` attribute will automatically be discovered by your console application.

You may read more about building commands in the [dedicated documentation](../1-essentials/04-console-commands.md).

Expand Down
Loading