Skip to content

Commit 4be25d0

Browse files
committed
chore: Add mapper discovery
1 parent e97ddff commit 4be25d0

File tree

6 files changed

+265
-20
lines changed

6 files changed

+265
-20
lines changed

src/ConciseServiceProvider.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
namespace Articulate\Concise;
55

66
use Articulate\Concise\Support\ImplicitBindingSubstitution;
7+
use Articulate\Concise\Support\MapperDiscovery;
8+
use Illuminate\Foundation\Application;
79
use Illuminate\Routing\Router;
810
use Illuminate\Support\ServiceProvider;
911

@@ -18,5 +20,14 @@ public function register(): void
1820
$this->app->afterResolving(Router::class, function (Router $router) {
1921
$router->substituteImplicitBindingsUsing($this->app->make(ImplicitBindingSubstitution::class));
2022
});
23+
24+
$this->app->booted(function (Application $app) {
25+
$mappers = MapperDiscovery::discover();
26+
$concise = $app->make(Concise::class);
27+
28+
foreach ($mappers as $mapper) {
29+
$concise->register($mapper);
30+
}
31+
});
2132
}
2233
}

src/Support/MapperDiscovery.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Articulate\Concise\Support;
5+
6+
use Articulate\Concise\Contracts\Mapper;
7+
use Illuminate\Support\Str;
8+
use InvalidArgumentException;
9+
use ReflectionClass;
10+
use ReflectionException;
11+
use SplFileInfo;
12+
use Symfony\Component\Finder\Finder;
13+
14+
/**
15+
*
16+
*/
17+
final class MapperDiscovery
18+
{
19+
private static bool $discover = true;
20+
21+
/**
22+
* Mapped paths to namespaces
23+
*
24+
* @var array<string, string>
25+
*/
26+
private static array $pathMappings = [];
27+
28+
/**
29+
* @var bool
30+
*/
31+
private static bool $useDefaults = true;
32+
33+
/**
34+
* @param string $path
35+
* @param string|null $namespace
36+
*
37+
* @return void
38+
*/
39+
public static function addPath(string $path, ?string $namespace = null): void
40+
{
41+
if (Str::startsWith($path, app_path())) {
42+
self::$pathMappings[Str::rtrim($path, DIRECTORY_SEPARATOR)] = $namespace ?? app()->getNamespace();
43+
} else {
44+
if ($namespace === null) {
45+
throw new InvalidArgumentException('Paths outside the app path must have a namespace.');
46+
}
47+
48+
self::$pathMappings[Str::rtrim($path, DIRECTORY_SEPARATOR)] = $namespace;
49+
}
50+
}
51+
52+
/**
53+
* @return void
54+
*/
55+
public static function withDefaults(): void
56+
{
57+
self::$useDefaults = true;
58+
}
59+
60+
public static function noDiscovery(): void
61+
{
62+
self::$discover = false;
63+
}
64+
65+
public static function withDiscovery(): void
66+
{
67+
self::$discover = true;
68+
}
69+
70+
/**
71+
* @return void
72+
*/
73+
public static function noDefaults(): void
74+
{
75+
self::$useDefaults = false;
76+
}
77+
78+
public static function reset(): void
79+
{
80+
self::$useDefaults = true;
81+
self::$pathMappings = [];
82+
}
83+
84+
/**
85+
* @return array<string, string>
86+
*/
87+
public static function getDefaultPaths(): array
88+
{
89+
return [
90+
app_path('Mappers/Entities') => app()->getNamespace() . 'Mappers\\Entities\\',
91+
app_path('Mappers/Components') => app()->getNamespace() . 'Mappers\\Components\\',
92+
];
93+
}
94+
95+
/**
96+
* @return array<string, string>
97+
*/
98+
public static function getPaths(): array
99+
{
100+
return array_merge(
101+
self::$useDefaults ? self::getDefaultPaths() : [],
102+
self::$pathMappings
103+
);
104+
}
105+
106+
/**
107+
* @return array<class-string<\Articulate\Concise\Contracts\Mapper<*>>>
108+
*/
109+
public static function discover(): array
110+
{
111+
if (! self::$discover) {
112+
return [];
113+
}
114+
115+
$mappers = [];
116+
$paths = self::getPaths();
117+
118+
foreach ($paths as $path => $namespace) {
119+
self::discoverMappers($path, $namespace, $mappers);
120+
}
121+
122+
return $mappers;
123+
}
124+
125+
/**
126+
* @param string $path
127+
* @param string $namespace
128+
* @param array<class-string<\Articulate\Concise\Contracts\Mapper<*>>> $mappers
129+
*
130+
* @return void
131+
*/
132+
private static function discoverMappers(string $path, string $namespace, array &$mappers): void
133+
{
134+
$files = Finder::create()->files()->in($path);
135+
136+
foreach ($files as $file) {
137+
try {
138+
$reflector = new ReflectionClass(self::classFromFile($file, $path, $namespace));
139+
} catch (ReflectionException) {
140+
continue;
141+
}
142+
143+
if (! $reflector->isInstantiable()) {
144+
continue;
145+
}
146+
147+
if (! $reflector->isSubclassOf(Mapper::class)) {
148+
continue;
149+
}
150+
151+
/** @var class-string<\Articulate\Concise\Contracts\Mapper<*>> $class */
152+
$class = $reflector->getName();
153+
154+
$mappers[] = $class;
155+
}
156+
}
157+
158+
/**
159+
* Extract the class name from the given file path.
160+
*
161+
* @param \SplFileInfo $file
162+
* @param string $path
163+
* @param string $namespace
164+
*
165+
* @return class-string
166+
*/
167+
protected static function classFromFile(SplFileInfo $file, string $path, string $namespace): string
168+
{
169+
$class = trim(Str::replaceFirst($path, '', $file->getRealPath()), DIRECTORY_SEPARATOR);
170+
171+
/** @var class-string */
172+
return $namespace . ucfirst(Str::camel(str_replace(
173+
[DIRECTORY_SEPARATOR, $path . '\\'],
174+
['\\', $namespace],
175+
ucfirst(Str::replaceLast('.php', '', $class))
176+
)));
177+
}
178+
}

