Skip to content

Commit a9fcb30

Browse files
committed
Merge branch '3.x' into attribute-fallthrough
2 parents 435e91b + dbf25dc commit a9fcb30

File tree

112 files changed

+1535
-851
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+1535
-851
lines changed

docs/0-getting-started/02-installation.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Tempest won't impose any file structure on you: one of its core features is that
7373
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.
7474

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

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

9999
## About discovery
100100

101-
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.
101+
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.
102102

103-
As an example, Tempest is able to determine which methods are controller methods based on their route attributes, such as `#[Get]` or `#[Post]`:
103+
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]`}:
104+
105+
:::code-group
104106

105107
```php app/BlogPostController.php
106108
use Tempest\Router\Get;
@@ -119,18 +121,22 @@ final readonly class BlogPostController
119121
}
120122
```
121123

122-
Likewise, it is able to detect console commands based on the `#[ConsoleCommand]` attribute:
123-
124124
```php app/RssSyncCommand.php
125125
use Tempest\Console\HasConsole;
126126
use Tempest\Console\ConsoleCommand;
127127

128128
final readonly class RssSyncCommand
129129
{
130-
use HasConsole;
131-
132130
#[ConsoleCommand('rss:sync')]
133131
public function __invoke(bool $force = false): void
134-
{ /* … */ }
132+
{
133+
// …
134+
}
135135
}
136136
```
137+
138+
:::
139+
140+
:::tip{tabler:link}
141+
Learn more about discovery in the [dedicated documentation](../1-essentials/05-discovery.md).
142+
:::

docs/1-essentials/03-database.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ Tempest uses migrations to create and update databases across different environm
501501

502502
### Writing migrations
503503

504-
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.
504+
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.
505505

506506
:::code-group
507507

docs/1-essentials/04-console-commands.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: "Learn how to write console commands with a modern, minimal syntax.
55

66
## Overview
77

8-
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.
8+
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.
99

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

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

313313
## Scheduling
314314

315-
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.
315+
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.
316316

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

@@ -331,4 +331,4 @@ $this->console
331331
->assertSee('caution')
332332
->submit()
333333
->assertSuccess();
334-
```
334+
```

docs/1-essentials/05-container.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ final readonly class CacheRepository implements Repository
427427
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.
428428

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

433433
## Proxy loading

