Skip to content

Commit d0d642c

Browse files
committed
feat(core): enable partial discovery by default during development
1 parent f36b43b commit d0d642c

File tree

7 files changed

+170
-75
lines changed

7 files changed

+170
-75
lines changed

docs/1-essentials/05-discovery.md

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ description: "Tempest automatically locates controller actions, event handlers,
55

66
## Overview
77

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

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. Tempest applies various rules to determine the purpose of different pieces of code. It can analyze file names, attributes, interfaces, return types, and more.
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.
1111

12-
For instance, web routes are discovered based on route attributes:
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:
1313

1414
```php app/HomeController.php
1515
final readonly class HomeController
@@ -22,33 +22,59 @@ final readonly class HomeController
2222
}
2323
```
2424

25-
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.
26-
27-
:::info
25+
:::tip
2826
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.
2927
:::
3028

31-
## Built-in discovery classes
29+
## Discovery in production
3230

33-
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.
31+
Discovery comes with performance considerations. In production, it is always cached to avoid scanning files on every request.
3432

35-
- {b`Tempest\Core\DiscoveryDiscovery`} discovers other discovery classes. This class is run manually by the framework when booted.
36-
- {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).
37-
- {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).
38-
- {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).
39-
- {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).
40-
- {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).
41-
- {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).
42-
- {b`Tempest\Router\RouteDiscovery`} discovers route attributes on methods and registers them as [controller actions](./01-routing.md) in the router.
43-
- {b`Tempest\Mapper\MapperDiscovery`} discovers classes that implement {b`Tempest\Mapper\Mapper`} and registers them for [mapping](../2-features/01-mapper.md#mapper-discovery).
44-
- {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).
45-
- {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).
46-
- {b`Tempest\View\ViewComponentDiscovery`} discovers `x-*.view.php` files and registers them as [view components](../1-essentials/02-views.md#view-components).
47-
- {b`Tempest\Vite\ViteDiscovery`} discovers `*.entrypoint.{ts,js,css}` files and register them as [entrypoints](../2-features/02-asset-bundling.md#entrypoints).
48-
- {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).
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.
4973

5074
## Implementing your own discovery
5175

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+
5278
### Discovering code in classes
5379

5480
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.
@@ -161,51 +187,34 @@ final class ViteDiscovery implements Discovery, DiscoversPath
161187
}
162188
```
163189

164-
## Discovery in production
165-
166-
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.
167-
168-
Caching is done by running the `discovery:generate` command, which should be part of your deployment pipeline before any other Tempest command.
169-
170-
```console ">_ ./tempest discovery:generate --no-interaction"
171-
Clearing discovery cache <dim>.....................................</dim> <strong>2025-12-30 15:51:46</strong>
172-
Clearing discovery cache <dim>.....................................</dim> <strong>DONE</strong>
173-
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>2025-12-30 15:51:46</strong>
174-
Generating discovery cache using the `full` strategy <dim>.........</dim> <strong>DONE</strong>
175-
```
176-
177-
## Discovery for local development
178-
179-
During development, discovery is enabled without a cache. Depending on the size of your project, you may benefit from enabling the partial cache strategy:
180-
181-
```env .env
182-
{:hl-property:DISCOVERY_CACHE:}={:hl-keyword:partial:}
183-
```
184-
185-
This strategy only caches discovery for vendor files. For this reason, it is recommended to run `discovery:generate` after every composer update:
186-
187-
```json composer.json
188-
{
189-
"scripts": {
190-
"post-package-update": [
191-
"php tempest discovery:generate"
192-
]
193-
}
194-
}
195-
```
196-
197-
:::info
198-
If your project was created using {`tempest/app`}, the `post-package-update` script is already included.
199-
:::
200-
201190
## Excluding files and classes from discovery
202191

203-
If needed, you can always exclude discovered files and classes by providing a discovery config file:
192+
Files and classes may be excluded from discovery by providing a {b`Tempest\Core\DiscoveryConfig`} [configuration](./06-configuration.md) file.
204193

205-
```php app/discovery.config.php
194+
```php src/discovery.config.php
206195
use Tempest\Core\DiscoveryConfig;
207196

208197
return new DiscoveryConfig()
209198
->skipClasses(GlobalHiddenDiscovery::class)
210199
->skipPaths(__DIR__ . '/../../Fixtures/GlobalHiddenPathDiscovery.php');
211200
```
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.

packages/core/src/Commands/DiscoveryGenerateCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737
)]
3838
public function __invoke(): void
3939
{
40-
$strategy = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: $this->environment->requiresCaution()));
40+
$strategy = DiscoveryCacheStrategy::resolveFromEnvironment();
4141

