Як працює discovery у Tempest: пояснення головної концепції фреймворку

Перекладено ШІ 0 stitcher.io 30 липня, 2026

Концепція discovery у Tempest дозволяє фреймворку самостійно розпізнавати логіку вашого коду без зайвих файлів конфігурації. Розповідаємо, як цей підхід змінює архітектуру застосунків та чи не шкодить він їхній швидкодії.

Цей матеріал було вперше опубліковано в блозі Tempest.

В основі Tempest лежить концепція під назвою «discovery». Це саме та фішка, що вирізняє його серед інших фреймворків. Поки Symfony та Laravel використовують обмежені можливості автоматичного виявлення для зручності, у Tempest discovery є фундаментом, на якому будується все інше. У цьому дописі я поясню, як працює discovery, у чому його сила та як створити власну реалізацію.

Як працює discovery

Ідея проста: фреймворк має розуміти ваш код самостійно, щоб ви не переймалися конфігурацією чи bootstrapping. Коли ми кажемо, що Tempest — це «фреймворк, який не заважає вам працювати», ми маємо на увазі саме discovery.

Розглянемо приклад екшну контролера:

use Tempest\Router\Get;
use Tempest\View\View;

final class BookController
{
    #[Get('/books')]
    public function index(): View
    { /* … */ }
}

Ви можете розмістити цей файл у будь-якій папці проєкту. Tempest розпізнає його як контролер і зареєструє маршрут у роутері. Сама по собі ця можливість не є унікальною — Symfony робить щось подібне. Але погляньмо на інші приклади.

Обробники подій позначаються атрибутом #[EventHandler], а конкретна подія визначається типом аргументу:

use Tempest\EventBus\EventHandler;

final class BooksEventHandlers
{
    #[EventHandler]
    public function onBookCreated(BookCreated $event): void
    {
        // …
    }
}

Консольні команди знаходяться завдяки атрибуту #[ConsoleCommand]. Визначення команди в консолі генерується автоматично на основі методу:

use Tempest\Console\ConsoleCommand;

final readonly class BooksCommand
{
    #[ConsoleCommand]
    public function list(): void
    {
        // ./tempest books:list
    }

    #[ConsoleCommand]
    public function info(string $name): void
    {
        // ./tempest books:info "Timeline Taxi"
    }
}

View-компоненти розпізнаються за назвою файлу:

<!-- x-button.view.php -->

<a :if="isset($href)" class="button" :href="$href">
    <x-slot/>
</a>

<div :else class="button">
    <x-slot/>
</div>

Таких прикладів багато. Чим же discovery у Tempest відрізняється від автоматичного пошуку файлів у Symfony чи Laravel? Є два ключові моменти:

  1. Discovery у Tempest працює всюди. Вам не потрібно налаштовувати конкретні папки для сканування. Tempest перевіряє весь проєкт, включно з директорією vendor (про це згодом).
  2. Discovery спроєктований як розширюваний механізм. Якщо вашому проєкту чи пакету потрібно автоматично знаходити нові типи елементів — ви просто створюєте один клас, і все працює.

Ці характеристики роблять Tempest надзвичайно гнучким. Ви можете створювати будь-яку структуру проєкту, не підлаштовуючись під вимоги фреймворку — і це саме те, що користувачі найбільше цінують у Tempest.

Процес discovery складається з трьох етапів:

  1. Спочатку Tempest аналізує залежності composer: до сканування потрапляють усі namespace проєкту та пакети, що потребують Tempest.
  2. Визначивши локації, Tempest шукає класи, що реалізують інтерфейс Discovery. Так, самі класи discovery також знаходяться через цей механізм.
  3. Наостанок Tempest перебирає знайдені класи discovery та передає їм локації для сканування. Кожен такий клас має доступ до контейнера та реєструє в ньому необхідні дані.

Розглянемо реалізацію RouteDiscovery з коментарями, що пояснюють логіку.

use Tempest\Discovery\Discovery;
use Tempest\Discovery\DiscoveryLocation;
use Tempest\Discovery\IsDiscovery;
use Tempest\Reflection\ClassReflector;

final class RouteDiscovery implements Discovery
{
    use IsDiscovery;

    // Залежності впроваджуються через autowiring
    public function __construct(
        private readonly RouteConfigurator $configurator,
        private readonly RouteConfig $routeConfig,
    ) {
    }

    // Метод discover викликається для кожного знайденого класу
    public function discover(DiscoveryLocation $location, ClassReflector $class): void
    {
        // Шукаємо публічні методи з атрибутом Route
        foreach ($class->getPublicMethods() as $method) {
            $routeAttributes = $method->getAttributes(Route::class);

            foreach ($routeAttributes as $routeAttribute) {
                // Зберігаємо знайдені елементи для подальшого застосування
                $this->discoveryItems->add($location, [$method, $routeAttribute]);
            }
        }
    }