docs/1-essentials/05-discovery.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
---
2+
title: Discovery
3+
description: "Tempest automatically locates controller actions, event handlers, console commands, and other components of your application, without needing any configuration from you."
4+
---
5+
6+
## Overview
7+
8+
Tempest introduces a unique approach to bootstrapping applications. 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**.
9+
10+
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.
11+
12+
Tempest applies [various rules](#built-in-discovery-classes) 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 when methods are annotated with route attributes:
13+
14+
```php app/HomeController.php
15+
final readonly class HomeController
16+
{
17+
#[Get(uri: '/home')]
18+
public function __invoke(): View
19+
{
20+
return view('home.view.php');
21+
}
22+
}
23+
```
24+
25+
:::tip
26+
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.
27+
:::
28+
29+
## Discovery in production
30+
31+
Discovery comes with performance considerations. In production, it is always cached to avoid scanning files on every request.
32+
33+
To ensure that the discovery cache is up-to-date, add the `discovery:generate` command before any other Tempest command in your deployment pipeline.
34+
35+
```console ">_ ./tempest discovery:generate --no-interaction"
36+
Clearing discovery cache <dim>.....................................</dim> <strong>2025-12-30 15:51:46</strong>
37+
Clearing discovery cache <dim>.....................................</dim> <strong>DONE</strong>
38+
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>2025-12-30 15:51:46</strong>
39+
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>DONE</strong>
40+
```
41+
42+
## Discovery for local development
43+
44+
During development, discovery is only enabled for application code. This implies that the cache should be regenerated whenever a package is installed or updated.
45+
46+
It is recommended to add the `discovery:generate` command to the `post-package-update` script in `composer.json`:
47+
48+
```json composer.json
49+
{
50+
"scripts": {
51+
"post-package-update": [
52+
"@php tempest discovery:generate"
53+
]
54+
}
55+
}
56+
```
57+
58+
### Disabling discovery cache
59+
60+
In some situations, you may want to enable discovery even for vendor code. For instance, if you are working on a third-party package that is being developed alongside your application, you may want to have discovery enabled all the time.
61+
62+
To achieve this, set the `DISCOVERY_CACHE` environment variable to `false`:
63+
64+
```env .env
65+
{:hl-property:DISCOVERY_CACHE:}={:hl-keyword:false:}
66+
```
67+
68+
### Troubleshooting
69+
70+
The `discovery:clear` command clears the discovery cache, which will be rebuilt the next time the framework boots. `discovery:generate` can be used to manually regenerate the cache.
71+
72+
If the discovery cache gets corrupted and even `discovery:clear` is not enough, the `.tempest/cache/discovery` may be manually deleted from your project.
73+
74+
## Implementing your own discovery
75+
76+
While Tempest provides a variety of [built-in discovery classes](#built-in-discovery-classes), you may want to implement your own to extend the framework's capabilities in your application or in a package you are building.
77+
78+
### Discovering code in classes
79+
80+
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.
81+
82+
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()`.
83+
84+
As an example, the following is a simplified version of the event bus discovery:
85+
86+
```php EventBusDiscovery.php
87+
use Tempest\Discovery\Discovery;
88+
use Tempest\Discovery\IsDiscovery;
89+
90+
final readonly class EventBusDiscovery implements Discovery
91+
{
92+
// This provides the default implementation for `Discovery`'s internals
93+
use IsDiscovery;
94+
95+
public function __construct(
96+
// Discovery classes are autowired,
97+
// so you can inject all dependencies you need
98+
private EventBusConfig $eventBusConfig,
99+
) {
100+
}
101+
102+
public function discover(DiscoveryLocation $location, ClassReflector $class): void
103+
{
104+
foreach ($class->getPublicMethods() as $method) {
105+
$eventHandler = $method->getAttribute(EventHandler::class);
106+
107+
// Extra checks to determine whether
108+
// we can actually use the current method as an event handler
109+
110+
// …
111+
112+
// Finally, we add all discovery-related data into `$this->discoveryItems`:
113+
$this->discoveryItems->add($location, [$eventName, $eventHandler, $method]);
114+
}
115+
}
116+
117+
// Next, the `apply` method is called whenever discovery is ready to be
118+
// applied into the framework. In this case, we want to loop over all
119+
// registered discovery items, and add them to the event bus config.
120+
public function apply(): void
121+
{
122+
foreach ($this->discoveryItems as [$eventName, $eventHandler, $method]) {
123+
$this->eventBusConfig->addClassMethodHandler(
124+
event: $eventName,
125+
handler: $eventHandler,
126+
reflectionMethod: $method,
127+
);
128+
}
129+
}
130+
}
131+
```
132+
133+
### Discovering files
134+
135+
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.
136+
137+
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.
138+
139+
The example below shows a simplified version of the Vite entrypoint discovery:
140+
141+
```php ViteDiscovery.php
142+
use Tempest\Discovery\Discovery;
143+
use Tempest\Discovery\DiscoversPath;
144+
use Tempest\Discovery\IsDiscovery;
145+
use Tempest\Support\Str;
146+
147+
final class ViteDiscovery implements Discovery, DiscoversPath
148+
{
149+
use IsDiscovery;
150+
151+
public function __construct(
152+
private readonly ViteConfig $viteConfig,
153+
) {}
154+
155+
// We are not discovering any class, so we return immediately.
156+
public function discover(DiscoveryLocation $location, ClassReflector $class): void
157+
{
158+
return;
159+
}
160+
161+
// This method is called for every file in registered discovery locations.
162+
// We can use the `$path` to determine whether we are interested in it.
163+
public function discoverPath(DiscoveryLocation $location, string $path): void
164+
{
165+
// We are insterested in `.ts`, `.css` and `.js` files only.
166+
if (! Str\ends_with($path, ['.ts', '.css', '.js'])) {
167+
return;
168+
}
169+
170+
// These files need to be specifically marked as `.entrypoint`.
171+
if (! str($path)->beforeLast('.')->endsWith('.entrypoint')) {
172+
return;
173+
}
174+
175+
$this->discoveryItems->add($location, [$path]);
176+
}
177+
178+
// When discovery is cached, `discover` and `discoverPath` are not called.
179+
// Instead, `discoveryItems` is already fed with serialized data, which
180+
// we can use. In this case, we add the paths to the Vite config.
181+
public function apply(): void
182+
{
183+
foreach ($this->discoveryItems as [$path]) {
184+
$this->viteConfig->addEntrypoint($path);
185+
}
186+
}
187+
}
188+
```
189+
190+
## Excluding files and classes from discovery
191+
192+
Files and classes may be excluded from discovery by providing a {b`Tempest\Core\DiscoveryConfig`} [configuration](./06-configuration.md) file.
193+
194+
```php src/discovery.config.php
195+
use Tempest\Core\DiscoveryConfig;
196+
197+
return new DiscoveryConfig()
198+
->skipClasses(GlobalHiddenDiscovery::class)
199+
->skipPaths(__DIR__ . '/../../Fixtures/GlobalHiddenPathDiscovery.php');
200+
```
201+
202+
## Built-in discovery classes
203+
204+
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.
205+
206+
- {b`Tempest\Core\DiscoveryDiscovery`} discovers other discovery classes. This class is run manually by the framework when booted.
207+
- {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).
208+
- {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).
209+
- {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).
210+
- {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).
211+
- {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).
212+
- {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).
213+
- {b`Tempest\Router\RouteDiscovery`} discovers route attributes on methods and registers them as [controller actions](./01-routing.md) in the router.
214+
- {b`Tempest\Mapper\MapperDiscovery`} discovers classes that implement {b`Tempest\Mapper\Mapper`} and registers them for [mapping](../2-features/01-mapper.md#mapper-discovery).
215+
- {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).
216+
- {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).
217+
- {b`Tempest\View\ViewComponentDiscovery`} discovers `x-*.view.php` files and registers them as [view components](../1-essentials/02-views.md#view-components).
218+
- {b`Tempest\Vite\ViteDiscovery`} discovers `*.entrypoint.{ts,js,css}` files and register them as [entrypoints](../2-features/02-asset-bundling.md#entrypoints).
219+
- {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).
220+
- {b`Tempest\Core\InsightsProviderDiscovery`} discovers classes that implement {b`Tempest\Core\InsightsProvider`} and registers them as insights providers, which power the `tempest about` command.

0 commit comments

Comments
 (0)