Skip to content

Commit 396f756

Browse files
authored
feat(discovery): skip discovery with closure (#2070)
1 parent 3ebf23c commit 396f756

10 files changed

Lines changed: 139 additions & 5 deletions

File tree

docs/1-essentials/05-discovery.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,44 @@ new BootDiscovery(
306306
)();
307307
```
308308

309+
You can also mark classes themselves to be skipped entirely by discovery:
310+
311+
```php
312+
use Tempest\Discovery\SkipDiscovery;
313+
314+
#[SkipDiscovery]
315+
final readonly class CautionMiddleware implements ConsoleMiddleware
316+
{
317+
// …
318+
}
319+
```
320+
321+
Furthermore, you can skip discovery entirely for a specific class, expect for a specific set of discovery classes:
322+
323+
```php
324+
use Tempest\Discovery\SkipDiscovery;
325+
326+
#[SkipDiscovery(except: [MigrationDiscovery::class])]
327+
final class HiddenMigratableDatabaseMigration implements MigratesUp
328+
{
329+
// …
330+
}
331+
```
332+
333+
Finally, you can pass in a callable to this `$except` parameter as well, which allows access to the container, and gives you even more flexibility on when a class should be skipped or not:
334+
335+
```php
336+
use Tempest\Discovery\SkipDiscovery;
337+
use Tempest\Container\Container;
338+
339+
#[SkipDiscovery(static function (Container $container): bool {
340+
return ! $container->get(Application::class) instanceof ConsoleApplication;
341+
})]
342+
final class BlogPostEventHandlers {
343+
// …
344+
}
345+
```
346+
309347
### Caching discovery
310348

311349
By default, discovery is not cached, meaning all configured discovery locations are scanned on every request. This is fine for development, but in production, it's recommended to cache discovery to remove any performance overhead.

packages/database/src/QueryStatements/CreateTableStatement.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ public function text(string $name, bool $nullable = false, int|DatabaseTextLengt
149149
$this->statements[] = new TextStatement(
150150
name: $name,
151151
nullable: $nullable,
152-
default: $default,
153152
length: $length,
153+
default: $default,
154154
);
155155

156156
return $this;

packages/discovery/src/BootDiscovery.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\Discovery;
66

77
use AssertionError;
8+
use Closure;
89
use Pest\Exceptions\InvalidPestCommand;
910
use Psr\Container\ContainerInterface;
1011
use Tempest\Container\GenericContainer;
@@ -214,6 +215,8 @@ private function discoverPath(string $input, DiscoveryLocation $location, array
214215

215216
if ($skipDiscovery !== null && $skipDiscovery->except === []) {
216217
$this->shouldSkipForClass[$resolvedClassName] = true;
218+
} elseif ($skipDiscovery !== null && $skipDiscovery->except instanceof Closure && ($skipDiscovery->except)($this->container)) {
219+
$this->shouldSkipForClass[$resolvedClassName] = true;
217220
} elseif ($skipDiscovery !== null) {
218221
foreach ($skipDiscovery->except as $except) {
219222
$this->shouldSkipForClass[$resolvedClassName][$except] = true;

packages/discovery/src/SkipDiscovery.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\Discovery;
66

77
use Attribute;
8+
use Closure;
89

910
/**
1011
* Instruct Tempest to not discover this class.
@@ -15,8 +16,9 @@
1516
public function __construct(
1617
/**
1718
* Allows the specified `Discovery` classes to still discover this class.
18-
* @var array<class-string<\Tempest\Discovery\Discovery>>
19+
* Pass a closure to dynamically determine if the class should be discovered.
20+
* @var array<class-string<\Tempest\Discovery\Discovery>>|Closure(\Tempest\Container\Container|\Psr\Container\ContainerInterface $container): bool
1921
*/
20-
public array $except = [],
22+
public Closure|array $except = [],
2123
) {}
2224
}

packages/discovery/tests/DiscoveryTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Tempest\Discovery\DiscoveryConfig;
1010
use Tempest\Discovery\DiscoveryLocation;
1111
use Tempest\Discovery\Tests\Fixtures\ContainerWithoutAutowiring;
12+
use Tempest\Discovery\Tests\Fixtures\DependencyForItemWithClosureSkip;
13+
use Tempest\Discovery\Tests\Fixtures\DiscoveryForItemWithClosureSkip;
1214
use Tempest\Discovery\Tests\Fixtures\MyDiscoveryClass;
1315

1416
final class DiscoveryTest extends TestCase
@@ -38,6 +40,38 @@ public function test_standalone_discovery(): void
3840
$this->assertSame('check', MyDiscoveryClass::$discoveredItem->name);
3941
}
4042

43+
public function test_skip_discovery_with_closure(): void
44+
{
45+
$container = new GenericContainer();
46+
47+
$bootDiscovery = fn () => (new BootDiscovery(
48+
container: $container,
49+
config: new DiscoveryConfig(),
50+
))(
51+
discoveryClasses: [
52+
DiscoveryForItemWithClosureSkip::class,
53+
],
54+
discoveryLocations: [
55+
new DiscoveryLocation(
56+
namespace: 'Tempest\Discovery\Tests\Fixtures',
57+
path: __DIR__ . '/Fixtures',
58+
),
59+
],
60+
);
61+
62+
// Should be skipped
63+
DiscoveryForItemWithClosureSkip::$discovered = false;
64+
$container->singleton(DependencyForItemWithClosureSkip::class, new DependencyForItemWithClosureSkip(true));
65+
$bootDiscovery();
66+
$this->assertFalse(DiscoveryForItemWithClosureSkip::$discovered);
67+
68+
// Should not be skipped
69+
DiscoveryForItemWithClosureSkip::$discovered = false;
70+
$container->singleton(DependencyForItemWithClosureSkip::class, new DependencyForItemWithClosureSkip(false));
71+
$bootDiscovery();
72+
$this->assertTrue(DiscoveryForItemWithClosureSkip::$discovered);
73+
}
74+
4175
public function test_discovery_with_other_container(): void
4276
{
4377
$container = new Container();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Tempest\Discovery\Tests\Fixtures;
4+
5+
final class DependencyForItemWithClosureSkip
6+
{
7+
public function __construct(
8+
public bool $shouldSkip,
9+
) {}
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Tempest\Discovery\Tests\Fixtures;
4+
5+
use Tempest\Discovery\Discovery;
6+
use Tempest\Discovery\DiscoveryLocation;
7+
use Tempest\Discovery\IsDiscovery;
8+
use Tempest\Reflection\ClassReflector;
9+
10+
final class DiscoveryForItemWithClosureSkip implements Discovery
11+
{
12+
use IsDiscovery;
13+
14+
public static bool $discovered = false;
15+
16+
public function discover(DiscoveryLocation $location, ClassReflector $class): void
17+
{
18+
if ($class->is(ItemWithClosureSkip::class)) {
19+
self::$discovered = true;
20+
}
21+
}
22+
23+
public function apply(): void
24+
{
25+
// TODO: Implement apply() method.
26+
}
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Discovery\Tests\Fixtures;
4+
5+
use Psr\Container\ContainerInterface;
6+
use Tempest\Discovery\SkipDiscovery;
7+
use Throwable;
8+
9+
#[SkipDiscovery(static function (ContainerInterface $container): bool {
10+
if (! $container->has(DependencyForItemWithClosureSkip::class)) {
11+
return true;
12+
}
13+
14+
try {
15+
return $container->get(DependencyForItemWithClosureSkip::class)->shouldSkip;
16+
} catch (Throwable) {
17+
return true;
18+
}
19+
})]
20+
final class ItemWithClosureSkip {}

packages/view/src/Attributes/AsAttribute.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function apply(Element $element): Element
2828

2929
$generic = $element->unwrap(GenericElement::class);
3030

31-
if ($generic === null) {
31+
if (! $generic instanceof Element) {
3232
return $element;
3333
}
3434

packages/view/src/Parser/TempestViewCompiler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ private function sourceLineMarker(int $sourceLine): string
235235
/** @return array{sourcePath: string|null, sourceLine: int}|null */
236236
private function resolveSourceLocation(Element $element): ?array
237237
{
238-
if ($element instanceof TextElement && $element->token !== null) {
238+
if ($element instanceof TextElement && $element->token instanceof Token) {
239239
return [
240240
'sourcePath' => $element->token->sourcePath,
241241
'sourceLine' => $element->token->line,

0 commit comments

Comments
 (0)