Skip to content

Commit d06fa1a

Browse files
authored
Merge branch '3.x' into feat/uuid
2 parents b4c0c49 + 3c0d1f3 commit d06fa1a

File tree

12 files changed

+510
-11
lines changed

12 files changed

+510
-11
lines changed

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

packages/console/src/Console.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,11 @@ public function supportsPrompting(): bool;
157157
* Forces the console to not be interactive.
158158
*/
159159
public function disablePrompting(): self;
160+
161+
/**
162+
* Whether the console is in forced mode (skipping confirmations).
163+
*/
164+
public bool $isForced {
165+
get;
166+
}
160167
}

packages/console/src/GenericConsole.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class GenericConsole implements Console
3939

4040
private ?string $label = null;
4141

42-
private bool $isForced = false;
42+
private(set) bool $isForced = false;
4343

4444
private bool $supportsPrompting = true;
4545

packages/console/src/Middleware/CautionMiddleware.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next
2525
$environment = $this->appConfig->environment;
2626

2727
if ($environment->isProduction() || $environment->isStaging()) {
28+
if ($this->console->isForced) {
29+
return $next($invocation);
30+
}
31+
2832
$continue = $this->console->confirm('This command might be destructive. Do you wish to continue?');
2933

3034
if (! $continue) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session\Config;
6+
7+
use Tempest\Container\Container;
8+
use Tempest\DateTime\Duration;
9+
use Tempest\Http\Session\Managers\RedisSessionManager;
10+
use Tempest\Http\Session\SessionConfig;
11+
12+
final class RedisSessionConfig implements SessionConfig
13+
{
14+
/**
15+
* @param Duration $expiration Time required for a session to expire.
16+
*/
17+
public function __construct(
18+
private(set) Duration $expiration,
19+
readonly string $prefix = 'session:',
20+
) {}
21+
22+
public function createManager(Container $container): RedisSessionManager
23+
{
24+
return $container->get(RedisSessionManager::class);
25+
}
26+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Session\Managers;
6+
7+
use Tempest\Clock\Clock;
8+
use Tempest\Http\Session\Session;
9+
use Tempest\Http\Session\SessionConfig;
10+
use Tempest\Http\Session\SessionDestroyed;
11+
use Tempest\Http\Session\SessionId;
12+
use Tempest\Http\Session\SessionManager;
13+
use Tempest\KeyValue\Redis\Redis;
14+
use Tempest\Support\Str\ImmutableString;
15+
use Throwable;
16+
17+
use function Tempest\event;
18+
19+
final readonly class RedisSessionManager implements SessionManager
20+
{
21+
public function __construct(
22+
private Clock $clock,
23+
private Redis $redis,
24+
private SessionConfig $sessionConfig,
25+
) {}
26+
27+
public function create(SessionId $id): Session
28+
{
29+
return $this->persist($id);
30+
}
31+
32+
public function set(SessionId $id, string $key, mixed $value): void
33+
{
34+
$this->persist($id, [...$this->getData($id), ...[$key => $value]]);
35+
}
36+
37+
public function get(SessionId $id, string $key, mixed $default = null): mixed
38+
{
39+
return $this->getData($id)[$key] ?? $default;
40+
}
41+
42+
public function remove(SessionId $id, string $key): void
43+
{
44+
$data = $this->getData($id);
45+
46+
unset($data[$key]);
47+
48+
$this->persist($id, $data);
49+
}
50+
51+
public function destroy(SessionId $id): void
52+
{
53+
$this->redis->command('UNLINK', $this->getKey($id));
54+
55+
event(new SessionDestroyed($id));
56+
}
57+
58+
public function isValid(SessionId $id): bool
59+
{
60+
$session = $this->resolve($id);
61+
62+
if ($session === null) {
63+
return false;
64+
}
65+
66+
if (! ($session->lastActiveAt ?? null)) {
67+
return false;
68+
}
69+
70+
return $this->clock->now()->before(
71+
other: $session->lastActiveAt->plus($this->sessionConfig->expiration),
72+
);
73+
}
74+
75+
private function resolve(SessionId $id): ?Session
76+
{
77+
try {
78+
$content = $this->redis->get($this->getKey($id));
79+
return unserialize($content, ['allowed_classes' => true]);
80+
} catch (Throwable) {
81+
return null;
82+
}
83+
}
84+
85+
public function all(SessionId $id): array
86+
{
87+
return $this->getData($id);
88+
}
89+
90+
/**
91+
* @return array<mixed>
92+
*/
93+
private function getData(SessionId $id): array
94+
{
95+
return $this->resolve($id)->data ?? [];
96+
}
97+
98+
/**
99+
* @param array<mixed>|null $data
100+
*/
101+
private function persist(SessionId $id, ?array $data = null): Session
102+
{
103+
$now = $this->clock->now();
104+
$session = $this->resolve($id) ?? new Session(
105+
id: $id,
106+
createdAt: $now,
107+
lastActiveAt: $now,
108+
);
109+
110+
$session->lastActiveAt = $now;
111+
112+
if ($data !== null) {
113+
$session->data = $data;
114+
}
115+
116+
$this->redis->set($this->getKey($id), serialize($session), $this->sessionConfig->expiration);
117+
118+
return $session;
119+
}
120+
121+
private function getKey(SessionId $id): string
122+
{
123+
return sprintf('%s%s', $this->sessionConfig->prefix, $id);
124+
}
125+
126+
private function getSessionIdFromKey(string $key): SessionId
127+
{
128+
return new SessionId(
129+
new ImmutableString($key)
130+
->afterFirst($this->sessionConfig->prefix)
131+
->toString(),
132+
);
133+
}
134+
135+
public function cleanup(): void
136+
{
137+
$cursor = '0';
138+
139+
do {
140+
$result = $this->redis->command('SCAN', $cursor, 'MATCH', $this->getKey(new SessionId('*')), 'COUNT', '100');
141+
$cursor = $result[0];
142+
foreach ($result[1] as $key) {
143+
$sessionId = $this->getSessionIdFromKey($key);
144+
145+
if ($this->isValid($sessionId)) {
146+
continue;
147+
}
148+
149+
$this->destroy($sessionId);
150+
}
151+
} while ($cursor !== '0');
152+
}
153+
}

packages/reflection/src/PropertyReflector.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,15 @@ public function getIterableType(): ?TypeReflector
9898
return null;
9999
}
100100

101-
preg_match('/@var ([\\\\\w]+)\[]/', $doc, $match);
101+
if (preg_match('/@var\s+([\\\\\w]+)\[\]/', $doc, $match)) {
102+
return new TypeReflector(ltrim($match[1], '\\'));
103+
}
102104

103-
if (! isset($match[1])) {
104-
return null;
105+
if (preg_match('/@var\s+(?:list|array)<([\\\\\w]+)>/', $doc, $match)) {
106+
return new TypeReflector(ltrim($match[1], '\\'));
105107
}
106108

107-
return new TypeReflector(ltrim($match[1], '\\'));
109+
return null;
108110
}
109111

110112
public function isUninitialized(object $object): bool
@@ -151,7 +153,7 @@ public function hasDefaultValue(): bool
151153

152154
$hasDefaultValue = $this->reflectionProperty->hasDefaultValue();
153155

154-
$hasPromotedDefaultValue = $this->isPromoted() && $constructorParameters[$this->getName()]->isDefaultValueAvailable();
156+
$hasPromotedDefaultValue = $this->isPromoted() && isset($constructorParameters[$this->getName()]) && $constructorParameters[$this->getName()]->isDefaultValueAvailable();
155157

156158
return $hasDefaultValue || $hasPromotedDefaultValue;
157159
}