    // Метод apply реєструє маршрути в RouteConfig
    // Розподіл discover та apply необхідний для кешування
    public function apply(): void
    {
        foreach ($this->discoveryItems as [$method, $routeAttribute]) {
            $route = DiscoveredRoute::fromRoute($routeAttribute, $method);
            $this->configurator->addRoute($route);
        }

        if ($this->configurator->isDirty()) {
            $this->routeConfig->apply($this->configurator->toRouteConfig());
        }
    }
}

Як бачите, усе досить просто. Ось ще один приклад — кастомна реалізація для сайту документації, яка знаходить усі класи, що реалізують інтерфейс Projector:

use Tempest\Discovery\Discovery;
use Tempest\Discovery\DiscoveryLocation;
use Tempest\Discovery\IsDiscovery;
use Tempest\Reflection\ClassReflector;

final class ProjectionDiscovery implements Discovery
{
    use IsDiscovery;

    public function __construct(
        private readonly StoredEventConfig $config,
    ) {}

    public function discover(DiscoveryLocation $location, ClassReflector $class): void
    {
        if ($class->implements(Projector::class)) {
            $this->discoveryItems->add($location, $class->getName());
        }
    }

    public function apply(): void
    {
        foreach ($this->discoveryItems as $className) {
            $this->config->projectors[] = $className;
        }
    }
}

Кешування та продуктивність

«Зачекайте, це ж не може бути швидко!» — це була моя перша думка, коли Ейдан (один із розробників Tempest) запропонував сканувати усі файли проєкту та папки vendor.

Ейдан запевнив, що все працюватиме, і він мав рацію. Але є кілька нюансів.

По-перше, у production сканування коду не відбувається. Саме тому методи discover() та apply() розділені: discover() лише готує дані, а результат його роботи кешується.

По-друге, залишається питання розробки на локальній машині, де кешувати файли незручно через постійні правки. Проте, хоча файли проєкту змінюються часто, файли vendor оновлюються лише під час composer up. Тому Tempest використовує «частковий кеш discovery» (partial discovery cache): vendor-файли кешуються, а проєктні — ні. Режими перемикаються в оточенні:

# .env

DISCOVERY_CACHE=false
DISCOVERY_CACHE=true
DISCOVERY_CACHE=partial

Якщо ви використовуєте повний або частковий кеш, після деплою або оновлення залежностей його потрібно регенерувати:

~ ./tempest discovery:generate

  │ Clearing discovery cache
  │ ✔ Done in 132ms.

  │ Generating discovery cache using the all strategy
  │ ✔ Done in 411ms.

У шаблоні проєкту tempest/app уже налаштовані composer-хуки, але ви можете додати їх вручну:

{
  "scripts": {
    "post-package-update": [
      "@php ./tempest discovery:generate"
    ]
  }
}

Ми проводили бенчмарки discovery без кешу на тисячах файлів, щоб імітувати реальний великий проєкт (код тестів доступний тут). Вплив на продуктивність під час розробки виявився мізерним.

Ми плануємо ще більше оптимізувати цей процес — наприклад, через сканування лише змінених файлів на основі статусу git. Але ми не хочемо впроваджувати передчасні оптимізації до ретельного тестування. Якщо ви виявите проблеми зі швидкістю — будь ласка, створіть issue.

Discovery — це серцебиття Tempest. Завдяки йому ми позбулися більшості конфігураційних файлів, адже фреймворк розуміє код і приймає рішення самостійно. Це дає вам свободу структурувати проєкт як завгодно: Tempest не диктує, де мають лежати ваші моделі чи контролери.

Робіть так, як зручно вам, а Tempest у всьому розбереться. Бо це фреймворк, який справді не стає на заваді.

Популярні

Інше, що варто прочитати

12 Оновлено 25 червня, 2025

Отримання параметрів команди в Laravel Artisan

Laravel спрощує доступ до аргументів та опцій у ваших кастомних командах Artisan, дозволяючи легко отримувати та валідувати параметри. Дізнайтеся, як ці вбудовані допоміжні методи можуть покращити ваш процес розробки!

102 Оновлено 26 червня, 2026

Все, що потрібно знати про Laravel 13

Laravel 13 вийде в березні 2026 року й вимагатиме мінімум PHP 8.3. Хочете дізнатися, як PHP‑атрибути для моделей, нові налаштування черг і метод Cache::touch() вплинуть на вашу розробку?

12 Оновлено 26 червня, 2026

Управління доступом у Filament за допомогою плагіна Shield

Дізнайтеся, як пакет Filament Shield забезпечує управління доступом до ваших панелей, ресурсів і віджетів у Laravel. Ця стаття розкриває основні можливості пакету, включаючи просту установку та підтримку багатокористувацьких середовищ — не пропустіть!