Tempest Framework Documentation

repository·3.x·Indexed 24 days ago

https://github.com/tempestphp/tempest-framework

A community-driven, modern PHP framework featuring a 'zero config, zero overhead' approach. Tempest leverages PHP attributes for routing, CLI commands, and event handling, utilizing a discovery mechanism to eliminate boilerplate and manual configuration. It supports flexible project structures, extensible core components, and modern PHP features like property hooks and proxy objects.

Tokens
93.5K
Snippets
305
Records
454
Agent score
80%

What's inside Tempest

  1. Overview of Tempest design philosophy

    3.x
    Tempest is a modern PHP framework designed to minimize framework-related boilerplate, allowing developers to focus on application logic. It follows a "zero config, zero overhead" philosophy, utilizing PHP attributes for routing, command definition, and event handling.
  2. Understand Tempest's core philosophy and features

    3.x

    Tempest is a modern PHP framework designed to be lightweight, powerful, and unobtrusive. It aims to provide a 'sweet spot' between the robustness of Symfony and the eloquence of Laravel by embracing modern PHP features and staying close to vanilla PHP.

    Key characteristics include:

    • Modern PHP First: Uses features like property hooks, attributes, and proxy objects.
    • Unopinionated Structure: Does not force a specific project structure (MVC, DDD, Hexagonal, etc.); it works with your existing organization out of the box.
    • Discovery-Driven: Uses a 'Discovery' mechanism to automatically handle routing, console commands, view components, event listeners, and more without manual configuration.
    • Extensible: Almost any part of the framework can be replaced by implementing an interface and registering it in the container.
  3. What is Discovery in Tempest

    3.x

    Discovery is Tempest's mechanism for automatically locating application components like controller actions, event handlers, and console commands without manual registration. It scans the codebase and uses composer metadata to include both your application code and package dependencies. Tempest determines the purpose of code by analyzing file names, attributes, interfaces, and return types.

    final readonly class HomeController
    {
        #[Get(uri: '/home')]
        public function __invoke(): View
        {
            return view('home.view.php');
        }
    }
  4. Prevent classes from being discovered by Tempest

    3.x

    You can prevent Tempest from automatically discovering specific classes using two methods:

    1. Attribute-based: Mark a class with the #[Tempest\Discovery\SkipDiscovery] attribute. This is useful for classes you want to use internally or publish via an installer, but don't want Tempest to automatically register.
    2. Composer-based: Use the extra.tempest.ignore array in composer.json to exclude specific file paths. This is recommended when dealing with optional dependencies to prevent Reflection errors when those dependencies are missing.
    use Tempest\Discovery\SkipDiscovery;
    
    #[SkipDiscovery]
    final readonly class UserMigration implements Migration
    {
        // …
    }
    {
    	"extra": {
    		"tempest": {
    			"ignore": [
    				"src/OptionalDependency.php"
    			]
    		}
    	}
    }
  5. How configuration works in Tempest

    3.x
    In Tempest, configuration is represented by objects rather than arrays. This approach provides superior developer experience through static analysis and autocompletion in code editors. Configuration objects are registered as singletons in the container, meaning once they are instantiated, the same instance is used throughout the application lifecycle. You can overwrite default framework configurations (like switching from SQLite to PostgreSQL) by providing your own configuration files.
  6. Define events as data classes or enumerations

    3.x

    Events in Tempest are used to decouple components. You can define them in two ways:

    1. Data Classes: Best for events that need to carry specific payload information. It is recommended to use final readonly classes with no logic.
    2. Enumerations: Best for simple state changes where no extra data is required. This is highly recommended for a better developer experience.

    Events can be any scalar value, but classes and enums are the standard.

    // Data class approach
    final readonly class AircraftRegistered
    {
        public function __construct(
            public string $registration,
        ) {}
    }
    
    // Enumeration approach
    enum AircraftLifecycle
    {
        case REGISTERED;
        case RETIRED;
    }
  7. Routing and Controller patterns in Tempest

    3.x

    Tempest uses PHP attributes to define routes directly on controller methods. This allows for clean, declarative routing without separate configuration files.

    Example of a controller using #[Get] and #[Post] attributes:

    final class BookController
    {
        #[Get('/books/{book}')]
        public function show(Book $book): Response
        {
            return new Ok($book);
        }
    
        #[Post('/books')]
        public function store(CreateBookRequest $request): Response
        {
            $book = map($request)->to(Book::class)->save();
    
            return new Redirect([self::class, 'show'], book: $book->id);
        }
    
        // …
    }
    final class BookController
    {
        #[Get('/books/{book}')]
        public function show(Book $book): Response
        {
            return new Ok($book);
        }
    
        #[Post('/books')]
        public function store(CreateBookRequest $request): Response
        {
            $book = map($request)->to(Book::class)->save();
    
            return new Redirect([self::class, 'show'], book: $book->id);
        }
    }
  8. Override validation translation messages

    3.x

    Tempest uses MessageFormat 2.0 for localization. You can override default validation messages by providing translation files (e.g., validation.en.yml).

    To use a specific translation key for a property instead of the default rule-based key, apply the #[\Tempest\Validation\TranslationKey] attribute to the property. This changes the lookup path from validation_error.{rule} to validation_error.{rule}.{your_key}.

    final class Book {
        #[Rules\HasLength(min: 5, max: 50)]
        #[TranslationKey('book_management.book_title')]
        public string $title;
    }
    
    // The validator will look for the key:
    // validation_error.has_length.book_management.book_title
  9. Manage asset entrypoints

    3.x

    Tempest supports two ways to define entrypoints:

    1. Automatic Discovery: Any file ending with .entrypoint.{ts,css,js} is automatically discovered and bundled. Example: app/main.entrypoint.ts

    2. Manual Configuration: If you want to use different naming conventions, create a app/vite.config.php file that returns a Tempest\Vite\ViteConfig instance and define your entrypoints in the entrypoints array. Paths must be relative to the project root.

    Note: If you manually include a specific entrypoint in a view using <x-vite-tags entrypoint="path/to/file" />, you must also ensure that file is configured as an entrypoint in your Vite configuration for it to be included in the production manifest.

    return new ViteConfig(
        entrypoints: [
            'app/main.css',
            'app/main.ts',
        ],
    );
    <x-base>
    	<slot name="head">
    		<x-vite-tags entrypoint="src/Profile/profile.css" />
    	</slot>
    </x-base>
  10. Use Data Providers to generate multiple static pages

    3.x

    To generate multiple pages from a single controller action (e.g., a blog post or documentation chapter), assign a Data Provider to the #[Tempest\Router\StaticPage] attribute.

    The Data Provider must implement the \Tempest\Router\DataProvider interface and its provide() method must return a Generator. This generator should yield an array of parameters that correspond to the controller action's arguments for every page that needs to be generated.

    Note that dependencies (like repositories) can be injected into the Data Provider's constructor via the container, just like in a controller.

    // 1. Define the Data Provider
    use Tempest\Router\DataProvider;
    
    final readonly class ChapterDataProvider implements DataProvider
    {
        public function __construct(
            private ChapterRepository $chapters
        ) {}
    
        public function provide(): Generator
        {
            foreach ($this->chapters->all() as $chapter) {
                yield [
                    'category' => $chapter->category,
                    'slug' => $chapter->slug,
                ];
            }
        }
    }
    
    // 2. Use it in the Controller
    use Tempest\Router\Get;
    use Tempest\Router\StaticPage;
    use Tempest\View\View;
    
    final readonly class ChapterController
    {
        #[StaticPage(ChapterDataProvider::class)]
        #[Get('/{category}/{slug}')]
        public function show(string $category, string $slug, ChapterRepository $chapters): View
        {
            return new ChapterView(
                repository: $chapters,
                current: $chapters->find($category, $slug),
            );
        }
    }
  11. Automatic fallthrough of HTML attributes

    3.x

    When {html}class, {html}style, or {html}id attributes are provided to a view component, Tempest automatically attempts to apply them to the root node within that component.

    Example: If x-button.view.php contains:

    <button class="rounded-md px-2.5 py-1.5 text-sm">
    	<x-slot />
    </button>

    And you use it like this:

    <x-button id="myBtn" style="color: red;" />

    It renders as:

    <button id="myBtn" style="color: red;" class="rounded-md px-2.5 py-1.5 text-sm" />

    Disabling automatic fallthrough: To prevent automatic application, explicitly define the attributes within the component using expression attributes. For example, using :id and :class inside the component will stop Tempest from overwriting or automatically applying the passed {html}id or {html}class.

    <button :id="uniqid(($id ?? 'mybtn') . '_')" :class="$class ?? 'rounded-md px-2.5 py-1.5 text-sm'">
    	<x-slot />
    </button>