tests/ConciseTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111
use App\Mappers\Entities\UserMapper;
1212
use Articulate\Concise\Concise;
1313
use Articulate\Concise\EntityRepository;
14+
use Articulate\Concise\Support\MapperDiscovery;
1415
use InvalidArgumentException;
16+
use Orchestra\Testbench\Concerns\WithWorkbench;
1517
use PHPUnit\Framework\Attributes\Test;
1618
use ReflectionClass;
1719
use stdClass;
1820

1921
class ConciseTest extends TestCase
2022
{
23+
use WithWorkbench;
24+
2125
#[Test]
2226
public function canManuallyRegisterEntityMappers(): void
2327
{

tests/MapperDiscoveryTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Articulate\Concise\Tests;
5+
6+
use Articulate\Concise\Support\MapperDiscovery;
7+
use Orchestra\Testbench\Concerns\WithWorkbench;
8+
use PHPUnit\Framework\Attributes\Test;
9+
10+
class MapperDiscoveryTest extends TestCase
11+
{
12+
use WithWorkbench;
13+
14+
protected function defineEnvironment($app): void
15+
{
16+
MapperDiscovery::noDiscovery();
17+
}
18+
19+
#[Test]
20+
public function hasDefaultPaths(): void
21+
{
22+
MapperDiscovery::reset();
23+
$paths = MapperDiscovery::getPaths();
24+
25+
$this->assertArrayHasKey(app_path('Mappers/Entities'), $paths);
26+
$this->assertArrayHasKey(app_path('Mappers/Components'), $paths);
27+
$this->assertSame('App\\Mappers\\Entities\\', array_values($paths)[0]);
28+
$this->assertSame('App\\Mappers\\Components\\', array_values($paths)[1]);
29+
}
30+
31+
#[Test]
32+
public function canHaveDefaultPathsDisabled(): void
33+
{
34+
MapperDiscovery::reset();
35+
MapperDiscovery::noDefaults();
36+
37+
$this->assertEmpty(MapperDiscovery::getPaths());
38+
}
39+
40+
#[Test]
41+
public function canAddCustomPaths(): void
42+
{
43+
MapperDiscovery::reset();
44+
MapperDiscovery::noDefaults();
45+
MapperDiscovery::addPath(base_path('workbench/app/Mappers/Entities'), 'App\\Mappers\\Entities\\');
46+
MapperDiscovery::addPath(base_path('workbench/app/Mappers/Components'), 'App\\Mappers\\Components\\');
47+
48+
$paths = MapperDiscovery::getPaths();
49+
50+
$this->assertArrayHasKey(base_path('workbench/app/Mappers/Entities'), $paths);
51+
$this->assertArrayHasKey(base_path('workbench/app/Mappers/Components'), $paths);
52+
$this->assertSame('App\\Mappers\\Entities\\', array_values($paths)[0]);
53+
$this->assertSame('App\\Mappers\\Components\\', array_values($paths)[1]);
54+
}
55+
56+
#[Test]
57+
public function canResolveMappers(): void
58+
{
59+
MapperDiscovery::reset();
60+
MapperDiscovery::noDefaults();
61+
MapperDiscovery::withDiscovery();
62+
MapperDiscovery::addPath(realpath(__DIR__ . '/../workbench/app/Mappers/Entities'), 'App\\Mappers\\Entities\\');
63+
MapperDiscovery::addPath(realpath(__DIR__ . '/../workbench/app/Mappers/Components'), 'App\\Mappers\\Components\\');
64+
65+
$mappers = MapperDiscovery::discover();
66+
67+
$this->assertNotEmpty($mappers);
68+
$this->assertCount(3, $mappers);
69+
}
70+
}

workbench/app/Providers/MapperServiceProvider.php

Lines changed: 0 additions & 19 deletions
This file was deleted.

workbench/app/Providers/WorkbenchServiceProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Providers;
44

5+
use Articulate\Concise\Support\MapperDiscovery;
56
use Illuminate\Support\ServiceProvider;
67

78
class WorkbenchServiceProvider extends ServiceProvider
@@ -11,7 +12,7 @@ class WorkbenchServiceProvider extends ServiceProvider
1112
*/
1213
public function register(): void
1314
{
14-
//
15+
MapperDiscovery::noDefaults();
1516
}
1617

1718
/**

0 commit comments

Comments
 (0)