Skip to content

Commit b0031b5

Browse files
committed
feat: allow object to define how they are mapped to array (#532)
1 parent d91cf0a commit b0031b5

13 files changed

+209
-37
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "tempest/core",
3+
"description": "The core component of the Tempest framework.",
34
"license": "MIT",
45
"minimum-stability": "dev",
56
"require": {
67
"php": "^8.3",
7-
"tempest/container": "self.version",
8+
"tempest/container": "dev-main",
89
"vlucas/phpdotenv": "^5.6",
9-
"spatie/ignition": "^1.15",
10+
"filp/whoops": "^2.15",
1011
"nunomaduro/collision": "^8.4"
1112
},
1213
"autoload": {

phpunit.xml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
4-
bootstrap="vendor/autoload.php"
5-
executionOrder="depends,defects"
6-
beStrictAboutOutputDuringTests="true"
7-
displayDetailsOnPhpunitDeprecations="true"
8-
failOnPhpunitDeprecation="true"
9-
failOnRisky="true"
10-
failOnWarning="true">
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd" bootstrap="vendor/autoload.php" executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" displayDetailsOnPhpunitDeprecations="true" failOnPhpunitDeprecation="false" failOnRisky="true" failOnWarning="true">
113
<testsuites>
124
<testsuite name="Tempest Core">
135
<directory>tests</directory>
146
</testsuite>
157
</testsuites>
16-
17-
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
8+
<source restrictNotices="true" restrictWarnings="true" ignoreIndirectDeprecations="true">
189
<include>
1910
<directory>src</directory>
2011
</include>

src/AppConfig.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,24 @@
66

77
final class AppConfig
88
{
9+
public Environment $environment;
10+
11+
public string $baseUri;
12+
913
public function __construct(
10-
public Environment $environment = Environment::LOCAL,
14+
?Environment $environment = null,
15+
?string $baseUri = null,
1116
public ExceptionHandlerSetup $exceptionHandlerSetup = new GenericExceptionHandlerSetup(),
1217

1318
/** @var \Tempest\Core\ExceptionHandler[] */
1419
public array $exceptionHandlers = [
1520
// …,
1621
],
1722
) {
23+
$this->environment = $environment
24+
?? Environment::tryFrom(\Tempest\env('ENVIRONMENT', 'local'))
25+
?? Environment::LOCAL;
26+
27+
$this->baseUri = $baseUri ?? \Tempest\env('BASE_URI') ?? '';
1828
}
1929
}

src/Composer.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Core;
6+
7+
use function Tempest\Support\arr;
8+
use Tempest\Support\PathHelper;
9+
10+
final readonly class Composer
11+
{
12+
/** @var array<ComposerNamespace> */
13+
public array $namespaces;
14+
15+
public ?ComposerNamespace $mainNamespace;
16+
17+
public array $composer;
18+
19+
public function __construct(
20+
string $root,
21+
) {
22+
$composerFilePath = PathHelper::make($root, 'composer.json');
23+
24+
$this->composer = $this->loadComposerFile($composerFilePath);
25+
$this->namespaces = arr($this->composer)
26+
->get('autoload.psr-4', default: arr())
27+
->map(fn (string $path, string $namespace) => new ComposerNamespace($namespace, $path))
28+
->values()
29+
->toArray();
30+
31+
foreach ($this->namespaces as $namespace) {
32+
if (str_starts_with($namespace->path, 'app/') || str_starts_with($namespace->path, 'src/')) {
33+
$this->mainNamespace = $namespace;
34+
35+
break;
36+
}
37+
}
38+
39+
if (! isset($this->mainNamespace) && count($this->namespaces)) {
40+
$this->mainNamespace = $this->namespaces[0];
41+
}
42+
43+
if (! isset($this->mainNamespace)) {
44+
throw new KernelException("Tempest requires at least one PSR-4 namespace to be defined in composer.json.");
45+
}
46+
}
47+
48+
private function loadComposerFile(string $path): array
49+
{
50+
if (! file_exists($path)) {
51+
throw new KernelException("Could not locate composer.json.");
52+
}
53+
54+
return json_decode(file_get_contents($path), associative: true);
55+
}
56+
}

src/ComposerNamespace.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Core;
6+
7+
final readonly class ComposerNamespace
8+
{
9+
public function __construct(
10+
public string $namespace,
11+
public string $path,
12+
) {
13+
}
14+
}

src/DiscoveryDiscovery.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
final readonly class DiscoveryDiscovery implements Discovery
1111
{
12-
public const string CACHE_PATH = __DIR__ . '/../../../.cache/tempest/discovery-discovery.cache.php';
12+
public const string CACHE_PATH = __DIR__ . '/../../../../.cache/tempest/discovery-discovery.cache.php';
1313

1414
public function __construct(
1515
private Kernel $kernel,

src/DoNotDiscover.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Core;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
final class DoNotDiscover
11+
{
12+
public function __construct(
13+
/**
14+
* Allows the specified `Discovery` classes to still discover this class.
15+
* @var array<class-string<\Tempest\Core\Discovery>>
16+
*/
17+
public readonly array $except = [],
18+
) {
19+
}
20+
}

src/GenericExceptionHandlerSetup.php

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,72 @@
55
namespace Tempest\Core;
66

77
use NunoMaduro\Collision\Provider as Collision;
8-
use Spatie\Ignition\Ignition;
8+
use Throwable;
9+
use Whoops\Handler\PrettyPageHandler;
10+
use Whoops\Run;
911

1012
final readonly class GenericExceptionHandlerSetup implements ExceptionHandlerSetup
1113
{
1214
public function setup(AppConfig $appConfig): void
1315
{
16+
if ($appConfig->environment->isTesting()) {
17+
return;
18+
}
19+
1420
// Console
15-
if ($_SERVER['argv'] ?? null) {
21+
if (PHP_SAPI === 'cli') {
1622
(new Collision())->register();
1723

1824
return;
1925
}
2026

2127
// Production web
2228
if ($appConfig->environment->isProduction()) {
23-
set_exception_handler($this->renderErrorPage(...));
24-
/** @phpstan-ignore-next-line */
29+
set_exception_handler($this->renderExceptionPage(...));
30+
/** @phpstan-ignore-next-line */
2531
set_error_handler($this->renderErrorPage(...));
2632

2733
return;
2834
}
2935

30-
// Production dev
31-
Ignition::make()->register();
36+
// Local web
37+
$whoops = new Run();
38+
$whoops->pushHandler(new PrettyPageHandler());
39+
$whoops->register();
3240
}
3341

34-
public function renderErrorPage(): void
42+
public function renderExceptionPage(Throwable $throwable): void
3543
{
44+
ll($throwable);
45+
46+
ob_start();
47+
48+
if (! headers_sent()) {
49+
http_response_code(500);
50+
}
51+
52+
echo file_get_contents(__DIR__ . '/500.html');
53+
54+
ob_end_flush();
55+
56+
exit;
57+
}
58+
59+
public function renderErrorPage(
60+
int $errNo,
61+
string $errstr,
62+
string $errFile,
63+
int $errLine,
64+
): void {
65+
ll("{$errFile}:{$errLine} {$errstr} ({$errNo})");
66+
67+
if (
68+
$errNo === E_USER_WARNING
69+
|| $errNo === E_DEPRECATED
70+
) {
71+
return;
72+
}
73+
3674
ob_start();
3775

3876
if (! headers_sent()) {

src/HandlesDiscoveryCache.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function getCachePath(): string
1515

1616
$name = array_pop($parts) . '.cache.php';
1717

18-
return __DIR__ . '/../../../.cache/tempest/' . $name;
18+
return __DIR__ . '/../../../../.cache/tempest/' . $name;
1919
}
2020

2121
abstract public function createCachePayload(): string;

src/Kernel.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ public function __construct(
3636
->setDiscoveryCache($discoveryCache)
3737
->registerShutdownFunction()
3838
->registerKernel()
39+
->loadComposer()
3940
->loadDiscoveryLocations()
4041
->loadConfig()
41-
->loadDiscovery();
42-
43-
$this->container->get(EventBus::class)->dispatch(KernelEvent::BOOTED);
42+
->loadExceptionHandler()
43+
->loadDiscovery()
44+
->event(KernelEvent::BOOTED);
4445
}
4546

4647
public static function boot(string $root, ?Container $container = null): self
@@ -62,6 +63,13 @@ private function createContainer(): Container
6263
return $container;
6364
}
6465

66+
private function loadComposer(): self
67+
{
68+
$this->container->singleton(Composer::class, new Composer($this->root));
69+
70+
return $this;
71+
}
72+
6573
private function loadEnv(): self
6674
{
6775
$dotenv = Dotenv::createUnsafeImmutable($this->root);
@@ -126,4 +134,22 @@ private function loadConfig(): self
126134

127135
return $this;
128136
}
137+
138+
private function loadExceptionHandler(): self
139+
{
140+
$appConfig = $this->container->get(AppConfig::class);
141+
142+
$appConfig->exceptionHandlerSetup->setup($appConfig);
143+
144+
return $this;
145+
}
146+
147+
private function event(object $event): self
148+
{
149+
if (interface_exists(EventBus::class)) {
150+
$this->container->get(EventBus::class)->dispatch($event);
151+
}
152+
153+
return $this;
154+
}
129155
}

src/Kernel/LoadDiscoveryClasses.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
use FilesystemIterator;
88
use RecursiveDirectoryIterator;
99
use RecursiveIteratorIterator;
10+
use ReflectionException;
1011
use SplFileInfo;
1112
use Tempest\Container\Container;
1213
use Tempest\Core\DiscoversPath;
1314
use Tempest\Core\Discovery;
15+
use Tempest\Core\DoNotDiscover;
1416
use Tempest\Core\Kernel;
1517
use Tempest\Reflection\ClassReflector;
1618
use Throwable;
@@ -32,11 +34,15 @@ public function __invoke(): void
3234
/** @var Discovery $discovery */
3335
$discovery = $this->container->get($discoveryClass);
3436

35-
if ($this->kernel->discoveryCache && $discovery->hasCache()) {
36-
$discovery->restoreCache($this->container);
37-
next($this->kernel->discoveryClasses);
37+
try {
38+
if ($this->kernel->discoveryCache && $discovery->hasCache()) {
39+
$discovery->restoreCache($this->container);
40+
next($this->kernel->discoveryClasses);
3841

39-
continue;
42+
continue;
43+
}
44+
} catch (ReflectionException) {
45+
// Invalid cache
4046
}
4147

4248
foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
@@ -90,7 +96,9 @@ public function __invoke(): void
9096
}
9197

9298
if ($input instanceof ClassReflector) {
93-
$discovery->discover($input);
99+
if ($this->shouldDiscover($discovery, $input)) {
100+
$discovery->discover($input);
101+
}
94102
} elseif ($discovery instanceof DiscoversPath) {
95103
$discovery->discoverPath($input);
96104
}
@@ -104,4 +112,13 @@ public function __invoke(): void
104112
}
105113
}
106114
}
115+
116+
private function shouldDiscover(Discovery $discovery, ClassReflector $input): bool
117+
{
118+
if (is_null($attribute = $input->getAttribute(DoNotDiscover::class))) {
119+
return true;
120+
}
121+
122+
return in_array($discovery::class, $attribute->except, strict: true);
123+
}
107124
}

0 commit comments

Comments
 (0)