4242
if ($strategy === DiscoveryCacheStrategy::NONE) {
4343
$this->info('Discovery cache disabled, nothing to generate.');

packages/core/src/DiscoveryCache.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tempest\Discovery\Discovery;
1111
use Tempest\Discovery\DiscoveryItems;
1212
use Tempest\Discovery\DiscoveryLocation;
13+
use Tempest\Support\Filesystem;
1314
use Throwable;
1415

1516
use function Tempest\internal_storage_path;
@@ -86,13 +87,10 @@ public function clear(): void
8687

8788
public function storeStrategy(DiscoveryCacheStrategy $strategy): void
8889
{
89-
$dir = dirname(self::getCurrentDiscoverStrategyCachePath());
90+
$path = self::getCurrentDiscoverStrategyCachePath();
9091

91-
if (! is_dir($dir)) {
92-
mkdir($dir, recursive: true);
93-
}
94-
95-
file_put_contents(self::getCurrentDiscoverStrategyCachePath(), $strategy->value);
92+
Filesystem\create_directory_for_file($path);
93+
Filesystem\write_file($path, $strategy->value);
9694
}
9795

9896
public static function getCurrentDiscoverStrategyCachePath(): string

packages/core/src/DiscoveryCacheInitializer.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Tempest\Container\Container;
66
use Tempest\Container\Initializer;
77
use Tempest\Container\Singleton;
8+
use Tempest\Support\Filesystem;
89

910
use function Tempest\env;
1011

@@ -24,13 +25,18 @@ private function resolveDiscoveryCacheStrategy(): DiscoveryCacheStrategy
2425
return DiscoveryCacheStrategy::NONE;
2526
}
2627

27-
$current = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: Environment::guessFromEnvironment()->requiresCaution()));
28+
$current = DiscoveryCacheStrategy::resolveFromEnvironment();
2829

2930
if ($current === DiscoveryCacheStrategy::NONE) {
3031
return $current;
3132
}
3233

33-
$original = DiscoveryCacheStrategy::make(@file_get_contents(DiscoveryCache::getCurrentDiscoverStrategyCachePath()));
34+
$path = DiscoveryCache::getCurrentDiscoverStrategyCachePath();
35+
$stored = Filesystem\exists($path)
36+
? Filesystem\read_file($path)
37+
: null;
38+
39+
$original = DiscoveryCacheStrategy::resolveFromInput($stored);
3440

