Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e47eb27
ci: cap benchmark runtime and split its steps (#2286)
osbre Sep 9, 2026
aea7f2b
feat(container): add dynamic tags (#2288)
brendt Sep 9, 2026
37c9012
chore: apply Rector (#2290)
brendt Sep 9, 2026
06addeb
fix(container): fix dynamic tag oversights (#2291)
brendt Sep 9, 2026
0061698
fix(console): properly restore terminal after testing (#2294)
brendt Sep 9, 2026
0f478fb
fix(database): don't emit `UNSIGNED` on Postgres (#2295)
osbre Sep 14, 2026
a14c2f3
fix(framework): read one-line docblock vars in meta:view-component (#…
dereuromark Sep 30, 2026
c9949b3
fix(database): qualify orderBy fields against the base table (#2306)
radoslav-grencik Oct 1, 2026
05c7376
fix(mapper): materialize promoted defaults for readonly properties (#…
radoslav-grencik Oct 1, 2026
ef53907
docs(vite): document asset prefetching (#2308)
osbre Oct 2, 2026
854954b
fix: dependencies and CI issues (#2314)
brendt Oct 22, 2026
af14fef
chore: release v3.19.3
brendt Oct 22, 2026
6f7d8f6
chore: post-release clean up
brendt Oct 22, 2026
d4459e0
feat(database): support unsigned primary keys on MySQL (#2312)
sjorsvanleeuwen Oct 22, 2026
f869ea7
fix(container): detect nested circular dependencies (#2297)
osbre Oct 22, 2026
e114a7f
chore: release v3.20.0
brendt Oct 22, 2026
b0ff137
chore: post-release clean up
brendt Oct 22, 2026
577f8dc
perf(http): find expired file sessions by modification time (#2317)
osbre Oct 23, 2026
c68e0ac
fix(database): resolve uuid primary keys from positional insert bindi…
radoslav-grencik Oct 23, 2026
1509573
perf: improve session handling (#2318)
brendt Oct 23, 2026
f759cdc
fix(database): declare TModel template on where-builder traits (#2322)
ondrejmirtes Oct 24, 2026
86b06a9
fix(cryptography): measure timelock elapsed time with the clock (#2321)
osbre Oct 25, 2026
809b73b
feat(database): support creating database configs from a URL (#2315)
boydbloemsma Oct 29, 2026
2f83459
feat(http)!: regenerate the session identifier on authentication (#2284)
osbre Oct 29, 2026
8c7de8d
chore: prepare for release (#2327)
brendt Oct 29, 2026
8fe1705
chore: release v3.21.0
brendt Oct 29, 2026
a626fe4
chore: post-release clean up
brendt Oct 29, 2026
e16e5a4
refactor(generation)!: replace nette/php-generator with a php-parser …
xHeaven Oct 31, 2026
2893163
refactor(view)!: replace commonmark with tempest markdown
xHeaven Nov 2, 2026
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
25 changes: 19 additions & 6 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
if: ${{ github.event.pull_request.base.ref == github.event.repository.default_branch }}
name: Performance Regression Check
runs-on: ubuntu-latest
timeout-minutes: 180

steps:
- name: Checkout PR branch
Expand All @@ -44,23 +45,35 @@ jobs:
- name: Install dependencies
uses: ramsey/composer-install@v3

- name: Benchmark base branch
- name: Check out base branch
run: |
git checkout ${{ github.event.pull_request.base.sha }}
cp .env.example .env
sed -i 's/^INTERNAL_CACHES=.*/INTERNAL_CACHES=false/' .env
sed -i 's/^DISCOVERY_CACHE=.*/DISCOVERY_CACHE=false/' .env
composer update --no-interaction --prefer-dist --quiet
vendor/bin/phpbench run --tag=base --store --progress=none

- name: Benchmark PR branch
- name: Install base branch dependencies
timeout-minutes: 20
run: composer update --no-interaction --prefer-dist --quiet

- name: Benchmark base branch
timeout-minutes: 70
run: vendor/bin/phpbench run --tag=base --store --progress=none

- name: Check out PR branch
run: |
git checkout ${{ github.event.pull_request.head.sha }}
cp .env.example .env
sed -i 's/^INTERNAL_CACHES=.*/INTERNAL_CACHES=false/' .env
sed -i 's/^DISCOVERY_CACHE=.*/DISCOVERY_CACHE=false/' .env
composer update --no-interaction --prefer-dist --quiet
vendor/bin/phpbench run --tag=pr --store --ref=base --report=aggregate --progress=none --output=benchmark-comment | tee benchmark-result.txt || true

- name: Install PR branch dependencies
timeout-minutes: 20
run: composer update --no-interaction --prefer-dist --quiet

- name: Benchmark PR branch
timeout-minutes: 70
run: vendor/bin/phpbench run --tag=pr --store --ref=base --report=aggregate --progress=none --output=benchmark-comment | tee benchmark-result.txt || true

- name: Prepare artifact
run: |
Expand Down
22 changes: 14 additions & 8 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ jobs:

phpunit:
runs-on: ${{ matrix.os }}
services:
mysql:
image: ${{ matrix.database == 'mysql' && 'mysql:8.0' || '' }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: app
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=5
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -97,20 +110,13 @@ jobs:
if: ${{ matrix.os != 'windows-latest' }}
uses: supercharge/redis-github-action@1.8.1

- name: "Setup MySQL"
if: ${{ matrix.database == 'mysql' }}
uses: ankane/setup-mysql@v1
with:
mysql-version: 8.0
database: "app"

- name: "Setup PostgreSQL"
if: ${{ matrix.database == 'postgres' }}
uses: ankane/setup-postgres@v1

- name: Create worker databases - mysql
if: ${{ matrix.database == 'mysql' }}
run: for i in 1 2 3 4; do mysql -u root -e "CREATE DATABASE IF NOT EXISTS app$i;"; done
run: for i in 1 2 3 4; do docker exec "${{ job.services.mysql.id }}" mysql -u root -e "CREATE DATABASE IF NOT EXISTS app$i;"; done

- name: Create worker databases - postgres
if: ${{ matrix.database == 'postgres' }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Top level directories
/log/
/bench/
/package-lock.json

# Top-level+Sub directories
Expand Down
56 changes: 55 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,61 @@

All notable changes to this project will be documented in this file.

## [3.19.2](https://github.com/tempestphp/tempest-framework/compare/v3.19.1..3.19.2) — 2026-09-08
## [3.21.0](https://github.com/tempestphp/tempest-framework/compare/v3.20.0..3.21.0) — 2026-10-29

### 🚨 Breaking changes

- **http**: [**breaking**] regenerate the session identifier on authentication (#2284) ([2f83459](https://github.com/tempestphp/tempest-framework/commit/2f83459d951fec13414b4c4b6d99b94771b17dcc))

### 🚀 Features

- **database**: support creating database configs from a URL (#2315) ([809b73b](https://github.com/tempestphp/tempest-framework/commit/809b73bc10ee72d128be078468b3642cf6f3d1bf))

### ⚡ Performance

- **http**: find expired file sessions by modification time (#2317) ([577f8dc](https://github.com/tempestphp/tempest-framework/commit/577f8dcfe8e7988cc8abcd0960a3ea4b4c519436))
- improve session handling (#2318) ([1509573](https://github.com/tempestphp/tempest-framework/commit/1509573a2148600c284435dde8761638d30aa2b9))

### 🐛 Bug fixes

- **cryptography**: measure timelock elapsed time with the clock (#2321) ([86b06a9](https://github.com/tempestphp/tempest-framework/commit/86b06a9455206fef95386d07aa7dd2f9a1e1a6a0))
- **database**: resolve uuid primary keys from positional insert bindings (#2301) ([c68e0ac](https://github.com/tempestphp/tempest-framework/commit/c68e0ac2c31364125c367ffa8ed9f26b0d2b28cd))
- **database**: declare TModel template on where-builder traits (#2322) ([f759cdc](https://github.com/tempestphp/tempest-framework/commit/f759cdca1da965aa1bae881c4b168e33dc9252e5))


## [3.20.0](https://github.com/tempestphp/tempest-framework/compare/v3.19.3..v3.20.0) — 2026-10-22

### 🚀 Features

- **database**: support unsigned primary keys on MySQL (#2312) ([d4459e0](https://github.com/tempestphp/tempest-framework/commit/d4459e0c205f5f30bbcaf66bdde7650f61c1a6d3))

### 🐛 Bug fixes

- **container**: detect nested circular dependencies (#2297) ([f869ea7](https://github.com/tempestphp/tempest-framework/commit/f869ea70ae16f16db6dc1991a60d5982e6164020))


## [3.19.3](https://github.com/tempestphp/tempest-framework/compare/v3.19.2..v3.19.3) — 2026-10-22

### 🚀 Features

- **container**: add dynamic tags (#2288) ([aea7f2b](https://github.com/tempestphp/tempest-framework/commit/aea7f2b9127f26120cf662a5f78f047f7e639efb))

### ⚡ Performance

- **mapper**: cache per-class reflection plans during hydration (#2276) ([952b707](https://github.com/tempestphp/tempest-framework/commit/952b70770fc64585b7c6557e1d150d5c243f168c))

### 🐛 Bug fixes

- **console**: properly restore terminal after testing (#2294) ([0061698](https://github.com/tempestphp/tempest-framework/commit/006169824ba6ae00edd0d87662f01bc6baddc401))
- **container**: fix dynamic tag oversights (#2291) ([06addeb](https://github.com/tempestphp/tempest-framework/commit/06addeb3076f995365c5960ea00af4fd596f9155))
- **database**: don't emit `UNSIGNED` on Postgres (#2295) ([0f478fb](https://github.com/tempestphp/tempest-framework/commit/0f478fb487e12a5891738a15a1663fa44c47295b))
- **database**: qualify orderBy fields against the base table (#2306) ([c9949b3](https://github.com/tempestphp/tempest-framework/commit/c9949b3dd6c96a656cf7881c130f1988e2ea4a94))
- **framework**: read one-line docblock vars in meta:view-component (#2299) ([a14c2f3](https://github.com/tempestphp/tempest-framework/commit/a14c2f34ab7d38d5326ed58a0781e942a1d0f970))
- **mapper**: materialize promoted defaults for readonly properties (#2304) ([05c7376](https://github.com/tempestphp/tempest-framework/commit/05c73765c7f1c65dfa908fd81f71768686105254))
- dependencies and CI issues (#2314) ([854954b](https://github.com/tempestphp/tempest-framework/commit/854954b40f2fe65d1d1b5f5cd8c893b6c266a0af))


## [3.19.2](https://github.com/tempestphp/tempest-framework/compare/v3.19.1..v3.19.2) — 2026-09-08

### 🚀 Features

Expand Down
6 changes: 2 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@
"giggsey/libphonenumber-for-php-lite": "^9.0",
"guzzlehttp/guzzle": "^7.8.2",
"laminas/laminas-diactoros": "^3.3",
"league/commonmark": "^2.7",
"league/flysystem": "^3.29.1",
"league/mime-type-detection": "^1.16",
"league/oauth2-client": "^2.8",
"monolog/monolog": "^3.7.0",
"nette/php-generator": "4.2.1",
"nette/schema": "^1.3.4",
"nikic/php-parser": "^5.3",
"nikic/php-parser": "^5.9",
"php": "^8.5",
"psr-discovery/http-client-implementations": "^1.4",
"psr-discovery/http-factory-implementations": "^1.2",
Expand All @@ -45,6 +42,7 @@
"symfony/var-exporter": "^7.1|^8.0",
"symfony/yaml": "^7.3|^8.0",
"tempest/highlight": "^2.11.4",
"tempest/markdown": "^1.3",
"vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "^2.0.3"
},
Expand Down
4 changes: 3 additions & 1 deletion docs/1-essentials/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,8 @@ return new CookieConfig(

Sessions in Tempest are managed by the {b`Tempest\Http\Session\Session`} class. It can be injected anywhere needed. As soon as the {b`Tempest\Http\Session\Session`} is injected, it is started behind the scenes.

New sessions are only persisted once something is stored in them. This way, clients that don't keep cookies, such as API consumers or crawlers, don't fill up the session storage. Likewise, sessions that didn't change during a request aren't written again; only their activity is tracked.

```php
use Tempest\Http\Session\Session;

Expand Down Expand Up @@ -1062,7 +1064,7 @@ return new DatabaseSessionConfig(

Sessions expire based on the last activity time. This means that as long as a user is actively using the application, their session remains valid.

By default, Tempest removes expired session randomly at the end of a request, in a deferred task. This can be configured by specifying a {b`Tempest\Http\Session\CleanupStrategy`} in the [session configuration](#session-configuration).
By default, Tempest removes expired sessions at the end of a random request, in a deferred task. To keep the cost of cleaning up from growing with traffic, this happens at most once per minute. This can be configured by specifying a {b`Tempest\Http\Session\CleanupStrategy`} in the [session configuration](#session-configuration).

```php app/Sessions/session.config.php
use Tempest\Http\Session\Config\DatabaseSessionConfig;
Expand Down
85 changes: 85 additions & 0 deletions docs/1-essentials/03-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,40 @@ return new PostgresConfig(
);
```

### Connecting with a connection string

Some hosting providers, such as Laravel Cloud, inject the connection details as a single `DATABASE_URL` environment variable. Each configuration class provides a `fromConnectionString()` method that builds the configuration from such a connection string:

```php app/Config/database.config.php
use Tempest\Database\Config\MysqlConfig;
use function Tempest\env;

return MysqlConfig::fromConnectionString(env('DATABASE_URL'));
```

Components missing from the connection string fall back to their defaults. Any other constructor argument may be passed by name, and takes precedence over the values parsed from the connection string:

```php app/Config/database.config.php
return PostgresConfig::fromConnectionString(env('DATABASE_URL'), persistent: true);
```

Query parameters are appended to the DSN, which allows passing driver-specific options such as `sslmode` or `connect_timeout`:

```
postgres://user:secret@db.example.com:5432/app?sslmode=require&connect_timeout=10
```

To connect through a Unix socket, pass its path as a percent-encoded host, or as the `host` query parameter for PostgreSQL and the `unix_socket` query parameter for MySQL:

```
postgres://user:secret@%2Fvar%2Frun%2Fpostgresql/app
mysql://user:secret@localhost/app?unix_socket=/var/run/mysqld/mysqld.sock
```

{b`Tempest\Database\Config\MysqlConfig`} accepts the `mysql` and `mariadb` schemes, {b`Tempest\Database\Config\PostgresConfig`} accepts `postgres`, `postgresql` and `pgsql`, and {b`Tempest\Database\Config\SQLiteConfig`} accepts connection strings such as `sqlite:///path/to/database.sqlite` and `sqlite::memory:`. Credentials containing special characters must be percent-encoded.

The parsed components are also available through the {b`Tempest\Database\Config\DatabaseConnectionString`} value object, using `DatabaseConnectionString::from()`.

## Querying the database

Multiple approaches exist for querying the database, all of which execute a {b`Tempest\Database\Query`} on the {b`Tempest\Database\Database`} class. The most straightforward approach is to inject {b`Tempest\Database\Database`}:
Expand Down Expand Up @@ -406,10 +440,61 @@ final class CreateBooksTable implements MigratesUp
}
```

Columns referencing a UUID primary key need a UUID-compatible type. Use `belongsToUuid()` and `foreignUuid()` as UUID counterparts of `belongsTo()` and `foreignId()`:

```php app/Books/CreateChaptersTable.php
use Tempest\Database\MigratesUp;
use Tempest\Database\QueryStatement;
use Tempest\Database\QueryStatements\CreateTableStatement;
use Tempest\Database\QueryStatements\OnDelete;

final class CreateChaptersTable implements MigratesUp
{
public string $name = '2024-08-13_create_chapters_table';

public function up(): QueryStatement
{
return new CreateTableStatement('chapters')
->uuid()
->text('title')
->foreignUuid('book_uuid', constrainedOn: 'books.uuid', onDelete: OnDelete::CASCADE);
}
}
```

UUID columns use `CHAR(36)` on MySQL, `UUID` on PostgreSQL, and `TEXT` on SQLite, so foreign keys referencing UUID primary keys are type-compatible on every supported dialect.

:::warning
Currently, the [`IsDatabaseModel`](#the-is-database-model-trait) trait already provides a primary `$id` property. It is therefore not possible to use UUIDs alongside `IsDatabaseModel`.
:::

### Using unsigned integers as primary keys

By default, Tempest uses signed auto-incrementing integers as primary keys. On MySQL, you may use unsigned integers instead by specifying `unsigned: true` to the `primary()` method. Foreign keys referencing an unsigned primary key must be unsigned as well, so specify `unsigned: true` to `belongsTo()` or `foreignId()` too:

```php app/Books/CreateBooksTable.php
use Tempest\Database\MigratesUp;
use Tempest\Database\QueryStatement;
use Tempest\Database\QueryStatements\CreateTableStatement;

final class CreateBooksTable implements MigratesUp
{
public string $name = '2024-08-12_create_books_table';

public function up(): QueryStatement
{
return new CreateTableStatement('books')
->primary(unsigned: true)
->foreignId('author_id', constrainedOn: 'authors', unsigned: true)
->text('title');
}
}
```

:::info
PostgreSQL and SQLite have no unsigned integer type, so this option has no effect on primary keys for those databases.
:::

### Table names

Tempest infers the table name for a model class based on the model's classname. By default, the table name is the pluralized, `snake_cased` version of the base class name. This can be overridden using the {b`Tempest\Database\Table`} attribute:
Expand Down
41 changes: 38 additions & 3 deletions docs/1-essentials/05-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ final readonly class MarkdownInitializer implements Initializer

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 differentiate them using the `tag` parameter of the `#[Singleton]` attribute:
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 for CLI highlighting. In this situation, you can differentiate them using the `tag` parameter of the `#[Singleton]` attribute:

```php app/WebHighlighterInitializer.php
use Tempest\Container\Container;
Expand Down Expand Up @@ -283,15 +283,50 @@ $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.
[This blog post](https://stitcher.io/blog/tagged-singletons) provides in-depth explanations about tagged singletons.
:::

### Dynamic tags
### The `HasTag` interface

Some components implement the {`Tempest\Container\HasTag`} interface, which requires a `tag` property. Singletons using this interface are tagged by the `tag` property, essentially providing the ability to have dynamic tags.

This is specifically useful to get multiple instances of the same configuration. This is how [multiple database connections support](../1-essentials/03-database.md#using-multiple-connections) is implemented.

### Dynamic tags

It's possible to allow on-the-fly tagging of certain dependencies with no pre-defined tags. However, you need to explicitly specify you want to enable dynamic tags for a given dependency with the `#[Singleton]` attribute:

```php
use Tempest\Container\Singleton;

#[Singleton(dynamicTags: true)]
final class TenantRepository
{ /* … */ }
```

```php
use Tempest\Container\Initializer;
use Tempest\Container\Container;
use Tempest\Container\Singleton;

final class TenantRepositoryInitializer implements Initializer
{
#[Singleton(dynamicTags: true)]
public function initialize(Container $container): TenantRepository
{
return new TenantRepository();
}
}
```

With dynamic tags configured, you can now retrieve dependencies from the container with tags that weren't pre-configured:

```php
$repository = $container->get(TenantRepository::class, tag: $tenant->id);
```

As you might have guessed from the examples, dynamic tags are useful to build features with like multi-tenant systems.

## 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).
Expand Down
17 changes: 16 additions & 1 deletion docs/2-features/02-asset-bundling.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Asset bundling
description: "Web applications usually need to serve assets to users. Tempest provide a seamless integration with Vite, the most popular front-end development server and build tool"
keywords: ["vite", "frontend", "js", "css", "ts", "typescript", "javascript", "sri", "manifest", "assets"]
keywords: ["vite", "frontend", "js", "css", "ts", "typescript", "javascript", "sri", "manifest", "assets", "prefetch"]
---

## Overview
Expand Down Expand Up @@ -102,6 +102,21 @@ By default, assets are compiled in the `public/build` directory. This directory
This directory is already in your `.gitignore` if you used the `{sh}php tempest install vite` command.
:::

## Prefetching assets

Tempest can instruct the browser to prefetch lazily loaded chunks in the background, so the user doesn't have to wait for them to download later. To enable it, configure the `prefetching` parameter in a `vite.config.php` file:

```php app/vite.config.php
return new ViteConfig(
prefetching: new PrefetchConfig(
strategy: PrefetchStrategy::WATERFALL,
concurrent: 3,
),
);
```

The `WATERFALL` strategy downloads `concurrent` assets at a time, while `AGGRESSIVE` downloads all of them at once. Prefetching starts on the `load` event by default, which you may change with the `prefetchEvent` parameter.

## Using a `nonce` attribute

If your application uses a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP), you may need to include `nonce` attributes to the tags generated by `{html}<x-vite-tags />`.
Expand Down
Loading
Loading