diff --git a/docs/0-getting-started/02-installation.md b/docs/0-getting-started/02-installation.md index fc0955a65c..2cdece922c 100644 --- a/docs/0-getting-started/02-installation.md +++ b/docs/0-getting-started/02-installation.md @@ -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: @@ -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; @@ -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). +::: diff --git a/docs/1-essentials/03-database.md b/docs/1-essentials/03-database.md index a61c6d3992..1a1bd9d243 100644 --- a/docs/1-essentials/03-database.md +++ b/docs/1-essentials/03-database.md @@ -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 diff --git a/docs/1-essentials/04-console-commands.md b/docs/1-essentials/04-console-commands.md index 31254e37ee..7ecf4c7b5e 100644 --- a/docs/1-essentials/04-console-commands.md +++ b/docs/1-essentials/04-console-commands.md @@ -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. @@ -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). @@ -331,4 +331,4 @@ $this->console ->assertSee('caution') ->submit() ->assertSuccess(); -``` \ No newline at end of file +``` diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index f87734ea8c..d4d459b934 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -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 diff --git a/docs/1-essentials/05-discovery.md b/docs/1-essentials/05-discovery.md new file mode 100644 index 0000000000..7e4c5f6896 --- /dev/null +++ b/docs/1-essentials/05-discovery.md @@ -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 ..................................... 2025-12-30 15:51:46 +Clearing discovery cache ..................................... DONE +Generating discovery cache using the `full` strategy ......... 2025-12-30 15:51:46 +Generating discovery cache using the `full` strategy ......... DONE +``` + +## 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'); +``` diff --git a/docs/1-essentials/06-configuration.md b/docs/1-essentials/06-configuration.md index 6d7cf10d75..e51ed7a73b 100644 --- a/docs/1-essentials/06-configuration.md +++ b/docs/1-essentials/06-configuration.md @@ -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; diff --git a/docs/2-features/11-localization.md b/docs/2-features/11-localization.md index 3ff24b2bc5..a377a30ce7 100644 --- a/docs/2-features/11-localization.md +++ b/docs/2-features/11-localization.md @@ -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 `..{yaml,json}` naming format, where `` may be any string, and `` 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 `..{yaml,json}` naming format, where `` may be any string, and `` 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: diff --git a/docs/2-features/11-scheduling.md b/docs/2-features/11-scheduling.md index 3fb63d0fc7..1ad9b4c34d 100644 --- a/docs/2-features/11-scheduling.md +++ b/docs/2-features/11-scheduling.md @@ -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 diff --git a/docs/2-features/14-exception-handling.md b/docs/2-features/14-exception-handling.md index e4bcd2ed94..43859d4532 100644 --- a/docs/2-features/14-exception-handling.md +++ b/docs/2-features/14-exception-handling.md @@ -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 diff --git a/docs/3-packages/02-console.md b/docs/3-packages/02-console.md index d7d3a7a7a6..cfc1b4897c 100644 --- a/docs/3-packages/02-console.md +++ b/docs/3-packages/02-console.md @@ -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). diff --git a/docs/4-internals/01-bootstrap.md b/docs/4-internals/01-bootstrap.md deleted file mode 100644 index 7d48810907..0000000000 --- a/docs/4-internals/01-bootstrap.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Framework bootstrap -description: "Learn the steps involved in bootstrapping the framework." ---- - -## Overview - -Here's a short summary of what booting Tempest looks like. - -- The entry point is either `public/index.php` or `./tempest`. -- Tempest boots using the {b`\Tempest\Core\FrameworkKernel`}. -- Bootstrap classes are located in the [`Tempest\Core\Kernel`](https://github.com/tempestphp/tempest-framework/tree/main/packages/core/src/Kernel) namespace. -- First, discovery is started through the {b`\Tempest\Core\LoadDiscoveryLocations`} and {b`\Tempest\Core\LoadDiscoveryClasses`} classes. -- Then, configuration files are registered through the {b`\Tempest\Core\LoadConfig`} class. -- When bootstrapping is completed, the `Tempest\Core\KernelEvent::BOOTED` event is fired. diff --git a/docs/4-internals/01-lifecycle.md b/docs/4-internals/01-lifecycle.md new file mode 100644 index 0000000000..999689a9d9 --- /dev/null +++ b/docs/4-internals/01-lifecycle.md @@ -0,0 +1,25 @@ +--- +title: Framework lifecycle +description: "Learn the steps involved in booting, running and shutting down the framework." +--- + +## Booting + +Tempest's entry point is usually `public/index.php` or `./tempest`. The former uses {b`Tempest\Router\HttpApplication`}, the latter {b`Tempest\Console\ConsoleApplication`}. + +When created, the application boots by creating the {b`\Tempest\Core\FrameworkKernel`}: + +- it loads the environment, the exception handler, and configures the container, +- it then starts discovery through the {b`\Tempest\Core\LoadDiscoveryLocations`} and {b`\Tempest\Core\LoadDiscoveryClasses`} classes, +- and finally registers configuration files through the {b`\Tempest\Core\LoadConfig`} class. + +When bootstrapping is completed, the `Tempest\Core\KernelEvent::BOOTED` event is fired. + +## Shutdown + +The shutdown process is managed by the kernel's `shutdown()` method, which is called at the end of both HTTP and console lifecycles, as well as in exception handlers. This method: + +- runs deferred tasks, +- dispatches the `KernelEvent::SHUTDOWN` event, +- performs any necessary cleanup, +- and terminates the application process. diff --git a/docs/4-internals/02-discovery.md b/docs/4-internals/02-discovery.md deleted file mode 100644 index d5225d2ef8..0000000000 --- a/docs/4-internals/02-discovery.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: Discovery -description: "Learn how Tempest automatically locates controller actions, event handlers, console commands, and other components of your application." ---- - -:::info -Read the [getting started with discovery](/blog/discovery-explained) guide if you are new to Tempest. -::: - -## 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. 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 implement {b`\Tempest\Container\Initializer`} or {b`\Tempest\Container\DynamicInitializer`} and registers them in the container. -- {`\Tempest\Database\MigrationDiscovery`}
- Discovers classes that implement {`\Tempest\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 implement {`\Tempest\Mapper\Mapper`}, which are registered in `\Tempest\Mapper\ObjectFactory` -- {`\Tempest\View\ViewComponentDiscovery`}
- Discovers classes that implement {`\Tempest\View\ViewComponent`}, as well as view files that contain `{html}` or named `x-*.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 {b`Tempest\Core\DiscoveryLocation`} and a {b`Tempest\Reflection\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: - -```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 - -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: - -```php -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: - -```env .env -{:hl-property:DISCOVERY_CACHE:}={:hl-keyword: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. - -```console -./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: - -```env .env -{:hl-property:DISCOVERY_CACHE:}={:hl-keyword: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: - -```json -{ - "scripts": { - "post-package-update": [ - "php tempest discovery:generate" - ] - } -} -``` - -:::info -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](../3-internals/02-discovery) to learn more. -::: - -## 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'); -``` diff --git a/docs/4-internals/03-view-spec.md b/docs/4-internals/02-view-spec.md similarity index 100% rename from docs/4-internals/03-view-spec.md rename to docs/4-internals/02-view-spec.md diff --git a/docs/5-extra-topics/01-package-development.md b/docs/5-extra-topics/01-package-development.md index 911c3a1e3d..75258ea47c 100644 --- a/docs/5-extra-topics/01-package-development.md +++ b/docs/5-extra-topics/01-package-development.md @@ -5,9 +5,9 @@ description: "Tempest comes with a handful of tools to help third-party package ## Overview -Creating a package for Tempest is as simple as adding `tempest/core` as a dependency. When this happens, [discovery](../4-internals/02-discovery.md) will find the package thanks to composer metadata and register discoverable classes. +Creating a package for Tempest is as simple as adding `tempest/core` as a dependency. When this happens, [discovery](../1-essentials/05-discovery.md) will find the package thanks to composer metadata and register discoverable classes. -Unlike Symfony or Laravel, Tempest doesn't have a dedicated "service provider" concept. Instead, you're encouraged to rely on [discovery](../4-internals/02-discovery.md) and [initializers](../1-essentials/05-container#dependency-initializers). +Unlike Symfony or Laravel, Tempest doesn't have a dedicated "service provider" concept. Instead, you're encouraged to rely on [discovery](../1-essentials/05-discovery.md) and [initializers](../1-essentials/05-container#dependency-initializers). ## Preventing discovery