diff --git a/src/Markdown/Alerts/AlertBlockParser.php b/src/Markdown/Alerts/AlertBlockParser.php index 4d861ded..355e463e 100644 --- a/src/Markdown/Alerts/AlertBlockParser.php +++ b/src/Markdown/Alerts/AlertBlockParser.php @@ -10,8 +10,8 @@ final class AlertBlockParser implements BlockContinueParserInterface { - protected $block; - protected $finished = false; + protected AlertBlock $block; + protected bool $finished = false; public function __construct( protected string $alertType, @@ -20,25 +20,30 @@ public function __construct( $this->block = new AlertBlock($alertType, $title); } + #[\Override] public function addLine(string $line): void { } + #[\Override] public function getBlock(): AbstractBlock { return $this->block; } + #[\Override] public function isContainer(): bool { return true; } + #[\Override] public function canContain(AbstractBlock $childBlock): bool { return true; } + #[\Override] public function canHaveLazyContinuationLines(): bool { return false; @@ -49,6 +54,7 @@ public function parseInlines(): bool return true; } + #[\Override] public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isIndented()) { @@ -64,6 +70,7 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active return BlockContinue::at($cursor); } + #[\Override] public function closeBlock(): void { // Nothing to do here diff --git a/src/Markdown/Alerts/AlertBlockRenderer.php b/src/Markdown/Alerts/AlertBlockRenderer.php index d3f051b8..75d9d1a6 100644 --- a/src/Markdown/Alerts/AlertBlockRenderer.php +++ b/src/Markdown/Alerts/AlertBlockRenderer.php @@ -10,14 +10,15 @@ final class AlertBlockRenderer implements NodeRendererInterface { - public function render(Node $node, ChildNodeRendererInterface $htmlRenderer) + #[\Override] + public function render(Node $node, ChildNodeRendererInterface $childRenderer): mixed { if (! ($node instanceof AlertBlock)) { throw new \InvalidArgumentException('Incompatible node type: ' . get_class($node)); } - if (! ($htmlRenderer instanceof HtmlRenderer)) { - throw new \InvalidArgumentException('Incompatible renderer type: ' . get_class($htmlRenderer)); + if (! ($childRenderer instanceof HtmlRenderer)) { + throw new \InvalidArgumentException('Incompatible renderer type: ' . get_class($childRenderer)); } $cssClass = 'alert alert-' . $node->alertType; @@ -26,7 +27,7 @@ public function render(Node $node, ChildNodeRendererInterface $htmlRenderer) attributes: ['class' => 'alert-wrapper'], contents: [ $node->title ? new HtmlElement('span', attributes: ['class' => 'alert-title'], contents: $node->title) : null, - $htmlRenderer->renderNodes($node->children()), + $childRenderer->renderNodes($node->children()), ], ); diff --git a/src/Markdown/Alerts/AlertBlockStartParser.php b/src/Markdown/Alerts/AlertBlockStartParser.php index 7c11676e..6db33732 100644 --- a/src/Markdown/Alerts/AlertBlockStartParser.php +++ b/src/Markdown/Alerts/AlertBlockStartParser.php @@ -10,6 +10,7 @@ final class AlertBlockStartParser implements BlockStartParserInterface { + #[\Override] public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented()) { diff --git a/src/Markdown/Alerts/AlertExtension.php b/src/Markdown/Alerts/AlertExtension.php index 4561325d..88ba81fc 100644 --- a/src/Markdown/Alerts/AlertExtension.php +++ b/src/Markdown/Alerts/AlertExtension.php @@ -7,6 +7,7 @@ final class AlertExtension implements ExtensionInterface { + #[\Override] public function register(EnvironmentBuilderInterface $environment): void { $environment->addBlockStartParser(new AlertBlockStartParser()); diff --git a/src/Markdown/CodeBlockRenderer.php b/src/Markdown/CodeBlockRenderer.php index 8de57a2e..ffee5bda 100644 --- a/src/Markdown/CodeBlockRenderer.php +++ b/src/Markdown/CodeBlockRenderer.php @@ -19,7 +19,8 @@ public function __construct( ) { } - public function render(Node $node, ChildNodeRendererInterface $childRenderer) + #[\Override] + public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { if (! ($node instanceof FencedCode)) { throw new InvalidArgumentException('Block must be instance of ' . FencedCode::class); diff --git a/src/Markdown/FqcnParser.php b/src/Markdown/FqcnParser.php index f42eadba..09af73d1 100644 --- a/src/Markdown/FqcnParser.php +++ b/src/Markdown/FqcnParser.php @@ -14,11 +14,13 @@ final readonly class FqcnParser implements InlineParserInterface { + #[\Override] public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex("{`((?:\\\{1,2}\w+|\w+\\\{1,2})(?:\w+\\\{0,2})+)`}"); } + #[\Override] public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); diff --git a/src/Markdown/HandleParser.php b/src/Markdown/HandleParser.php index 5c3ceb66..ecdf132f 100644 --- a/src/Markdown/HandleParser.php +++ b/src/Markdown/HandleParser.php @@ -11,11 +11,13 @@ final readonly class HandleParser implements InlineParserInterface { + #[\Override] public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('{(twitter|x|bluesky|bsky|gh|github):(.+?)(?:,(.+))?}'); } + #[\Override] public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); diff --git a/src/Markdown/TempestPackageParser.php b/src/Markdown/TempestPackageParser.php index ed00e7b6..8359b357 100644 --- a/src/Markdown/TempestPackageParser.php +++ b/src/Markdown/TempestPackageParser.php @@ -12,11 +12,13 @@ final readonly class TempestPackageParser implements InlineParserInterface { + #[\Override] public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex("{`tempest\\/([\w-]+)`}"); } + #[\Override] public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); diff --git a/src/Web/Documentation/ChapterController.php b/src/Web/Documentation/ChapterController.php index 45ea7d7c..e267e322 100644 --- a/src/Web/Documentation/ChapterController.php +++ b/src/Web/Documentation/ChapterController.php @@ -4,8 +4,6 @@ namespace App\Web\Documentation; -use Stringable; -use Tempest\Router\Exceptions\NotFoundException; use Tempest\Router\Get; use Tempest\Router\Response; use Tempest\Router\Responses\NotFound; diff --git a/src/Web/Documentation/ChapterRepository.php b/src/Web/Documentation/ChapterRepository.php index 547989b1..7c6a323b 100644 --- a/src/Web/Documentation/ChapterRepository.php +++ b/src/Web/Documentation/ChapterRepository.php @@ -10,6 +10,7 @@ use Tempest\Support\Arr\ImmutableArray; use function Tempest\Support\arr; +use function Tempest\Support\Arr\get_by_key; use function Tempest\Support\Regex\replace; use function Tempest\Support\str; @@ -39,7 +40,9 @@ public function find(Version $version, string $category, string $slug): ?Chapter throw new \RuntimeException(sprintf('Documentation entry [%s] is missing a frontmatter.', $path)); } - ['title' => $title, 'description' => $description] = $markdown->getFrontMatter() + ['description' => null]; + $frontmatter = $markdown->getFrontMatter(); + $title = get_by_key($frontmatter, 'title'); + $description = get_by_key($frontmatter, 'description'); return new Chapter( version: $version, diff --git a/src/Web/Documentation/ChapterView.php b/src/Web/Documentation/ChapterView.php index 24edcecb..0c0715d1 100644 --- a/src/Web/Documentation/ChapterView.php +++ b/src/Web/Documentation/ChapterView.php @@ -35,12 +35,44 @@ public function isCurrent(Chapter $other): bool public function getSubChapters(): array { - preg_match_all('/.*.*?)".*<\/span>(?.*)<\/a><\/h2>/', $this->currentChapter->body, $matches); + // TODO: clean up + preg_match_all('/<h2.*>.*<a.*href="(?<uri>.*?)".*<\/span>(?<title>.*)<\/a><\/h2>/', $this->currentChapter->body, $h2Matches); + preg_match_all('/<h3.*>.*<a.*href="(?<h3uri>.*?)".*<\/span>(?<h3title>.*)<\/a><\/h3>/', $this->currentChapter->body, $h3Matches); $subChapters = []; - foreach ($matches[0] as $key => $match) { - $subChapters[$matches['uri'][$key]] = $matches['title'][$key]; + foreach ($h2Matches[0] as $key => $match) { + $h2Uri = $h2Matches['uri'][$key]; + $h2Title = $h2Matches['title'][$key]; + $subChapters[$h2Uri] = [ + 'title' => $h2Title, + 'children' => [], + ]; + } + + foreach ($h3Matches[0] as $key => $match) { + $h3Uri = $h3Matches['h3uri'][$key]; + $h3Title = $h3Matches['h3title'][$key]; + $parentH2Uri = null; + $h3Position = strpos($this->currentChapter->body, $match); + + foreach ($h2Matches[0] as $h2Key => $h2Match) { + $h2Position = strpos($this->currentChapter->body, $h2Match); + if ($h2Position < $h3Position) { + $parentH2Uri = $h2Matches['uri'][$h2Key]; + } else { + break; + } + } + + if ($parentH2Uri !== null && isset($subChapters[$parentH2Uri])) { + $subChapters[$parentH2Uri]['children'][$h3Uri] = $h3Title; + } else { + $subChapters[$h3Uri] = [ + 'title' => $h3Title, + 'children' => [], + ]; + } } return $subChapters; diff --git a/src/Web/Documentation/DocumentationIndexer.php b/src/Web/Documentation/DocumentationIndexer.php index 85fdd6b6..e3f89d14 100644 --- a/src/Web/Documentation/DocumentationIndexer.php +++ b/src/Web/Documentation/DocumentationIndexer.php @@ -10,8 +10,10 @@ use League\CommonMark\Node\Inline\Text; use League\CommonMark\Node\Query; use Tempest\Support\Arr\ImmutableArray; +use Tempest\Support\Str\ImmutableString; use function Tempest\Support\arr; +use function Tempest\Support\Arr\get_by_key; use function Tempest\Support\Str\to_kebab_case; use function Tempest\Support\Str\to_sentence_case; use function Tempest\uri; @@ -34,18 +36,20 @@ public function __invoke(Version $version): ImmutableArray return arr(glob(__DIR__ . "/content/{$version->value}/*/*.md")) ->flatMap(function (string $path) use ($version) { $markdown = $this->markdown->convert(file_get_contents($path)); - preg_match('/(?<index>\d+-)?(?<slug>.*)\.md/', pathinfo($path, PATHINFO_BASENAME), $matches); if (! ($markdown instanceof RenderedContentWithFrontMatter)) { throw new \RuntimeException(sprintf('Documentation entry [%s] is missing a frontmatter.', $path)); } - ['title' => $title, 'category' => $category] = $markdown->getFrontMatter(); + $path = new ImmutableString($path); + $category = $path->beforeLast('/')->afterLast('/')->replaceRegex('/\d+-/', ''); + $chapter = $path->basename('.md')->replaceRegex('/\d+-/', ''); + $title = get_by_key($markdown->getFrontMatter(), 'title'); $main = new Command( type: Type::URI, title: $title, - uri: uri(ChapterController::class, version: $version, category: $category, slug: $matches['slug']), + uri: uri(ChapterController::class, version: $version, category: $category, slug: $chapter), hierarchy: [ 'Documentation', to_sentence_case($category), diff --git a/src/Web/Documentation/content/main/0-getting-started/00-introduction.md b/src/Web/Documentation/content/main/0-getting-started/00-introduction.md new file mode 100644 index 00000000..c096e9dc --- /dev/null +++ b/src/Web/Documentation/content/main/0-getting-started/00-introduction.md @@ -0,0 +1,94 @@ +--- +title: Introduction +description: "Tempest is a PHP framework designed to get out of your way. Its core philosophy is to enable developers to write as little framework-specific code as possible, so that they can focus on application code instead." +--- + +Tempest makes writing PHP applications pleasant thanks to carefully crafted quality-of-life features that feel like a natural extension of vanilla PHP. + +It embraces modern PHP syntax in its implementation of routing, ORM, console commands, messaging, logging, it takes inspiration from the best front-end frameworks for its templating engine syntax, and provides unique capabilities, such as [discovery](../3-internals/02-discovery), to improve developer experience. + +You may be interested in reading how it has an [unfair advantage](/blog/unfair-advantage) over other frameworks—but code says more than words, so here are a few examples of code written on top of Tempest: + +```php +use Tempest\Router\Get; +use Tempest\Router\Post; +use Tempest\Router\Response; +use Tempest\Router\Responses\Ok; +use Tempest\Router\Responses\Redirect; +use function Tempest\uri; + +final readonly 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(uri([self::class, 'show'], book: $book->id)); + } + + // … +} +``` + +The above snippet is an example of a controller controller. It features [attribute-based routes](../1-framework/03-controllers), mapping a request to a data object using the [mapper](../1-framework/11-mapper), [URL generation](../1-framework/03-controllers#generating-uris) and [dependency injection](../1-framework/02-the-container#autowired-dependencies). + +```php +use Tempest\Console\Console; +use Tempest\Console\ConsoleCommand; +use Tempest\Console\Middleware\ForceMiddleware; +use Tempest\Console\Middleware\CautionMiddleware; +use Tempest\EventBus\EventHandler; + +final readonly class MigrateUpCommand +{ + public function __construct( + private Console $console, + private MigrationManager $migrationManager, + ) {} + + #[ConsoleCommand( + name: 'migrate:up', + description: 'Run all new migrations', + middleware: [ForceMiddleware::class, CautionMiddleware::class], + )] + public function __invoke(bool $fresh = false): void + { + if ($fresh) { + $this->migrationManager->dropAll(); + $this->console->success("Database dropped."); + } + + $this->migrationManager->up(); + $this->console->success("Migrations applied."); + } + + #[EventHandler] + public function onTableDropped(TableDropped $event): void + { + $this->console->writeln("- Dropped {$event->name}"); + } + + #[EventHandler] + public function onMigrationMigrated(MigrationMigrated $migrationMigrated): void + { + $this->console->writeln("- {$migrationMigrated->name}"); + } +} +``` + +This is a [console command](../2-console/02-building-console-commands). Console commands can be defined in any class, as long as the `#[ConsoleCommand]` attribute is used on a method. Command arguments are defined as the method's arguments, effectively removing the need to learn some specific framework syntax. + +This example also shows how to [register events globally](../1-framework/07-events) using the `#[EventHandler]`. + +--- + +:::info Ready to give it a try? +Keep on reading and consider [**giving Tempest a star️ on GitHub**](https://github.com/tempestphp/tempest-framework). If you want to be part of the community, you can [**join our Discord server**](https://discord.gg/pPhpTGUMPQ), and if you feel like contributing, you can check out our [contributing guide](/docs/internals/contributing)! +::: diff --git a/src/Web/Documentation/content/main/0-getting-started/01-getting-started.md b/src/Web/Documentation/content/main/0-getting-started/01-getting-started.md new file mode 100644 index 00000000..f3d8b9ce --- /dev/null +++ b/src/Web/Documentation/content/main/0-getting-started/01-getting-started.md @@ -0,0 +1,188 @@ +--- +title: Getting started +description: Tempest can be installed as a standalone PHP project, as well as a package within existing projects. The framework modules can also be installed individually, including in projects built on other frameworks. +--- + +## Installation + +Tempest requires PHP [8.4+](https://www.php.net/downloads.php) and [Composer](https://getcomposer.org/) to be installed. Optionally, you may install either [Bun](https://bun.sh) or [Node](https://nodejs.org) if you chose to bundle front-end assets. + +For a better experience, it is recommended to have a complete development environment, such as [ServBay](https://www.servbay.com), [Herd](https://herd.laravel.com/docs), or [Valet](https://laravel.com/docs/valet). However, Tempest can serve applications using PHP's built-in server just fine. + +Once the prerequisites are installed, you can chose your installation method. Tempest can be a standalone application, or be added in an existing project—even one built on top of another framework. + +## Creating a Tempest application + +To get started with a new Tempest project, you may use {`tempest/app`} as the starting point. The `composer create-project` command will scaffold it for you: + +```sh +{:hl-keyword:composer:} create-project tempest/app {:hl-type:my-app:} --stability alpha +{:hl-keyword:cd:} {:hl-type:my-app:} +``` + +If you have a dedicated development environment, you may then access your application by opening `https://my-app.test` in your browser. Otherwise, you may use PHP's built-in server: + +```sh +{:hl-keyword:php:} tempest serve +{:hl-comment:PHP 8.3.3 Development Server (http://localhost:8000) started:} +``` + +### Scaffolding front-end assets + +Optionally, you may install a basic front-end scaffolding that includes [Vite](https://vite.dev/) and [Tailwind CSS](https://tailwindcss.com/). To do so, run the Vite installer and follow through the wizard: + +```sh +{:hl-keyword:php:} tempest install vite --tailwind +``` + +<!-- TODO: docs --> +The assets created by this wizard, `main.entrypoint.ts` and `main.entrypoint.css`, are automatically discovered by Tempest. You can serve them using the `<x-vite-tags />` component in your templates. + +You may then run the front-end development server, which will serve your assets on-the-fly: + +```bash +{:hl-keyword:npm:} run dev +``` + +## Tempest as a package + +If you already have a project, you can opt to install {`tempest/framework`} as a standalone package. You could do this in any project; it could already contain code, or it could be an empty project. + +```sh +{:hl-keyword:composer:} require tempest/framework:1.0-alpha.5 +``` + +Installing Tempest this way will give you access to the Tempest console, `./vendor/bin/tempest`. Optionally, you can choose to install Tempest's entry points in your project. To do so, you may run the framework installer: + +```txt +{:hl-keyword:./vendor/bin/tempest:} install framework +``` + +This installer will prompt you to install the following files into your project: + +- `public/index.php` — the web application entry point +- `tempest` – the console application entry point +- `.env.example` – a clean example of a `.env` file +- `.env` – the real environment file for your local installation + +You can choose which files you want to install, and you can always rerun the `install` command at a later point in time. + +## Project structure + +Tempest won't impose any file structure on you: one of its core features is that it will scan all project and package code for you, and will automatically discover any files the framework needs to know about. + +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](../3-internals/02-discovery), and is one of Tempest's most powerful features. +::: + +### Examples + +The following projects structures work the same way in Tempest, without requiring any specific configuration: + +```txt +app +├── Console +│ └── RssSyncCommand.php +├── Controllers +│ ├── BlogPostController.php +│ └── HomeController.php +└── Views + ├── blog.view.php + └── home.view.php +``` + +```txt +src +├── Blog +│ ├── BlogPostController.php +│ ├── RssSyncCommand.php +│ └── blog.view.php +└── Home + ├── HomeController.php + └── home.view.php +``` + +From Tempest's perspective, it's all the same. + +## About Discovery + +Discovery works by scanning your project code, and looking at each file and method individually to determine what that code does. For production application, Tempest will cache the discovery process, avoiding any performance overhead. + +As an example, Tempest is able to determine which methods are controller methods based on their route attributes: + +```php app/BlogPostController.php +use Tempest\Router\Get; +use Tempest\Router\Response; +use Tempest\View\View; + +final readonly class BlogPostController +{ + #[Get('/blog')] + public function index(): View + { /* … */ } + + #[Get('/blog/{post}')] + public function show(Post $post): Response + { /* … */ } +} +``` + +And likewise, it's able to detect console commands based on their console command attribute: + +```php src/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 + { /* … */ } +} +``` + +### Discovery in production + +While discovery is a really powerful feature, it also comes with some performance considerations. In production environments, you want 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 local development. 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. +::: diff --git a/src/Web/Documentation/content/main/1-essentials/01-container.md b/src/Web/Documentation/content/main/1-essentials/01-container.md new file mode 100644 index 00000000..78507e59 --- /dev/null +++ b/src/Web/Documentation/content/main/1-essentials/01-container.md @@ -0,0 +1,351 @@ +--- +title: Container +description: "Learn how and why Tempest's dependency container is the heart of the framework, most features being built upon it." +--- + +## Overview + +A dependency container is a system that manages the creation and resolution of objects within an application. Instead of manually instantiating dependencies, classes declare what they need, and the container provides them automatically. + +Tempest has a dependency container capable of resolving dependencies without any configuration. Most features are built upon this concept, from controllers to console commands, through event handlers and the command bus. + +```php src/Aircraft/AircraftService.php +use App\Aircraft\ExternalAircraftProvider; +use App\Aircraft\AircraftRepository; +use Tempest\Console\ConsoleCommand; + +final readonly class AircraftService +{ + public function __construct( + private ExternalAircraftProvider $externalAircraftProvider, + private AircraftRepository $repository, + ) {} + + #[ConsoleCommand] + public function synchronize(): void + { + // … + } +} +``` + +## Dependency initializers + +When you need fine-grained control over how a dependency is constructed instead of relying on Tempest's autowiring capabilities, you can use initializer classes. + +Initializers are classes that know how to construct a specific class or interface. Whenever that class or interface is requested from the container, Tempest will use its corresponding initializer to construct it. + +### Implementing an initializer + +Initializers are classes that implement the {`\Tempest\Container\Initializer`} interface. The `initialize` method receives the container as its only parameter, and returns an instanciated object. + +**Most importantly**, Tempest knows which object this initializer is tied to thanks to the return type of the `initialize` method, which needs to be typed. + +```php app/MarkdownInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; + +final readonly class MarkdownInitializer implements Initializer +{ + public function initialize(Container $container): MarkdownConverter + { + $environment = new Environment(); + $highlighter = new Highlighter(new CssTheme()); + + $highlighter + ->addLanguage(new TempestViewLanguage()) + ->addLanguage(new TempestConsoleWebLanguage()) + ->addLanguage(new ExtendedJsonLanguage()); + + $environment + ->addExtension(new CommonMarkCoreExtension()) + ->addExtension(new FrontMatterExtension()) + ->addRenderer(FencedCode::class, new CodeBlockRenderer($highlighter)) + ->addRenderer(Code::class, new InlineCodeBlockRenderer($highlighter)); + + return new MarkdownConverter($environment); + } +} +``` + +The above example is an initializer for a `MarkdownConverter` class. It will set up a markdown converter, configure its extensions, and finally return the object. Whenever `MarkdownConverter` is requested via the container, this initializer class will be used to construct it. + +### Matching multiple classes or interfaces + +The container may match several classes to a single initializer if it has a union return type. + +```php app/MarkdownInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; + +final readonly class MarkdownInitializer implements Initializer +{ + public function initialize(Container $container): MarkdownConverter|Markdown + { + // … + } +} +``` + +### Dynamically matching classes or interfaces + +While initializers are capable of resolving almost all situations, there are times where the return type of `initialize` is not enough and more flexibility is needed. + +Let's take use the concept of route model binding as an example. A controller might accept an instance of a model as its parameters: + +```php app/BookController.php +use Tempest\Router\Get; +use Tempest\Router\Response; + +final readonly class BookController +{ + #[Get('/books/{book}')] + public function show(Book $book): Response { /* … */ } +} +``` + +Since `$book` isn't a scalar value, Tempest will try to resolve `{php}Book` from the container whenever this controller action is invoked. This means we need an initializer that's able to match the `Book` model: + +```php app/BookInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; + +final class BookInitializer implements Initializer +{ + public function initialize(Container $container): Book + { + // … + } +} +``` + +While this approach works, it would be very inconvenient to create an initializer for every model class. Furthermore, we want route binding to be provided by the framework, so we need a more generic approach. + +In essence, we need a way of using this initializer whenever a class that implements `{php}Model` is requested. A dynamic initializer fulfils this need, allowing you to dynamically match class names instead of relying on the return type of `initialize`: + +```php app/RouteBindingInitializer.php +use Tempest\Container\Container; +use Tempest\Container\DynamicInitializer; + +final class RouteBindingInitializer implements DynamicInitializer +{ + public function canInitialize(string $className): bool + { + return is_a($className, Model::class, true); + } + + public function initialize(string $className, Container $container): object + { + // … + } +} +``` + +## Autowired dependencies + +When you need to assign a default implementation to an interface without any specific instantiation steps, creating an initializer class for a single line of code might feel excessive. + +```php app/AircraftServiceInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; + +final readonly class AircraftServiceInitializer implements Initializer +{ + public function initialize(Container $container): AircraftServiceInterface + { + return new AircraftService(); + } +} +``` + +For simple one-to-one mappings, you can skip the initializer class, instead using the `#[Autowire]` attribute on the default implementation. Tempest will discover this, and link that class to the interface it implements: + +```php app/AircraftService.php +use Tempest\Container\Autowire; + +#[Autowire] +final readonly class AircraftService implements AircraftServiceInterface +{ + // … +} +``` + +## Singletons + +If you need to register a class as a singleton in the container, you can use the `#[Singleton]` attribute. Any class can have this attribute: + +```php app/Services/AircraftService/Client.php +use Tempest\Container\Singleton; +use Tempest\HttpClient\HttpClient; + +#[Singleton] +final readonly class Client +{ + public function __construct( + private HttpClient $http, + ) {} + + public function fetch(Icao $icao): Aircraft + { + // … + } +} +``` + +Furthermore, an initializer method can be annotated as a `#[Singleton]`, meaning its return object will only ever be resolved once: + +```php app/MarkdownInitializer.php +use Tempest\Console\ConsoleCommand; +use Tempest\Container\Initializer; +use Tempest\Container\Singleton; + +final readonly class MarkdownInitializer implements Initializer +{ + #[Singleton] + public function initialize(Container $container): MarkdownConverter|Markdown + { + // … + } +} +``` + +### Tagged singletons + +In some cases, you want more control over singleton definitions. + +Let's say you want an instance of `{php}\Tempest\Highlight\Highlighter` that would be configured for web highlighting, and one that would be configured CLI highlighting. In this situation, you can differenciate them using the `tag` parameter of the `#[Singleton]` attribute: + +```php app/WebHighlighterInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; +use Tempest\Container\Singleton; + +final readonly class WebHighlighterInitializer implements Initializer +{ + #[Singleton(tag: 'web')] + public function initialize(Container $container): Highlighter + { + return new Highlighter(new CssTheme()); + } +} +``` + +Retrieving this specific instance from the container may be done by using the `{php}#[Tag]` attribute during autowiring: + +```php app/HttpExceptionHandler.php +use Tempest\Container\Tag; + +class HttpExceptionHandler implements ExceptionHandler +{ + public function __construct( + #[Tag('web')] + private Highlighter $highlighter, + ) {} +} +``` + +If you have a container instance, you may also get it directly using the `tag` argument: + +```php +$container->get(Highlighter::class, tag: 'cli'); +``` + +:::info +[This blog post](https://stitcher.io/blog/tagged-singletons), by {gh:brendt}, provides in-depth explanations about tagged singletons. +::: + +## Built-in types dependencies + +Besides being able to depend on objects, sometimes you'd want to depend on built-in types like `string`, `int` or more often `array`. It is possible to depend on these built-in types, but these cannot be autowired and must be initialized through a [tagged singleton](#tagged-singletons). + +For example if we want to group a specific set of validators together as a tagged collection, you can initialize them in a tagged singleton initializer like so: + +```php +// app/BookValidatorsInitializer.php + +use Tempest\Container\Container; +use Tempest\Container\Initializer; + +final readonly class BookValidatorsInitializer implements Initializer +{ + #[Singleton(tag: 'book-validators')] + public function initialize(Container $container): array + { + return [ + $container->get(HeaderValidator::class), + $container->get(BodyValidator::class), + $container->get(FooterValidator::class), + ]; + } +} +``` + +Now you can use this group of validators as a normal tagged value in your container: + +```php +// app/BookController.php + +use Tempest\Container\Tag; + +final readonly class BookController +{ + public function __constructor( + #[Tag('book-validators')] private readonly array $contentValidators, + ) { /* … */ } +} +``` + +## Injected properties + +While constructor injection is almost always the preferred way to go, Tempest also offers the ability to inject values straight into properties, without them being requested by the constructor. + +You may mark any property—public, protected, or private—with the `#[Inject]` attribute. Whenever a class instance is resolved via the container, its properties marked for injection will be provided the right value. + +```php Tempest/Console/src/HasConsole.php +use Tempest\Container\Inject; + +trait HasConsole +{ + #[Inject] + private Console $console; + + // … +} +``` + +Keep in mind that injected properties are a form of service location. While it's recommended to rely on constructor injection by default, injected properties may offer flexibility when using traits without having to claim the constructor within that trait. + +For example, without injected properties, the above example would have to define a constructor within the trait to inject the `Console` dependency: + +```php +trait HasConsole +{ + public function __construct( + private readonly Console $console, + ) {} + + // … +} +``` + +On its own, that isn't a problem, but it causes some usability issues when using this trait in classes that require other dependencies as well: + +```php +use Tempest\Console\HasConsole; + +class MyCommand +{ + use HasConsole; + + public function __construct( + private BlogPostRepository $repository, + + // The `HasConsole` trait breaks if you didn't remember to explicitly inject it here + private Console $console, + ) {} + + // … +} +``` + +For these edge cases, it's nicer to make the trait self-contained without having to rely on constructor injection. That's why injected properties are supported. diff --git a/src/Web/Documentation/content/main/1-framework/03-controllers.md b/src/Web/Documentation/content/main/1-essentials/02-routing.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/03-controllers.md rename to src/Web/Documentation/content/main/1-essentials/02-routing.md index 74fc8370..2aff6b2e 100644 --- a/src/Web/Documentation/content/main/1-framework/03-controllers.md +++ b/src/Web/Documentation/content/main/1-essentials/02-routing.md @@ -1,6 +1,5 @@ --- title: Controllers -category: framework --- Controllers are the core of any web app, they route an HTTP request through the necessary layers of code to finally return a response. diff --git a/src/Web/Documentation/content/main/1-framework/04-views.md b/src/Web/Documentation/content/main/1-essentials/03-views.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/04-views.md rename to src/Web/Documentation/content/main/1-essentials/03-views.md index 857cb9b9..d7a9a3e7 100644 --- a/src/Web/Documentation/content/main/1-framework/04-views.md +++ b/src/Web/Documentation/content/main/1-essentials/03-views.md @@ -1,6 +1,5 @@ --- title: Views -category: framework --- Tempest supports three templating engines: Tempest View, Twig, and Blade. Tempest View is a new templating engine, while Twig and Blade have widespread support because of Symfony and Laravel. Tempest View is the default templating engine when creating Tempest projects, but the end of this page discusses how to install and switch to Twig or Blade instead. diff --git a/src/Web/Documentation/content/main/1-essentials/04-assets-bundling.md b/src/Web/Documentation/content/main/1-essentials/04-assets-bundling.md new file mode 100644 index 00000000..bf06d2e3 --- /dev/null +++ b/src/Web/Documentation/content/main/1-essentials/04-assets-bundling.md @@ -0,0 +1,7 @@ +--- +title: Assets bundling +--- + +:::warning +This part of the documentation is in progress. +::: diff --git a/src/Web/Documentation/content/main/1-framework/05-models.md b/src/Web/Documentation/content/main/1-essentials/05-database.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/05-models.md rename to src/Web/Documentation/content/main/1-essentials/05-database.md index a49e8c12..0638c35d 100644 --- a/src/Web/Documentation/content/main/1-framework/05-models.md +++ b/src/Web/Documentation/content/main/1-essentials/05-database.md @@ -1,6 +1,5 @@ --- -title: Models -category: framework +title: Database --- In contrast to many popular ORMs, Tempest models aren't required to be tied to the database. A model's persisted data can be loaded from any kind of data source: an external API, JSON, Redis, XML, … In essence, a model is nothing more than a class with public typed properties and methods. Tempest will use a model class' type information to determine how data should be mapped between objects. @@ -187,7 +186,7 @@ interface Model // Find a specific model, optionally loading relations as well public static function get(Id $id, array $relations = []): ?self; - + // Create a model query with a number of field conditions public static function find(mixed ...$conditions): ModelQueryBuilder; @@ -245,13 +244,13 @@ use Tempest\Database\DatabaseModel; use Tempest\Database\IsDatabaseModel; class Book implements DatabaseModel -{ +{ use IsDatabaseModel; - + // … - + public DateTimeImmutable $publishedAt; - + #[Virtual] public DateTimeImmutable $saleExpiresAt { get => $this->publishedAt->add(new DateInterval('P5D')); diff --git a/src/Web/Documentation/content/main/1-essentials/06-configuration.md b/src/Web/Documentation/content/main/1-essentials/06-configuration.md new file mode 100644 index 00000000..dfcde146 --- /dev/null +++ b/src/Web/Documentation/content/main/1-essentials/06-configuration.md @@ -0,0 +1,37 @@ +--- +title: Configuration +--- + +## Configuration + +As mentioned, configuration is represented by objects in Tempest. Tempest provides many configuration classes for you, although the framework is designed to use them as little as possible. Whenever you need fine-grained control over part of the framework's config, you can overwrite Tempest's default config by creating one or more `*.config.php` files, anywhere in your project. Each `*.config.php` file should return one config object. + +```php +// app/database.config.php + +use Tempest\Database\DatabaseConfig; +use Tempest\Database\Connections\MySqlConnection; +use function Tempest\env; + +return new DatabaseConfig( + connection: new MySqlConnection( + host: env('DB_HOST'), + port: env('DB_PORT'), + username: env('DB_USERNAME'), + password: env('DB_PASSWORD'), + database: env('DB_DATABASE'), + ), +); +``` + +Project-level configuration files will be discovered automatically, and will overwrite Tempest's default config. In this example, the default `DatabaseConfig` object will be overwritten by your custom one, using MySQL instead of SQLite, and retrieving its credentials from environment variables. + +### Config Cache + +Config files are cached by Tempest, you can read more about caching in the [dedicated chapter](/docs/framework/caching). You can enable or disable config cache with the `{txt}{:hl-property:CONFIG_CACHE:}` environment variable. + +```env +{:hl-comment:# .env:} + +{:hl-property:CONFIG_CACHE:}={:hl-keyword:true:} +``` diff --git a/src/Web/Documentation/content/main/1-framework/17-testing.md b/src/Web/Documentation/content/main/1-essentials/07-testing.md similarity index 98% rename from src/Web/Documentation/content/main/1-framework/17-testing.md rename to src/Web/Documentation/content/main/1-essentials/07-testing.md index ea177ed3..b033e43a 100644 --- a/src/Web/Documentation/content/main/1-framework/17-testing.md +++ b/src/Web/Documentation/content/main/1-essentials/07-testing.md @@ -1,6 +1,5 @@ --- title: Testing -category: framework --- Testing is a crucial part of any application. Tempest uses [PHPUnit](https://phpunit.de/) as its testing framework. diff --git a/src/Web/Documentation/content/main/1-framework/01-getting-started.md b/src/Web/Documentation/content/main/1-framework/01-getting-started.md deleted file mode 100644 index 1bf474ae..00000000 --- a/src/Web/Documentation/content/main/1-framework/01-getting-started.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: Getting started -category: framework -description: "Tempest is a PHP framework that gets out of your way. Its design philosophy is that developers should write as little framework-related code as possible, so that they can focus on application code instead." ---- - -## Introduction - -Tempest embraces **modern PHP syntax** and covers a wide range of features: routing, MVC, ORM and database, rich console applications, events and commands, logging, a modern view engine, and unique capabilities such as [discovery](#project-structure) to improve developer experience. - -Tempest can be installed **as a standalone PHP project**, as well as **a package within existing projects**. The framework modules — like, for example, {`tempest/console`} or {`tempest/event-bus`} — can also be installed **individually**, including in projects built on other frameworks. - -Since code says more than words, here's a Tempest controller: - -```php -// app/BookController.php - -use Tempest\Router\Get; -use Tempest\Router\Post; -use Tempest\Router\Response; -use Tempest\Router\Responses\Ok; -use Tempest\Router\Responses\Redirect; -use function Tempest\uri; - -final readonly 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(uri([self::class, 'show'], book: $book->id)); - } - - // … -} -``` - -And here's a Tempest console command: - -```php -// app/MigrateUpCommand.php - -use Tempest\Console\Console; -use Tempest\Console\ConsoleCommand; -use Tempest\Console\Middleware\ForceMiddleware; -use Tempest\Console\Middleware\CautionMiddleware; -use Tempest\EventBus\EventHandler; - -final readonly class MigrateUpCommand -{ - public function __construct( - private Console $console, - private MigrationManager $migrationManager, - ) {} - - #[ConsoleCommand( - name: 'migrate:up', - description: 'Run all new migrations', - middleware: [ForceMiddleware::class, CautionMiddleware::class], - )] - public function __invoke(): void - { - $this->migrationManager->up(); - - $this->console->success("Everything migrated"); - } - - #[EventHandler] - public function onMigrationMigrated(MigrationMigrated $migrationMigrated): void - { - $this->console->writeln("- {$migrationMigrated->name}"); - } -} -``` - -Ready to give it a try? Keep on reading and consider [**giving Tempest a star️ on GitHub**](https://github.com/tempestphp/tempest-framework). If you want to be part of the community, you can [**join our Discord server**](https://discord.gg/pPhpTGUMPQ), and you can check out our [contributing guide](/docs/internals/contributing)! - -## Installation - -You can install Tempest in two ways: as a web app with a basic frontend bootstrap, or by requiring the framework as a package in any project you'd like — these can be projects built on top of other frameworks. - -### A standalone Tempest app - -If you want to start a new Tempest project, you can use {`tempest/app`} as the starting point. Use `composer create-project` to start: - -```bash -composer create-project tempest/app my-app --stability alpha -cd my-app -./tempest install vite --tailwind -``` - -Run NPM: - -```bash -npm run dev -``` - -You can access your app by using PHP's built-in server. - -```text -./tempest serve -PHP 8.3.3 Development Server (http://localhost:8000) started -``` - -### Tempest as a package - -If you don't need an app scaffold, you can opt to install `tempest/framework` as a standalone package. You could do this in any project; it could already contain code, or it could be an empty project. - -```txt -composer require tempest/framework:1.0-alpha.5 -``` - -Installing Tempest this way will give you access to the tempest console as a composer binary: - -```txt -./vendor/bin/tempest -``` - -Optionally, you can choose to install Tempest's entry points in your project: - -```txt -./vendor/bin/tempest install framework -``` - -Installing Tempest into a project means copying one or more of these files into that project: - -- `public/index.php` — the web application entry point -- `tempest` – the console application entry point -- `.env.example` – a clean example of a `.env` file -- `.env` – the real environment file for your local installation - -You can choose which files you want to install, and you can always rerun the `install` command at a later point in time. - -## Project structure - -Tempest won't impose any file structure on you: one of its core features is that it will scan all project and package code for you, and it will automatically discover any files the framework needs to know about. For example: 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 verbose configuration files. This concept is called **discovery**, and it's one of Tempest's most powerful features. - -For example, you can make a project that looks like this: - -```txt -app -├── Console -│   └── RssSyncCommand.php -├── Controllers -│   ├── BlogPostController.php -│   └── HomeController.php -└── Views - ├── blog.view.php - └── home.view.php -``` - -Or a project that looks like this: - -```txt -app -├── Blog -│   ├── BlogPostController.php -│   ├── RssSyncCommand.php -│   └── blog.view.php -└── Home - ├── HomeController.php - └── home.view.php -``` - -From Tempest's perspective, it's all the same. - -Discovery works by scanning your project code, and looking at each file and method individually to determine what that code does. For production apps, Tempest will cache the discovery process, so there's no performance overhead that comes with it. - -As an example, Tempest is able to determine which methods are controller methods based on their route attributes: - -```php -// app/BlogPostController.php - -use Tempest\Router\Get; -use Tempest\Router\Response; -use Tempest\View\View; - -final readonly class BlogPostController -{ - #[Get('/blog')] - public function index(): View - { /* … */ } - - #[Get('/blog/{post}')] - public function show(Post $post): Response - { /* … */ } -} -``` - -And likewise, it's able to detect console commands based on their console command attribute: - -```php -// src/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 - { /* … */ } -} -``` - -## Discovery in production - -While discovery is a really powerful feature, it also comes with some performance considerations. In production environments, you want to make sure that the discovery workflow is cached. That's done like this: - -```env -{:hl-comment:# .env:} -{:hl-property:DISCOVERY_CACHE:}={:hl-keyword:true:} -``` - -What's important though, is that the production discovery cache will also need to be pre-generated. You can do this by running the `discovery:generate` command: - -```console -~ ./tempest discovery:generate -<em>Clearing existing discovery cache…</em> -<success>Discovery cached has been cleared</success> -<em>Generating new discovery cache… (cache strategy used: all)</em> -<success>Done</success> 1114 items cached -``` - -In other words: it's best that you include the `discovery:generate` command in your deployment pipeline. Make sure to run it before you run any other Tempest commands. - -## Discovery for local development - -By default, the discovery cache will be disabled in local development. Depending on your local setup, it's likely that you won't run into noticeable slowdowns. However, for larger projects, you might benefit from enabling a _partial discovery cache_: - -```env -{:hl-comment:# .env:} -{:hl-property:DISCOVERY_CACHE:}={:hl-keyword:partial:} -``` - -This caching strategy will only cache discovery for vendor files. Keep in mind that you will also have to generate the discovery cache: - -```console -~ ./tempest discovery:generate -<em>Clearing existing discovery cache…</em> -<success>Discovery cached has been cleared</success> -<em>Generating new discovery cache… (cache strategy used: partial)</em> -<success>Done</success> 111 items cached -``` - -If you're using a partial discovery cache, it is recommended to automatically run `discovery:generate` after every composer update: - -```json -{:hl-comment:// …:} - -"scripts": { - "post-package-update": [ - "php tempest discovery:generate" - ] -} -``` - -Note that, if you've created your project using {`tempest/app`}, you'll have the `post-package-update` script already included. If you want to, you can read the [internal documentation about discovery](/docs/internals/02-discovery) to learn more. diff --git a/src/Web/Documentation/content/main/1-framework/02-the-container.md b/src/Web/Documentation/content/main/1-framework/02-the-container.md deleted file mode 100644 index 6bdf1efb..00000000 --- a/src/Web/Documentation/content/main/1-framework/02-the-container.md +++ /dev/null @@ -1,433 +0,0 @@ ---- -title: Container and configuration -category: framework ---- - -Tempest's dependency container is the heart of the framework. Anything you do framework related will be run through the container, meaning you'll have autowiring everywhere: from controllers to console commands, from event handlers to the command bus: - -```php -// app/Package.php - -use Tempest\Console\ConsoleCommand; -use Tempest\Console\Console; - -final readonly class Package -{ - public function __construct( - private Console $console, - ) {} - - #[ConsoleCommand] - public function all(): void - { - // … - } -} -``` - -On top of that, Tempest configuration files are PHP objects, meaning they'll be registered in the container as singletons as well: - -```php -// app/Package.php - -use Tempest\Console\Console; -use Tempest\Core\AppConfig; - -final readonly class Package -{ - public function __construct( - private Console $console, - private AppConfig $config, - ) {} -} -``` - -Tempest will automatically discover configuration files, please read the [Config](#config) section for more info. - -## Dependency initializers - -When you need fine-grained control over how a dependency is constructed instead of relying on Tempest's autowiring capabilities, you can use initializer classes. Initializers are classes that know how to construct a specific class or interface. Whenever that class or interface is requested from the container, Tempest will use the initializer class to construct it. - -Here's an example for a markdown initializer, this class will set up a markdown convertor, configure its extensions, and finally return the object that's resolved from the container. Whenever `{php}MarkdownConverter` is requested via the container, this initializer class will be used to construct it: - -```php -// app/MarkdownInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; - -final readonly class MarkdownInitializer implements Initializer -{ - public function initialize(Container $container): MarkdownConverter - { - $environment = new Environment(); - - $highlighter = new Highlighter(new CssTheme()); - - $highlighter - ->addLanguage(new TempestViewLanguage()) - ->addLanguage(new TempestConsoleWebLanguage()) - ->addLanguage(new ExtendedJsonLanguage()) - ; - - $environment - ->addExtension(new CommonMarkCoreExtension()) - ->addExtension(new FrontMatterExtension()) - ->addRenderer(FencedCode::class, new CodeBlockRenderer($highlighter)) - ->addRenderer(Code::class, new InlineCodeBlockRenderer($highlighter)) - ; - - return new MarkdownConverter($environment); - } -} -``` - -Note that initializers are discovered by Tempest. The only thing you need to do is have a class implement `{php}Initializer`, and configure the return type of the `{php}initialize()` method. It's the return type that's used by the container to determine which class or interface this initializer provides. - -Initializers can also return union types, meaning the container can match several classes to a single initializer: - -```php -// app/MarkdownInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; - -final readonly class MarkdownInitializer implements Initializer -{ - public function initialize(Container $container): MarkdownConverter|Markdown - { - // … - } -} -``` - -## Autowired dependencies - -Oftentimes, you want to link a default implementation to an interface. In these cases, it might feel like overhead to create an initializer class with one line of code: - -```php -// app/BlogRepositoryInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; - -final readonly class BlogRepositoryInitializer implements Initializer -{ - public function initialize(Container $container): BlogRepository - { - return new FileBlogRepository(); - } -} -``` - -For simple one-to-one mappings, you can skip the initializer, and use the `#[Autowire]` attribute instead. Add the attribute to the default implementation, and Tempest will link that class to whatever interface it implements: - -```php -// app/FileBlogRepository.php - -use Tempest\Container\Autowire; - -#[Autowire] -final readonly class FileBlogRepository implements BlogRepository -{ - // … -} -``` - -(Note: autowired classes can also be defined as singletons, keep on reading.) - -## Singletons - -If you need to register a class as a singleton in the container, you can use the `{php}#[Singleton]` attribute. Any class can have this attribute: - -```php -// app/Package.php - -use Tempest\Container\Singleton; -use Tempest\Console\Console; -use Tempest\Console\ConsoleCommand; - -#[Singleton] -final readonly class Package -{ - public function __construct( - private Console $console, - ) {} - - #[ConsoleCommand] - public function all(): void {} -} -``` - -Furthermore, an initializer method can be annotated as a singleton, meaning its return object will be registered as a singleton: - -```php -// app/MarkdownInitializer.php - -use Tempest\Console\ConsoleCommand; -use Tempest\Container\Initializer; -use Tempest\Container\Singleton; - -final readonly class MarkdownInitializer implements Initializer -{ - #[Singleton] - public function initialize(Container $container): MarkdownConverter|Markdown - { - // … - } -} -``` - -### Tagged singletons - -In some cases, you want more control over singleton definitions. Let's say you need two singleton variants of the same class. More concrete: you want an instance of `{php}\Tempest\Hihglight\Highlighter` that's configured for web highlighting, and one for CLI highlighting. This is where tagged singletons come in. The `{php}#[Singleton]` attribute can receive an optional `$tag` parameter, which is used to tag this specific singleton initializer: - -```php -// app/WebHighlighterInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; -use Tempest\Container\Singleton; - -final readonly class WebHighlighterInitializer implements Initializer -{ - #[Singleton(tag: 'web')] - public function initialize(Container $container): Highlighter - { - return new Highlighter(new CssTheme()); - } -} -``` - -Retrieving this specific instance from the container can be done by using the `{php}#[Tag]` attribute during autowiring: - -```php -// app/HttpExceptionHandler.php - -use Tempest\Container\Tag; - -class HttpExceptionHandler implements ExceptionHandler -{ - public function __construct( - #[Tag('web')] private Highlighter $highlighter, - ) {} -} -``` - -You can also get it from the container directly like so: - -```php -$container->get(Highlighter::class, tag: 'cli'); -``` - -You can read [this blog post](https://stitcher.io/blog/tagged-singletons) for a more in-depth explanation on tagged singletons. - -## Injected properties - -While constructor injection is almost always the preferred way to go, Tempest also offers the ability to inject values straight into properties, without them being requested by the constructor. You can mark any property — public, protected, or private — with the `#[Inject]` attribute. Whenever a class instance is resolved via the container, its properties marked for injection will be provided the right value. - -```php -// Tempest/Console/src/HasConsole.php - -use Tempest\Container\Inject; - -trait HasConsole -{ - #[Inject] - private Console $console; - - // … -} -``` - -Keep in mind that injected properties are _technically_ a form of service location because an injected property has is tightly coupled to the dependency container. While it's recommended to rely on constructor injection by default, injected properties _can_ offer flexibility when using traits without having to claim the constructor within that trait. - -For example, without injected properties, the above example would have to define a constructor within the trait to inject the `Console` dependency: - -```php -trait HasConsole -{ - public function __construct( - private readonly Console $console, - ) {} - - // … -} -``` - -On its own, that isn't a problem, but it causes some usability issues when using this trait in classes that require other dependencies as well: - -```php -use Tempest\Console\HasConsole; - -class MyCommand -{ - use HasConsole; - - public function __construct( - private BlogPostRepository $repository, - - // The `HasConsole` trait breaks if you didn't remember to explicitly inject it here - private Console $console, - ) {} - - // … -} -``` - -For these edge cases, it's nicer to make the trait self-contained without having to rely on constructor injection. That's why injected properties are supported. - -## Dynamic initializers - -Some edge cases require more flexibility to match a requested class to an initializer. Let's take the example of route model binding. Let's say you have a controller like this: - -```php -// app/BookController.php - -use Tempest\Router\Get; -use Tempest\Router\Response; - -final readonly class BookController -{ - #[Get('/books/{book}')] - public function show(Book $book): Response { /* … */ } -} -``` - -Since `$book` isn't a scalar value, Tempest will try to resolve `{php}Book` from the container whenever this controller action is invoked. This means we need an initializer that's able to match the `Book` model: - -```php -// app/BookInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; - -final class BookInitializer implements Initializer -{ - public function initialize(Container $container): Book - { - // … - } -} -``` - -While this approach works, it would be very inconvenient to create an initializer for every model class. Furthermore, we want route binding to be provided by the framework, so we need a more generic approach. In essence, we need a way of using this initializer whenever a class is requested the implements `{php}Model`. That's where `{php}DynamicInitializer` comes in: this interfaces allows you to do dynamic matching on class names, instead of simply using the return type of the `{php}initialize()` method: - -```php -// app/RouteBindingInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\DynamicInitializer; - -final class RouteBindingInitializer implements DynamicInitializer -{ - public function canInitialize(string $className): bool - { - return is_a($className, Model::class, true); - } - - public function initialize(string $className, Container $container): object - { - // … - } -} -``` - -While dynamic initializers aren't often required, they are useful to some edge cases like these. Another example of a dynamic initializer is the `{php}BladeInitializer`, which should only be used whenever the blade package is installed. It looks like this: - -```php -// tempest/view/src/Renderers/BladeInitializer.php - -use Tempest\Container\DynamicInitializer; -use Tempest\Container\Singleton; - -#[Singleton] -final readonly class BladeInitializer implements DynamicInitializer -{ - public function canInitialize(string $className): bool - { - if (! class_exists('\Jenssegers\Blade\Blade')) { - return false; - } - - return $className === Blade::class; - } - - // … -} -``` - -## Built-in types dependencies - -Besides being able to depend on objects, sometimes you'd want to depend on built-in types like `string`, `int` or more often `array`. It is possible to depend on these built-in types, but these cannot be autowired and must be initialized through a [tagged singleton](#tagged-singletons). - -For example if we want to group a specific set of validators together as a tagged collection, you can initialize them in a tagged singleton initializer like so: - -```php -// app/BookValidatorsInitializer.php - -use Tempest\Container\Container; -use Tempest\Container\Initializer; - -final readonly class BookValidatorsInitializer implements Initializer -{ - #[Singleton(tag: 'book-validators')] - public function initialize(Container $container): array - { - return [ - $container->get(HeaderValidator::class), - $container->get(BodyValidator::class), - $container->get(FooterValidator::class), - ]; - } -} -``` - -Now you can use this group of validators as a normal tagged value in your container: - -```php -// app/BookController.php - -use Tempest\Container\Tag; - -final readonly class BookController -{ - public function __constructor( - #[Tag('book-validators')] private readonly array $contentValidators, - ) { /* … */ } -} -``` - -## Config - -As mentioned, configuration is represented by objects in Tempest. Tempest provides many configuration classes for you, although the framework is designed to use them as little as possible. Whenever you need fine-grained control over part of the framework's config, you can overwrite Tempest's default config by creating one or more `*.config.php` files, anywhere in your project. Each `*.config.php` file should return one config object. - -```php -// app/database.config.php - -use Tempest\Database\DatabaseConfig; -use Tempest\Database\Connections\MySqlConnection; -use function Tempest\env; - -return new DatabaseConfig( - connection: new MySqlConnection( - host: env('DB_HOST'), - port: env('DB_PORT'), - username: env('DB_USERNAME'), - password: env('DB_PASSWORD'), - database: env('DB_DATABASE'), - ), -); -``` - -Project-level configuration files will be discovered automatically, and will overwrite Tempest's default config. In this example, the default `DatabaseConfig` object will be overwritten by your custom one, using MySQL instead of SQLite, and retrieving its credentials from environment variables. - -### Config Cache - -Config files are cached by Tempest, you can read more about caching in the [dedicated chapter](/docs/framework/caching). You can enable or disable config cache with the `{txt}{:hl-property:CONFIG_CACHE:}` environment variable. - -```env -{:hl-comment:# .env:} - -{:hl-property:CONFIG_CACHE:}={:hl-keyword:true:} -``` diff --git a/src/Web/Documentation/content/main/1-framework/11-mapper.md b/src/Web/Documentation/content/main/1-tempest-in-depth/01-mapper.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/11-mapper.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/01-mapper.md index 39dfc312..1dce701c 100644 --- a/src/Web/Documentation/content/main/1-framework/11-mapper.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/01-mapper.md @@ -1,6 +1,5 @@ --- title: Mapper -category: framework --- Tempest comes with a flexible mapper component that can be used to map all sorts of data to objects and back. The mapper is internally used to handle persistence between models and the database, map PSR objects to internal requests, map request data to objects, and more. diff --git a/src/Web/Documentation/content/main/1-framework/12-validation.md b/src/Web/Documentation/content/main/1-tempest-in-depth/02-validation.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/12-validation.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/02-validation.md index a91f818a..01a46b6a 100644 --- a/src/Web/Documentation/content/main/1-framework/12-validation.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/02-validation.md @@ -1,6 +1,5 @@ --- title: Validation -category: framework --- Validation with Tempest is done by taking an array of raw input data, and validating whether that array of data is valid against a class. While validation and [data mapping](/main/framework/validation) often work together, the two are separate components and can also be used separately. @@ -72,4 +71,4 @@ final class Book #[SkipValidation] public string $title; } -``` \ No newline at end of file +``` diff --git a/src/Web/Documentation/content/main/1-framework/13-auth.md b/src/Web/Documentation/content/main/1-tempest-in-depth/03-auth.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/13-auth.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/03-auth.md index 6a7def5a..a386f5f2 100644 --- a/src/Web/Documentation/content/main/1-framework/13-auth.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/03-auth.md @@ -1,6 +1,5 @@ --- title: Authentication and authorization -category: framework --- Logging in (authentication )and verifying whether a user is allowed to perform a specific action (authorization) are two crucial parts of any web application. Tempest comes with a built-in authenticator and authorizer, as well as a base `User` and `Permission` model (if you want to). diff --git a/src/Web/Documentation/content/main/1-framework/10-caching.md b/src/Web/Documentation/content/main/1-tempest-in-depth/04-caching.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/10-caching.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/04-caching.md index 321f7656..ead790f3 100644 --- a/src/Web/Documentation/content/main/1-framework/10-caching.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/04-caching.md @@ -1,6 +1,5 @@ --- title: Caching -category: framework --- Tempest comes with a simple wrapper around PSR-6, which means you can use all PSR-6 compliant cache pools. Tempest uses [Symfony's Cache Component](https://symfony.com/doc/current/components/cache.html) as a default implementation, so all of [Symfony's adapters](https://symfony.com/doc/current/components/cache.html#available-cache-adapters) are available out of the box. diff --git a/src/Web/Documentation/content/main/1-framework/07-events.md b/src/Web/Documentation/content/main/1-tempest-in-depth/07-events.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/07-events.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/07-events.md index ff978438..175023fe 100644 --- a/src/Web/Documentation/content/main/1-framework/07-events.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/07-events.md @@ -1,6 +1,5 @@ --- title: Event bus -category: framework description: "Tempest comes with a built-in event bus, which can be used to dispatch events throughout your application." --- diff --git a/src/Web/Documentation/content/main/1-framework/08-commands.md b/src/Web/Documentation/content/main/1-tempest-in-depth/08-commands.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/08-commands.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/08-commands.md index 4757f4fb..9912ae18 100644 --- a/src/Web/Documentation/content/main/1-framework/08-commands.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/08-commands.md @@ -1,6 +1,5 @@ --- title: Command bus -category: framework --- Tempest comes with a built-in command bus, which can be used to dispatch a command to its handler (synchronous or asynchronous). A command bus offers multiple advantages over a more direct approach to modelling processes: commands and their handlers can easily be tested in isolation, they are simple to serialize, and similar to the eventbus, the command bus also supports middleware. diff --git a/src/Web/Documentation/content/main/1-framework/09-logging.md b/src/Web/Documentation/content/main/1-tempest-in-depth/09-logging.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/09-logging.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/09-logging.md index 58ff660f..cb6a82ee 100644 --- a/src/Web/Documentation/content/main/1-framework/09-logging.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/09-logging.md @@ -1,6 +1,5 @@ --- title: Logging -category: framework --- Logging is an essential part of any developer's job. Whether it's for debugging or for production monitoring. Tempest has a powerful set of tools to help you access the relevant information you need. diff --git a/src/Web/Documentation/content/main/1-framework/14-static-pages.md b/src/Web/Documentation/content/main/1-tempest-in-depth/10-static-pages.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/14-static-pages.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/10-static-pages.md index 8d460858..537d8900 100644 --- a/src/Web/Documentation/content/main/1-framework/14-static-pages.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/10-static-pages.md @@ -1,6 +1,5 @@ --- title: Static pages -category: framework --- Tempest comes with a built-in static site generator. When a controller action is tagged with `#[StaticPage]`, it can be compiled by Tempest as a static HTML page. These pages can then directly be served via your webserver. diff --git a/src/Web/Documentation/content/main/1-framework/15-primitive-utilities.md b/src/Web/Documentation/content/main/1-tempest-in-depth/11-primitive-utilities.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/15-primitive-utilities.md rename to src/Web/Documentation/content/main/1-tempest-in-depth/11-primitive-utilities.md index 0a9a555c..7f87996e 100644 --- a/src/Web/Documentation/content/main/1-framework/15-primitive-utilities.md +++ b/src/Web/Documentation/content/main/1-tempest-in-depth/11-primitive-utilities.md @@ -1,6 +1,5 @@ --- title: Primitive utilities -category: framework --- Tempest comes with a handful of classes that improve working with primitive types such as strings and arrays. The most important feature is an object-oriented API around PHP's built-in primitive helper functions. Let's take a look at what's available. diff --git a/src/Web/Documentation/content/main/1-framework/06-console.md b/src/Web/Documentation/content/main/2-console/01-introduction.md similarity index 98% rename from src/Web/Documentation/content/main/1-framework/06-console.md rename to src/Web/Documentation/content/main/2-console/01-introduction.md index 5051d909..25de4702 100644 --- a/src/Web/Documentation/content/main/1-framework/06-console.md +++ b/src/Web/Documentation/content/main/2-console/01-introduction.md @@ -1,6 +1,5 @@ --- title: Console -category: framework --- Tempest's console component can be used both within the framework, and as a standalone package. Console commands are discovered automatically by Tempest, and the framework ships with a console application in your project's root. You can call it like so: diff --git a/src/Web/Documentation/content/main/2-console/02-building-console-commands.md b/src/Web/Documentation/content/main/2-console/02-building-console-commands.md index 06bb8e1f..4a6cb6cd 100644 --- a/src/Web/Documentation/content/main/2-console/02-building-console-commands.md +++ b/src/Web/Documentation/content/main/2-console/02-building-console-commands.md @@ -1,6 +1,5 @@ --- title: Building console commands -category: console --- Any method tagged with the `{php}#[ConsoleCommand]` attribute will be automatically discovered and be available within the console application. By default, you don't have to pass in any parameters to the `{php}#[ConsoleCommand]` attribute, since Tempest will use the class and method names to generate a command name: diff --git a/src/Web/Documentation/content/main/2-console/03-input-output.md b/src/Web/Documentation/content/main/2-console/03-input-output.md index 955fe169..1d1e9691 100644 --- a/src/Web/Documentation/content/main/2-console/03-input-output.md +++ b/src/Web/Documentation/content/main/2-console/03-input-output.md @@ -1,6 +1,5 @@ --- title: Input and output -category: console --- Every command can read and write from and to the terminal by injecting the `{php}Console` interface. You don't have to configure anything, Tempests takes care of injecting the right dependencies for you behind the scenes: diff --git a/src/Web/Documentation/content/main/2-console/04-components.md b/src/Web/Documentation/content/main/2-console/04-components.md index 493c636c..8ac5c14a 100644 --- a/src/Web/Documentation/content/main/2-console/04-components.md +++ b/src/Web/Documentation/content/main/2-console/04-components.md @@ -1,6 +1,5 @@ --- title: Components -category: console --- Console components are Tempest's way of providing interactivity via the console. There are a number diff --git a/src/Web/Documentation/content/main/2-console/05-scheduler.md b/src/Web/Documentation/content/main/2-console/05-scheduler.md index 2242ff1a..c365e24e 100644 --- a/src/Web/Documentation/content/main/2-console/05-scheduler.md +++ b/src/Web/Documentation/content/main/2-console/05-scheduler.md @@ -1,6 +1,5 @@ --- title: Scheduling -category: console --- `tempest/console` comes with a built-in scheduler to run commands repeatedly in the background. You can schedule console commands, as well as plain functions that aren't directly accessible via the console. diff --git a/src/Web/Documentation/content/main/2-console/06-middleware.md b/src/Web/Documentation/content/main/2-console/06-middleware.md index 78270eb9..02ab3399 100644 --- a/src/Web/Documentation/content/main/2-console/06-middleware.md +++ b/src/Web/Documentation/content/main/2-console/06-middleware.md @@ -1,6 +1,5 @@ --- title: Middleware -category: console --- `tempest/console` has support for middleware, a well known concept within the context of web applications, which also makes building a lot of console features easier. diff --git a/src/Web/Documentation/content/main/2-console/01-getting-started.md b/src/Web/Documentation/content/main/2-console/07-standalone.md similarity index 97% rename from src/Web/Documentation/content/main/2-console/01-getting-started.md rename to src/Web/Documentation/content/main/2-console/07-standalone.md index 54b3e652..888432fd 100644 --- a/src/Web/Documentation/content/main/2-console/01-getting-started.md +++ b/src/Web/Documentation/content/main/2-console/07-standalone.md @@ -1,6 +1,5 @@ --- -title: Getting started -category: console +title: Standalone --- `tempest/console` is a standalone package used to build console applications. diff --git a/src/Web/Documentation/content/main/4-internals/01-bootstrap.md b/src/Web/Documentation/content/main/3-internals/01-bootstrap.md similarity index 99% rename from src/Web/Documentation/content/main/4-internals/01-bootstrap.md rename to src/Web/Documentation/content/main/3-internals/01-bootstrap.md index 3df2ba74..ff820d14 100644 --- a/src/Web/Documentation/content/main/4-internals/01-bootstrap.md +++ b/src/Web/Documentation/content/main/3-internals/01-bootstrap.md @@ -1,6 +1,5 @@ --- title: Framework bootstrap -category: internals --- Here's a short summary of what booting Tempest looks like. diff --git a/src/Web/Documentation/content/main/4-internals/02-discovery.md b/src/Web/Documentation/content/main/3-internals/02-discovery.md similarity index 99% rename from src/Web/Documentation/content/main/4-internals/02-discovery.md rename to src/Web/Documentation/content/main/3-internals/02-discovery.md index 05ac79d9..230a0a3f 100644 --- a/src/Web/Documentation/content/main/4-internals/02-discovery.md +++ b/src/Web/Documentation/content/main/3-internals/02-discovery.md @@ -1,6 +1,5 @@ --- title: Discovery -category: internals --- Tempest has a unique design when it comes to bootstrapping an application, more specifically when it comes to loading framework related code. Instead of having to manually registering project code or packages, Tempest will scan your codebase and automatically detect code that should be loaded. This concept is called **Discovery**. diff --git a/src/Web/Documentation/content/main/4-internals/03-container.md b/src/Web/Documentation/content/main/3-internals/03-container.md similarity index 98% rename from src/Web/Documentation/content/main/4-internals/03-container.md rename to src/Web/Documentation/content/main/3-internals/03-container.md index 986ae4fe..792b95c0 100644 --- a/src/Web/Documentation/content/main/4-internals/03-container.md +++ b/src/Web/Documentation/content/main/3-internals/03-container.md @@ -1,6 +1,5 @@ --- title: The container -category: internals --- Here's a short summary of how the Tempest container works. diff --git a/src/Web/Documentation/content/main/4-internals/04-tempest-views.md b/src/Web/Documentation/content/main/3-internals/04-tempest-views.md similarity index 99% rename from src/Web/Documentation/content/main/4-internals/04-tempest-views.md rename to src/Web/Documentation/content/main/3-internals/04-tempest-views.md index b132e7b2..ef623e7b 100644 --- a/src/Web/Documentation/content/main/4-internals/04-tempest-views.md +++ b/src/Web/Documentation/content/main/3-internals/04-tempest-views.md @@ -1,6 +1,5 @@ --- title: Tempest views -category: internals --- This page describes the technical specification for Tempest views. diff --git a/src/Web/Documentation/content/main/4-internals/05-package-development.md b/src/Web/Documentation/content/main/3-internals/05-package-development.md similarity index 99% rename from src/Web/Documentation/content/main/4-internals/05-package-development.md rename to src/Web/Documentation/content/main/3-internals/05-package-development.md index c6d3cdea..decfa6d1 100644 --- a/src/Web/Documentation/content/main/4-internals/05-package-development.md +++ b/src/Web/Documentation/content/main/3-internals/05-package-development.md @@ -1,6 +1,5 @@ --- title: Package development -category: internals --- Tempest comes with a handful of tools to help third-party package developers. diff --git a/src/Web/Documentation/content/main/3-highlight/01-getting-started.md b/src/Web/Documentation/content/main/4-highlight/01-getting-started.md similarity index 99% rename from src/Web/Documentation/content/main/3-highlight/01-getting-started.md rename to src/Web/Documentation/content/main/4-highlight/01-getting-started.md index 19eea480..51079f2d 100644 --- a/src/Web/Documentation/content/main/3-highlight/01-getting-started.md +++ b/src/Web/Documentation/content/main/4-highlight/01-getting-started.md @@ -1,6 +1,5 @@ --- title: Getting started -category: highlight --- `tempest/highlight` is a package for server-side, high-performance, and flexible code highlighting. [**Give it a ⭐️ on GitHub**](https://github.com/tempestphp/highlight)! diff --git a/src/Web/Documentation/content/main/3-highlight/02-custom-language.md b/src/Web/Documentation/content/main/4-highlight/02-custom-language.md similarity index 99% rename from src/Web/Documentation/content/main/3-highlight/02-custom-language.md rename to src/Web/Documentation/content/main/4-highlight/02-custom-language.md index 5b815ea9..584c6b14 100644 --- a/src/Web/Documentation/content/main/3-highlight/02-custom-language.md +++ b/src/Web/Documentation/content/main/4-highlight/02-custom-language.md @@ -1,6 +1,5 @@ --- title: Building a custom language -category: highlight --- Let's explain how `tempest/highlight` works by implementing a new language — [Blade](https://laravel.com/docs/11.x/blade) is a good candidate. It looks something like this: diff --git a/src/Web/Documentation/content/main/3-highlight/03-adding-tokens.md b/src/Web/Documentation/content/main/4-highlight/03-adding-tokens.md similarity index 99% rename from src/Web/Documentation/content/main/3-highlight/03-adding-tokens.md rename to src/Web/Documentation/content/main/4-highlight/03-adding-tokens.md index 9b2dc776..91e03249 100644 --- a/src/Web/Documentation/content/main/3-highlight/03-adding-tokens.md +++ b/src/Web/Documentation/content/main/4-highlight/03-adding-tokens.md @@ -1,6 +1,5 @@ --- title: Adding tokens -category: highlight --- <style> diff --git a/src/Web/Documentation/content/main/3-highlight/04-optin.md b/src/Web/Documentation/content/main/4-highlight/04-optin.md similarity index 99% rename from src/Web/Documentation/content/main/3-highlight/04-optin.md rename to src/Web/Documentation/content/main/4-highlight/04-optin.md index 2dfc7a68..5a813081 100644 --- a/src/Web/Documentation/content/main/3-highlight/04-optin.md +++ b/src/Web/Documentation/content/main/4-highlight/04-optin.md @@ -1,6 +1,5 @@ --- title: "Opt-in features" -category: highlight --- `tempest/highlight` has a couple of opt-in features, if you need them. diff --git a/src/Web/Documentation/content/main/0-intro/roadmap.md b/src/Web/Documentation/content/main/5-extra-topics/00-roadmap.md similarity index 99% rename from src/Web/Documentation/content/main/0-intro/roadmap.md rename to src/Web/Documentation/content/main/5-extra-topics/00-roadmap.md index c56eb3cc..bf57dcdf 100644 --- a/src/Web/Documentation/content/main/0-intro/roadmap.md +++ b/src/Web/Documentation/content/main/5-extra-topics/00-roadmap.md @@ -1,6 +1,5 @@ --- title: Roadmap -category: intro --- Tempest is still a work in progress, and we're actively working towards a stable 1.0 release. We keep track of that progress on [GitHub](https://github.com/tempestphp/tempest-framework/milestones). diff --git a/src/Web/Documentation/content/main/4-internals/00-contributing.md b/src/Web/Documentation/content/main/5-extra-topics/01-contributing.md similarity index 99% rename from src/Web/Documentation/content/main/4-internals/00-contributing.md rename to src/Web/Documentation/content/main/5-extra-topics/01-contributing.md index 6a3d817c..4f85975e 100644 --- a/src/Web/Documentation/content/main/4-internals/00-contributing.md +++ b/src/Web/Documentation/content/main/5-extra-topics/01-contributing.md @@ -1,6 +1,5 @@ --- title: Contributing -category: internals --- Welcome aboard! We're excited that you are interested in contributing to the Tempest framework. We value all contributions to the project and have assembled the following resources to help you get started. Thanks for being a contributor! diff --git a/src/Web/Documentation/content/main/1-framework/16-standalone-components.md b/src/Web/Documentation/content/main/5-extra-topics/16-standalone-components.md similarity index 99% rename from src/Web/Documentation/content/main/1-framework/16-standalone-components.md rename to src/Web/Documentation/content/main/5-extra-topics/16-standalone-components.md index 3fe07694..697362b6 100644 --- a/src/Web/Documentation/content/main/1-framework/16-standalone-components.md +++ b/src/Web/Documentation/content/main/5-extra-topics/16-standalone-components.md @@ -1,6 +1,5 @@ --- title: Standalone components -category: framework --- Many Tempest components can be installed as standalone packages in existing or new projects: `tempest/console`, `tempest/http`, `tempest/event-bus`, `tempest/debug`, `tempest/command-bus`, etc. diff --git a/src/Web/Documentation/show.view.php b/src/Web/Documentation/show.view.php index f973a4a4..88842a7d 100644 --- a/src/Web/Documentation/show.view.php +++ b/src/Web/Documentation/show.view.php @@ -17,11 +17,14 @@ <!-- Chapter list --> <ul class="flex flex-col border-s border-(--ui-border)"> <li :foreach="$this->chaptersForCategory($category) as $chapter" class="-ms-px ps-1.5"> - <a :href="$chapter->getUri()" class="group relative w-full px-2.5 py-1.5 flex items-center gap-1.5 text-sm focus:outline-none focus-visible:outline-none hover:text-(--ui-text-highlighted) data-[state=open]:text-(--ui-text-highlighted) transition-colors <?= $this->isCurrent( - $chapter, - ) - ? 'text-(--ui-primary) after:absolute after:-left-1.5 after:inset-y-0.5 after:block after:w-px after:rounded-full after:transition-colors after:bg-(--ui-primary)' - : 'text-(--ui-text-muted)' ?>"> + <a + :href="$chapter->getUri()" + class=" + group relative w-full px-2.5 py-1.5 flex items-center gap-1.5 text-sm focus:outline-none focus-visible:outline-none hover:text-(--ui-text-highlighted) data-[state=open]:text-(--ui-text-highlighted) transition-colors + <?= $this->isCurrent($chapter) + ? 'text-(--ui-primary) after:absolute after:-left-1.5 after:inset-y-0.5 after:block after:w-px after:rounded-full after:transition-colors after:bg-(--ui-primary)' + : 'text-(--ui-text-muted)' ?> + "> {{ $chapter->title }} </a> </li> @@ -92,16 +95,28 @@ </x-template> </article> <!-- On this page --> - <nav class="w-2xs shrink-0 hidden xl:block sticky max-h-[calc(100dvh-var(--ui-header-height))] overflow-auto top-28 pt-4 pl-12 pr-4"> - <div :if="($subChapters = $this->getSubChapters()) !== []" class="text-sm"> + <nav class="w-2xs shrink-0 hidden xl:flex flex-col sticky max-h-[calc(100dvh-var(--ui-header-height))] overflow-auto top-28 pt-4 pl-12 pr-4"> + <div :if="($subChapters = $this->getSubChapters()) !== []" class="text-sm flex flex-col grow"> <span class="inline-block font-bold text-[--primary] mb-3">On this page</span> <ul class="flex flex-col"> - <li :foreach="['#top' => $this->currentChapter->title, ...$subChapters] as $url => $title"> - <a :href="$url" :data-on-this-page="$title" class="group relative text-sm flex items-center focus-visible:outline-(--ui-primary) py-1 text-(--ui-text-muted) hover:text-(--ui-text) data-[active]:text-(--ui-primary) transition-colors"> - {{ \Tempest\Support\Str\strip_tags($title) }} - </a> - </li> + <x-template :foreach="$subChapters as $url => $chapter"> + <li> + <a :href="$url" :data-on-this-page="$chapter['title']" class="group relative text-sm flex items-center focus-visible:outline-(--ui-primary) py-1 text-(--ui-text-muted) hover:text-(--ui-text) data-[active]:text-(--ui-primary) transition-colors"> + {{ \Tempest\Support\Str\strip_tags($chapter['title']) }} + </a> + </li> + <li :foreach="$chapter['children'] as $url => $title"> + <a :href="$url" :data-on-this-page="$title" class="pl-4 group relative text-sm flex items-center focus-visible:outline-(--ui-primary) py-1 text-(--ui-text-dimmed) hover:text-(--ui-text) data-[active]:text-(--ui-primary) transition-colors"> + {{ \Tempest\Support\Str\strip_tags($title) }}</span> + </a> + </li> + </x-template> </ul> + <div class="my-10 mt-auto flex"> + <a href="#top" class="border border-(--ui-border) bg-(--ui-bg-elevated) text-(--ui-text-muted) hover:text-(--ui-text) transition rounded-lg p-2"> + <x-icon name="tabler:arrow-up" class="size-5" /> + </a> + </div> </div> </nav> </div> diff --git a/src/Web/Meta/x-meta-image.view.php b/src/Web/Meta/x-meta-image.view.php index 0c9ef67b..c97f157e 100644 --- a/src/Web/Meta/x-meta-image.view.php +++ b/src/Web/Meta/x-meta-image.view.php @@ -1,5 +1,6 @@ <?php use function Tempest\uri; + ?> <x-component name="x-meta-image"> diff --git a/src/Web/assets/highlight-current-prose-title.ts b/src/Web/assets/highlight-current-prose-title.ts index 2a1b8423..8feb25d7 100644 --- a/src/Web/assets/highlight-current-prose-title.ts +++ b/src/Web/assets/highlight-current-prose-title.ts @@ -1,7 +1,7 @@ function findPreviousH2(element: Element | null): HTMLHeadingElement | null { while (element && element.previousElementSibling) { element = element.previousElementSibling - if (element.tagName === 'H2') { + if (element.tagName === 'H2' || element.tagName === 'H3') { return element as HTMLHeadingElement } } @@ -17,7 +17,7 @@ function updateActiveChapters(): void { for (const el of elements) { const rect = el.getBoundingClientRect() if (rect.top - topMargin >= 0 && rect.bottom <= window.innerHeight) { - if (el.tagName === 'H2' || el.tagName === 'H1') { + if (el.tagName === 'H3' || el.tagName === 'H2' || el.tagName === 'H1') { if (el.textContent) { visibleH2s.add(el.textContent.trim()) } diff --git a/src/Web/assets/typography.css b/src/Web/assets/typography.css index f780841a..3fdcfd04 100644 --- a/src/Web/assets/typography.css +++ b/src/Web/assets/typography.css @@ -18,6 +18,10 @@ @apply bg-(--ui-success)/10 text-(--ui-success) ring-(--ui-success)/25 [--tw-prose-links:var(--ui-success)]; } + &.alert-info { + @apply bg-(--ui-info)/5 text-(--ui-info) ring-(--ui-info)/25 [--tw-prose-links:var(--ui-info)]; + } + div.alert-wrapper { @apply flex flex-col gap-y-2; diff --git a/src/Web/x-base.view.php b/src/Web/x-base.view.php index bf9fa734..48966fda 100644 --- a/src/Web/x-base.view.php +++ b/src/Web/x-base.view.php @@ -51,7 +51,7 @@ function toggleDarkMode() { <x-slot name="head"/> </head> -<body :class="trim($bodyClass ?? '')" class="relative antialiased flex flex-col grow"> +<body :class="trim($bodyClass ?? '')" class="relative antialiased flex flex-col grow selection:bg-(--ui-primary)/20 selection:text-(--ui-primary)"> <div class="absolute pointer-events-none inset-0 bg-repeat" style="background-image: url(/noise.svg)"> <div id="command-palette"></div> </div>