Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Markdown/Alerts/AlertBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -49,6 +54,7 @@ public function parseInlines(): bool
return true;
}

#[\Override]
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($cursor->isIndented()) {
Expand All @@ -64,6 +70,7 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
return BlockContinue::at($cursor);
}

#[\Override]
public function closeBlock(): void
{
// Nothing to do here
Expand Down
9 changes: 5 additions & 4 deletions src/Markdown/Alerts/AlertBlockRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()),
],
);

Expand Down
1 change: 1 addition & 0 deletions src/Markdown/Alerts/AlertBlockStartParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

final class AlertBlockStartParser implements BlockStartParserInterface
{
#[\Override]
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
Expand Down
1 change: 1 addition & 0 deletions src/Markdown/Alerts/AlertExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

final class AlertExtension implements ExtensionInterface
{
#[\Override]
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addBlockStartParser(new AlertBlockStartParser());
Expand Down
3 changes: 2 additions & 1 deletion src/Markdown/CodeBlockRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Markdown/FqcnParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/Markdown/HandleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/Markdown/TempestPackageParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions src/Web/Documentation/ChapterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/Web/Documentation/ChapterRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
38 changes: 35 additions & 3 deletions src/Web/Documentation/ChapterView.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,44 @@ public function isCurrent(Chapter $other): bool

public function getSubChapters(): array
{
preg_match_all('/<h2.*>.*<a.*href="(?<uri>.*?)".*<\/span>(?<title>.*)<\/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;
Expand Down
10 changes: 7 additions & 3 deletions src/Web/Documentation/DocumentationIndexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)!
:::
Loading
Loading