packages/reflection/src/TypeReflector.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use BackedEnum;
88
use DateTimeInterface;
9-
use Exception;
109
use Generator;
1110
use Iterator;
1211
use ReflectionClass as PHPReflectionClass;
@@ -228,7 +227,13 @@ private function resolveDefinition(PHPReflector|PHPReflectionType|string $reflec
228227
}
229228

230229
if ($reflector instanceof PHPReflectionParameter || $reflector instanceof PHPReflectionProperty) {
231-
return $this->resolveDefinition($reflector->getType());
230+
$type = $reflector->getType();
231+
232+
if ($type === null) {
233+
return 'mixed';
234+
}
235+
236+
return $this->resolveDefinition($type);
232237
}
233238

234239
if ($reflector instanceof PHPReflectionClass) {
@@ -253,7 +258,9 @@ private function resolveDefinition(PHPReflector|PHPReflectionType|string $reflec
253258
));
254259
}
255260

256-
throw new Exception('Could not resolve type');
261+
throw new \InvalidArgumentException(
262+
sprintf('Could not resolve type for reflector of type: %s', get_debug_type($reflector)),
263+
);
257264
}
258265

259266
private function resolveIsNullable(PHPReflectionType|PHPReflector|string $reflector): bool
@@ -263,7 +270,9 @@ private function resolveIsNullable(PHPReflectionType|PHPReflector|string $reflec
263270
}
264271

265272
if ($reflector instanceof PHPReflectionParameter || $reflector instanceof PHPReflectionProperty) {
266-
return $reflector->getType()->allowsNull();
273+
$type = $reflector->getType();
274+
275+
return $type === null || $type->allowsNull();
267276
}
268277

269278
if ($reflector instanceof PHPReflectionType) {

packages/router/src/RouteBindingInitializer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Con
3131
}
3232
}
3333

34+
if ($parameter === null) {
35+
throw new RouteBindingFailed();
36+
}
37+
3438
$object = $class->callStatic('resolve', $matchedRoute->params[$parameter->getName()]);
3539

3640
if ($object === null) {

packages/router/src/Routing/Construction/RoutingTree.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private function isOptionalSegment(string $segment): bool
5151

5252
private function stripOptionalMarker(string $segment): string
5353
{
54-
return str_replace('?', '', $segment);
54+
return preg_replace('/^\{\?/', '{', $segment);
5555
}
5656

5757
/** @return array<string, MatchingRegex> */

0 commit comments

Comments
 (0)