3541
if ($current !== $original) {
3642
return DiscoveryCacheStrategy::INVALID;

packages/core/src/DiscoveryCacheStrategy.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,42 @@
44

55
namespace Tempest\Core;
66

7+
use function Tempest\env;
8+
79
enum DiscoveryCacheStrategy: string
810
{
11+
/**
12+
* Discovery is completely cached and will not be re-run.
13+
*/
914
case FULL = 'full';
15+
16+
/**
17+
* Vendors are cached, application discovery is re-run.
18+
*/
1019
case PARTIAL = 'partial';
20+
21+
/**
22+
* Discovery is not cached.
23+
*/
1124
case NONE = 'none';
25+
26+
/**
27+
* There is a mismatch between the stored strategy and the resolved strategy, discovery is considered as not cached.
28+
*/
1229
case INVALID = 'invalid';
1330

14-
public static function make(mixed $input): self
31+
public static function resolveFromEnvironment(): self
32+
{
33+
$environment = Environment::guessFromEnvironment();
34+
35+
return static::resolveFromInput(env('DISCOVERY_CACHE', default: match (true) {
36+
$environment->requiresCaution() => true,
37+
$environment->isLocal() => 'partial',
38+
default => false,
39+
}));
40+
}
41+
42+
public static function resolveFromInput(mixed $input): self
1543
{
1644
return match ($input) {
1745
true, 'true', '1', 1, 'all', 'full' => self::FULL,

packages/core/src/FrameworkKernel.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,8 @@ public function registerEmergencyExceptionHandler(): self
240240

241241
public function registerExceptionHandler(): self
242242
{
243-
$environment = $this->container->get(Environment::class);
244-
245243
// During tests, PHPUnit registers its own error handling.
246-
if ($environment->isTesting()) {
244+
if (Environment::guessFromEnvironment()->isTesting()) {
247245
return $this;
248246
}
249247

tests/Integration/Core/DiscoveryCacheTest.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace Tests\Tempest\Integration\Core;
44

5+
use PHPUnit\Framework\Attributes\PostCondition;
6+
use PHPUnit\Framework\Attributes\Test;
57
use Tempest\Core\CouldNotStoreDiscoveryCache;
68
use Tempest\Core\DiscoveryCache;
9+
use Tempest\Core\DiscoveryCacheStrategy;
710
use Tempest\Discovery\DiscoveryLocation;
811
use Tests\Tempest\Integration\Core\Fixtures\TestDiscovery;
912
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
@@ -12,7 +15,15 @@
1215

1316
final class DiscoveryCacheTest extends FrameworkIntegrationTestCase
1417
{
15-
public function test_exception_with_unserializable_discovery_items(): void
18+
#[PostCondition]
19+
protected function cleanup(): void
20+
{
21+
putenv('ENVIRONMENT=testing');
22+
putenv('DISCOVERY_CACHE=true');
23+
}
24+
25+
#[Test]
26+
public function exception_with_unserializable_discovery_items(): void
1627
{
1728
$this->assertException(CouldNotStoreDiscoveryCache::class, function (): void {
1829
$discoveryCache = $this->container->get(DiscoveryCache::class);
@@ -26,4 +37,49 @@ public function test_exception_with_unserializable_discovery_items(): void
2637
]);
2738
});
2839
}
40+
41+
#[Test]
42+
public function partial_locally(): void
43+
{
44+
putenv('ENVIRONMENT=local');
45+
putenv('DISCOVERY_CACHE=null');
46+
47+
$this->assertSame(DiscoveryCacheStrategy::PARTIAL, DiscoveryCacheStrategy::resolveFromEnvironment());
48+
}
49+
50+
#[Test]
51+
public function overridable_locally(): void
52+
{
53+
putenv('ENVIRONMENT=local');
54+
putenv('DISCOVERY_CACHE=false');
55+
56+
$this->assertSame(DiscoveryCacheStrategy::NONE, DiscoveryCacheStrategy::resolveFromEnvironment());
57+
}
58+
59+
#[Test]
60+
public function enabled_in_production(): void
61+
{
62+
putenv('ENVIRONMENT=production');
63+
putenv('DISCOVERY_CACHE=null');
64+
65+
$this->assertSame(DiscoveryCacheStrategy::FULL, DiscoveryCacheStrategy::resolveFromEnvironment());
66+
}
67+
68+
#[Test]
69+
public function enabled_in_staging(): void
70+
{
71+
putenv('ENVIRONMENT=staging');
72+
putenv('DISCOVERY_CACHE=null');
73+
74+
$this->assertSame(DiscoveryCacheStrategy::FULL, DiscoveryCacheStrategy::resolveFromEnvironment());
75+
}
76+
77+
#[Test]
78+
public function overridable_in_production(): void
79+
{
80+
putenv('ENVIRONMENT=production');
81+
putenv('DISCOVERY_CACHE=partial');
82+
83+
$this->assertSame(DiscoveryCacheStrategy::PARTIAL, DiscoveryCacheStrategy::resolveFromEnvironment());
84+
}
2985
}

0 commit comments

Comments
 (0)