Skip to content

Commit 0ec037f

Browse files
authored
Merge branch 'main' into fix/cache-lock
2 parents 9d03738 + fe31ada commit 0ec037f

File tree

120 files changed

+1648
-177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+1648
-177
lines changed

CHANGELOG.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,30 @@
22

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

5-
## [2.12.0](https://github.com/tempestphp/tempest-framework/compare/v2.11.0..2.12.0) — 2025-11-28
5+
## [2.13.0](https://github.com/tempestphp/tempest-framework/compare/v2.12.0..2.13.0) — 2025-12-04
6+
7+
### 🚀 Features
8+
9+
- **auth**: add OAuth installer (#1674) ([9c82b71](https://github.com/tempestphp/tempest-framework/commit/9c82b715b448633a704591e9b78823da28debc98))
10+
- **cache**: make `assertLocked` ensure that the checked lock has an expiration (#1758) ([1a2e8fb](https://github.com/tempestphp/tempest-framework/commit/1a2e8fbe90259d2bd5a8a0876d4b8fed35c5dcd7))
11+
- **container**: make all container properties publicly readable (#1785) ([be93ec1](https://github.com/tempestphp/tempest-framework/commit/be93ec1388ec7d253637705d4335d13a78a39f00))
12+
- **database**: add support for self-referencing relations (#1745) ([df2dcdc](https://github.com/tempestphp/tempest-framework/commit/df2dcdc231384d2dd359f8b621f0ae1f31a3e703))
13+
- **http**: add support to mark Request properties as #[SensitiveField] (#1746) ([0000c99](https://github.com/tempestphp/tempest-framework/commit/0000c99251b31d0bfa84389fb101be1560d916c3))
14+
15+
### 🐛 Bug fixes
16+
17+
- **auth**: correctly map user in GitHub OAuth provider (#1751) ([ad2182a](https://github.com/tempestphp/tempest-framework/commit/ad2182ac40684b78752e7f7511228688f5093c1a))
18+
- **auth**: pass scopes/options to auth URL builder (#1750) ([cbe54d7](https://github.com/tempestphp/tempest-framework/commit/cbe54d7f3f7e137fe43e9ad7f8837bd2f7103e9a))
19+
- **auth**: update outdated authenticatable import (#1752) ([5c68b96](https://github.com/tempestphp/tempest-framework/commit/5c68b968763229dfb5a78c01a80df3b1b134e6c0))
20+
- **cache**: support enum tags (#1756) ([678b695](https://github.com/tempestphp/tempest-framework/commit/678b69582e526e25ff545c346179bda9636f1415))
21+
- **cache**: add descriptions to `cache:clear` arguments (#1755) ([e324f6e](https://github.com/tempestphp/tempest-framework/commit/e324f6e767b50acd6e76e8310be12422b85e782b))
22+
- **command-bus**: extract uuid from pending commands when not provided (#1761) ([b787c16](https://github.com/tempestphp/tempest-framework/commit/b787c16e57f60de3bd7883944561f02fce3a661a))
23+
- **console**: properly normalize boolean flag names (#1762) ([c6e6867](https://github.com/tempestphp/tempest-framework/commit/c6e6867ede678b9798386bab12e1e2afaef91bc8))
24+
- **core**: gracefully handle missing seeders when using `db:seed` (#1759) ([450ca75](https://github.com/tempestphp/tempest-framework/commit/450ca7576c6e5a8f4f5719dd27e7d4d4a29954c9))
25+
- **process**: properly return exit code if missing (#1776) ([9ad1587](https://github.com/tempestphp/tempest-framework/commit/9ad158747a810db490aef43a7a6c1bcfe062d900))
26+
27+
28+
## [2.12.0](https://github.com/tempestphp/tempest-framework/compare/v2.11.0..v2.12.0) — 2025-11-28
629

730
### 🚀 Features
831

@@ -798,7 +821,7 @@ All notable changes to this project will be documented in this file.
798821
- rector (#680) ([7fdff1d](https://github.com/tempestphp/tempest-framework/commit/7fdff1d7be48ab91fb35e1a07434ae54ef47781c))
799822

800823

801-
## [1.0.0-alpha.3](https://github.com/tempestphp/tempest-framework/compare/v1.0.0-alpha.2..v1.0.0-alpha.3) — 2024-10-30
824+
## [1.0.0-alpha.3](https://github.com/tempestphp/tempest-framework/compare/v1.0.0-alpha.2..v1.0.0-alpha.3) — 2024-10-31
802825

803826
### 🚨 Breaking changes
804827

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"adam-paterson/oauth2-slack": "^1.1",
5050
"aws/aws-sdk-php": "^3.338.0",
5151
"azure-oss/storage-blob-flysystem": "^1.2",
52+
"brianium/paratest": "^7.14",
5253
"carthage-software/mago": "1.0.0-beta.28",
5354
"depotwarehouse/oauth2-twitch": "^1.3",
5455
"guzzlehttp/psr7": "^2.6.1",
@@ -87,8 +88,7 @@
8788
"tempest/blade": "dev-main",
8889
"thenetworg/oauth2-azure": "^2.2",
8990
"twig/twig": "^3.16",
90-
"wohali/oauth2-discord-new": "^1.2",
91-
"brianium/paratest": "^7.14"
91+
"wohali/oauth2-discord-new": "^1.2"
9292
},
9393
"replace": {
9494
"tempest/auth": "self.version",

docs/1-essentials/01-routing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public function docsRedirect(string $path): Redirect
241241

242242
## Generating URIs
243243

244-
Tempest provides a `\Tempest\uri` function that can be used to generate a URI to a controller method. This function accepts the FQCN of the controller or a callable to a method as its first argument, and named parameters as [the rest of its arguments](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list).
244+
Tempest provides a `\Tempest\Router\uri` function that can be used to generate a URI to a controller method. This function accepts the FQCN of the controller or a callable to a method as its first argument, and named parameters as [the rest of its arguments](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list).
245245

246246
```php
247247
use function Tempest\Router\uri;
@@ -337,7 +337,7 @@ A core pattern of any web application is to access data from the current request
337337

338338
In most situations, the data you expect to receive from a request is structured. You expect clients to send specific values, and you want them to follow specific rules.
339339

340-
The idiomatic way to achieve this is by using request classes. They are classes with public properties that correspond to the data you want to retrieve from the request. Tempest will automatically validate these properties using PHP's type system, in addition to optional [validation attributes](../2-features/06-validation) if needed.
340+
The idiomatic way to achieve this is by using request classes. They are classes with public properties that correspond to the data you want to retrieve from the request. Tempest will automatically validate these properties using PHP's type system, in addition to optional [validation attributes](../2-features/03-validation) if needed.
341341

342342
A request class must implement {`Tempest\Http\Request`} and should use the {`Tempest\Http\IsRequest`} trait, which provides the default implementation.
343343

@@ -454,7 +454,7 @@ The JSON encoded header is available for when you're building APIs with Tempest.
454454
</x-form>
455455
```
456456

457-
`{html}<x-form>` is a view component that will automatically include the CSRF token, as well as default to sending `POST` requests. `{html}<x-input>` is a view component that renders a label, input field, and validation errors all at once. In practice, you'll likely want to make changes to these built-in view components. That's why you can run `./tempest install view-components` and select the components you want to pull into your project. You can [read more about installing view components here](/2.x/essentials/views#built-in-components).
457+
`{html}<x-form>` is a view component that will automatically include the CSRF token, as well as default to sending `POST` requests. `{html}<x-input>` is a view component that renders a label, input field, and validation errors all at once. In practice, you'll likely want to make changes to these built-in view components. That's why you can run `./tempest install view-components` and select the components you want to pull into your project. You can [read more about installing view components here](../1-essentials/02-views.md#built-in-components).
458458

459459
## Route middleware
460460

@@ -946,7 +946,7 @@ return new DatabaseSessionConfig(
946946

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

949-
Outdated sessions must occasionally be cleaned up. Tempest comes with a built-in command to do so, `session:clean`. This command makes use of the [scheduler](/2.x/features/scheduling). If you have scheduling enabled, it will automatically run behind the scenes.
949+
Outdated sessions must occasionally be cleaned up. Tempest comes with a built-in command to do so, `session:clean`. This command makes use of the [scheduler](../2-features/11-scheduling.md). If you have scheduling enabled, it will automatically run behind the scenes.
950950

951951
## Deferring tasks
952952

docs/1-essentials/03-database.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ final class Book
219219
}
220220
```
221221

222+
:::warning
223+
Relation types in docblocks must always be fully qualified, and not use short class names.
224+
:::
225+
222226
Tempest will infer all the information it needs to build the right queries for you. However, there might be cases where property names and type information don't map one-to-one on your database schema. In that case you can use dedicated attributes to define relations.
223227

224228
### Relation attributes

docs/2-features/17-oauth.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@ This implementation is built on top of the PHP league's [OAuth client](https://g
1212

1313
## Getting started
1414

15-
To get started with OAuth, you will first need to create a configuration file for your desired OAuth provider.
15+
Tempest provides an installer to quickly set up OAuth in your project. You can run the installer using the following command:
16+
17+
```sh
18+
./tempest install auth --oauth
19+
```
20+
21+
The installer will:
22+
- Prompt you to select one or more OAuth providers from the available options
23+
- Publish the necessary configuration files and controller stubs
24+
- Optionally add the OAuth credentials to your `.env` and `.env.example` files
25+
- Optionally install the required Composer dependencies for the selected providers
26+
27+
This is the quickest way to get started with OAuth in your Tempest application.
28+
29+
Alternatively, you can manually create a configuration file for your desired OAuth provider.
1630

1731
Tempest provides a [different configuration object for each provider](#available-providers). For instance, if you wish to authenticate users with GitHub, you may create a `github.config.php` file returning an instance of {b`Tempest\Auth\OAuth\Config\GitHubOAuthConfig`}:
1832

packages/auth/src/Exceptions/AuthenticatableModelWasInvalid.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Tempest\Auth\Exceptions;
44

55
use Exception;
6-
use Tempest\Auth\Authenticatable;
6+
use Tempest\Auth\Authentication\Authenticatable;
77

88
final class AuthenticatableModelWasInvalid extends Exception implements AuthenticationException
99
{

packages/auth/src/Exceptions/OAuthProviderWasMissing.php

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@
44

55
namespace Tempest\Auth\Exceptions;
66

7-
use AdamPaterson\OAuth2\Client\Provider\Slack;
87
use Exception;
9-
use League\OAuth2\Client\Provider\Apple;
10-
use League\OAuth2\Client\Provider\Facebook;
11-
use League\OAuth2\Client\Provider\Instagram;
12-
use League\OAuth2\Client\Provider\LinkedIn;
13-
use Stevenmaguire\OAuth2\Client\Provider\Microsoft;
14-
use Wohali\OAuth2\Client\Provider\Discord;
8+
use Tempest\Auth\OAuth\SupportedOAuthProvider;
159

1610
final class OAuthProviderWasMissing extends Exception implements AuthenticationException
1711
{
@@ -28,15 +22,6 @@ public function __construct(
2822

2923
private function getPackageName(): ?string
3024
{
31-
return match ($this->missing) {
32-
Facebook::class => 'league/oauth2-facebook',
33-
Instagram::class => 'league/oauth2-instagram',
34-
LinkedIn::class => 'league/oauth2-linkedin',
35-
Apple::class => 'patrickbussmann/oauth2-apple',
36-
Microsoft::class => 'stevenmaguire/oauth2-microsoft',
37-
Discord::class => 'wohali/oauth2-discord-new',
38-
Slack::class => 'adam-paterson/oauth2-slack',
39-
default => null,
40-
};
25+
return SupportedOAuthProvider::tryFrom($this->missing)?->composerPackage();
4126
}
4227
}

packages/auth/src/Installer/AuthenticationInstaller.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public function install(): void
4040
migration: $this->container->get(to_fqcn($migration, root: root_path())),
4141
);
4242
}
43+
44+
if ($this->shouldInstallOAuth()) {
45+
$this->container->get(OAuthInstaller::class)->install();
46+
}
4347
}
4448

4549
private function shouldMigrate(): bool
@@ -52,5 +56,16 @@ private function shouldMigrate(): bool
5256

5357
return (bool) $argument->value;
5458
}
59+
60+
private function shouldInstallOAuth(): bool
61+
{
62+
$argument = $this->consoleArgumentBag->get('oauth');
63+
64+
if ($argument === null || ! is_bool($argument->value)) {
65+
return $this->console->confirm('Do you want to install OAuth?', default: false);
66+
}
67+
68+
return (bool) $argument->value;
69+
}
5570
}
5671
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Auth\Installer;
6+
7+
use Tempest\Auth\OAuth\SupportedOAuthProvider;
8+
use Tempest\Core\PublishesFiles;
9+
use Tempest\Process\ProcessExecutor;
10+
use Tempest\Support\Filesystem\Exceptions\PathWasNotFound;
11+
use Tempest\Support\Filesystem\Exceptions\PathWasNotReadable;
12+
use Tempest\Support\Str\ImmutableString;
13+
14+
use function Tempest\root_path;
15+
use function Tempest\src_path;
16+
use function Tempest\Support\arr;
17+
use function Tempest\Support\Filesystem\read_file;
18+
use function Tempest\Support\Namespace\to_fqcn;
19+
use function Tempest\Support\str;
20+
21+
final class OAuthInstaller
22+
{
23+
use PublishesFiles;
24+
25+
public function __construct(
26+
private readonly ProcessExecutor $processExecutor,
27+
) {}
28+
29+
public function install(): void
30+
{
31+
$providers = $this->getProviders();
32+
33+
if (count($providers) === 0) {
34+
return;
35+
}
36+
37+
$this->publishStubs(...$providers);
38+
39+
if ($this->confirm('Would you like to add the OAuth config variables to your .env file?', default: true)) {
40+
$this->updateEnvFile(...$providers);
41+
}
42+
43+
if ($this->confirm('Install composer dependencies?', default: true)) {
44+
$this->installComposerDependencies(...$providers);
45+
}
46+
47+
$this->console->instructions([
48+
sprintf('<strong>The selected OAuth %s installed in your project</strong>', count($providers) > 1 ? 'providers are' : 'provider is'),
49+
'',
50+
'Next steps:',
51+
'1. Update the .env file with your OAuth credentials',
52+
'2. Implement the OAuth controller callback method',
53+
'3. Review and customize the published files if needed',
54+
'',
55+
'<strong>Published files</strong>',
56+
...arr($this->publishedFiles)->map(fn (string $file) => '<style="fg-green">→</style> ' . $file),
57+
]);
58+
}
59+
60+
/**
61+
* @return list<SupportedOAuthProvider>
62+
*/
63+
private function getProviders(): array
64+
{
65+
return $this->ask(
66+
question: 'Please choose an OAuth provider',
67+
options: SupportedOAuthProvider::cases(),
68+
multiple: true,
69+
);
70+
}
71+
72+
private function publishStubs(SupportedOAuthProvider ...$providers): void
73+
{
74+
foreach ($providers as $provider) {
75+
$this->publishController($provider);
76+
77+
$this->publishConfig($provider);
78+
79+
$this->publishImports();
80+
}
81+
}
82+
83+
private function publishConfig(SupportedOAuthProvider $provider): void
84+
{
85+
$name = strtolower($provider->name);
86+
$source = __DIR__ . "/../Installer/oauth/{$name}.config.stub.php";
87+
88+
$this->publish(
89+
source: $source,
90+
destination: src_path("Authentication/OAuth/{$name}.config.php"),
91+
);
92+
}
93+
94+
private function publishController(SupportedOAuthProvider $provider): void
95+
{
96+
$fileName = str($provider->value)
97+
->classBasename()
98+
->replace('Provider', '')
99+
->append('Controller.php')
100+
->toString();
101+
102+
$this->publish(
103+
source: __DIR__ . '/oauth/OAuthControllerStub.php',
104+
destination: src_path("Authentication/OAuth/{$fileName}"),
105+
callback: function (string $source, string $destination) use ($provider) {
106+
$providerFqcn = $provider::class;
107+
$name = strtolower($provider->name);
108+
$userModelFqcn = to_fqcn(src_path('Authentication/User.php'), root: root_path());
109+
110+
$this->update(
111+
path: $destination,
112+
callback: fn (ImmutableString $contents) => $contents->replace(
113+
search: [
114+
"'tag_name'",
115+
'redirect-route',
116+
'callback-route',
117+
"'user-model-fqcn'",
118+
'provider_db_column',
119+
],
120+
replace: [
121+
"\\{$providerFqcn}::{$provider->name}",
122+
"/auth/{$name}",
123+
"/auth/{$name}/callback",
124+
"\\{$userModelFqcn}::class",
125+
"{$name}_id",
126+
],
127+
),
128+
);
129+
},
130+
);
131+
}
132+
133+
private function installComposerDependencies(SupportedOAuthProvider ...$providers): void
134+
{
135+
$packages = arr($providers)
136+
->map(fn (SupportedOAuthProvider $provider) => $provider->composerPackage())
137+
->filter();
138+
139+
if ($packages->isNotEmpty()) {
140+
$this->processExecutor->run("composer require {$packages->implode(' ')}");
141+
}
142+
}
143+
144+
private function updateEnvFile(SupportedOAuthProvider ...$providers): void
145+
{
146+
arr($providers)
147+
->map(fn (SupportedOAuthProvider $provider) => $this->extractSettings($provider))
148+
->filter()
149+
->flatten()
150+
->each(function (string $setting) {
151+
foreach (['.env', '.env.example'] as $envFile) {
152+
$this->update(
153+
path: root_path($envFile),
154+
callback: static fn (ImmutableString $contents): ImmutableString => $contents->contains($setting)
155+
? $contents
156+
: $contents->append(PHP_EOL, "{$setting}="),
157+
ignoreNonExisting: true,
158+
);
159+
}
160+
});
161+
}
162+
163+
private function extractSettings(SupportedOAuthProvider $provider): array
164+
{
165+
$name = strtolower($provider->name);
166+
$configPath = __DIR__ . "/../Installer/oauth/{$name}.config.stub.php";
167+
168+
try {
169+
return str(read_file($configPath))
170+
->matchAll("/env\('(OAUTH_[^']*)'/", matches: 1)
171+
->map(fn (array $matches) => $matches[1] ?? null)
172+
->filter()
173+
->toArray();
174+
} catch (PathWasNotFound|PathWasNotReadable) {
175+
return [];
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)