• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orchestral / testbench-core / 13198058225

07 Feb 2025 10:34AM UTC coverage: 92.08% (+0.4%) from 91.687%
13198058225

Pull #292

github

web-flow
Merge cab9eba4a into 432920503
Pull Request #292: Supports Parallel Testing on PHPUnit 12.0

1523 of 1654 relevant lines covered (92.08%)

74.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.37
/src/Workbench/Workbench.php
1
<?php
2

3
namespace Orchestra\Testbench\Workbench;
4

5
use Illuminate\Console\Application as Artisan;
6
use Illuminate\Console\Command;
7
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
8
use Illuminate\Database\Eloquent\Factories\Factory;
9
use Illuminate\Foundation\Events\DiagnosingHealth;
10
use Illuminate\Routing\Router;
11
use Illuminate\Support\Collection;
12
use Illuminate\Support\Facades\Event;
13
use Illuminate\Support\Facades\View;
14
use Illuminate\Support\Str;
15
use Orchestra\Testbench\Contracts\Config as ConfigContract;
16
use Orchestra\Testbench\Foundation\Config;
17
use Orchestra\Testbench\Foundation\Env;
18
use ReflectionClass;
19
use Symfony\Component\Finder\Finder;
20
use Throwable;
21

22
use function Orchestra\Testbench\after_resolving;
23
use function Orchestra\Testbench\join_paths;
24
use function Orchestra\Testbench\package_path;
25
use function Orchestra\Testbench\workbench_path;
26

27
/**
28
 * @api
29
 *
30
 * @phpstan-import-type TWorkbenchDiscoversConfig from \Orchestra\Testbench\Foundation\Config
31
 */
32
class Workbench
33
{
34
    /**
35
     * The cached test case configuration.
36
     *
37
     * @var \Orchestra\Testbench\Contracts\Config|null
38
     */
39
    protected static ?ConfigContract $cachedConfiguration = null;
40

41
    /**
42
     * Cached namespace by path.
43
     *
44
     * @var array<string, string|null>
45
     */
46
    protected static array $cachedNamespaces = [];
47

48
    /**
49
     * The cached test case configuration.
50
     *
51
     * @var class-string<\Illuminate\Foundation\Auth\User>|false|null
52
     */
53
    protected static string|false|null $cachedUserModel = null;
54

55
    /**
56
     * The cached core workbench bindings.
57
     *
58
     * @var array{kernel: array{console?: string|null, http?: string|null}, handler: array{exception?: string|null}}
59
     */
60
    public static array $cachedCoreBindings = [
61
        'kernel' => [],
62
        'handler' => [],
63
    ];
64

65
    /**
66
     * Start Workbench.
67
     *
68
     * @internal
69
     *
70
     * @param  \Illuminate\Contracts\Foundation\Application  $app
71
     * @param  \Orchestra\Testbench\Contracts\Config  $config
72
     * @param  array<int, string|class-string<\Illuminate\Support\ServiceProvider>>  $providers
73
     * @return void
74
     *
75
     * @codeCoverageIgnore
76
     */
77
    public static function start(ApplicationContract $app, ConfigContract $config, array $providers = []): void
78
    {
79
        $app->singleton(ConfigContract::class, static fn () => $config);
80

81
        Collection::make($providers)
82
            ->filter(static fn ($provider) => ! empty($provider) && class_exists($provider))
83
            ->each(static function ($provider) use ($app) {
84
                $app->register($provider);
85
            });
86
    }
87

88
    /**
89
     * Start Workbench with providers.
90
     *
91
     * @internal
92
     *
93
     * @param  \Illuminate\Contracts\Foundation\Application  $app
94
     * @param  \Orchestra\Testbench\Contracts\Config  $config
95
     * @return void
96
     *
97
     * @codeCoverageIgnore
98
     */
99
    public static function startWithProviders(ApplicationContract $app, ConfigContract $config): void
100
    {
101
        $hasAuthentication = $config->getWorkbenchAttributes()['auth'] ?? false;
102

103
        static::start($app, $config, array_filter([
104
            $hasAuthentication === true && class_exists('Orchestra\Workbench\AuthServiceProvider') ? 'Orchestra\Workbench\AuthServiceProvider' : null,
105
            'Orchestra\Workbench\WorkbenchServiceProvider',
106
        ]));
107
    }
108

109
    /**
110
     * Discover Workbench routes.
111
     *
112
     * @param  \Illuminate\Contracts\Foundation\Application  $app
113
     * @param  \Orchestra\Testbench\Contracts\Config  $config
114
     * @return void
115
     */
116
    public static function discoverRoutes(ApplicationContract $app, ConfigContract $config): void
117
    {
118
        /** @var TWorkbenchDiscoversConfig $discoversConfig */
119
        $discoversConfig = $config->getWorkbenchDiscoversAttributes();
44✔
120

121
        $healthCheckEnabled = $config->getWorkbenchAttributes()['health'] ?? false;
44✔
122

123
        $app->booted(static function ($app) use ($discoversConfig, $healthCheckEnabled) {
44✔
124
            tap($app->make('router'), static function (Router $router) use ($discoversConfig, $healthCheckEnabled) {
44✔
125
                if (($discoversConfig['api'] ?? false) === true) {
44✔
126
                    if (is_file($route = workbench_path('routes', 'api.php'))) {
44✔
127
                        $router->middleware('api')->group($route);
44✔
128
                    }
129
                }
130

131
                if ($healthCheckEnabled === true) {
44✔
132
                    $router->get('/up', static function () {
44✔
133
                        $exception = null;
1✔
134

135
                        try {
136
                            Event::dispatch(new DiagnosingHealth);
1✔
137
                        } catch (Throwable $error) {
×
138
                            if (app()->hasDebugModeEnabled()) {
×
139
                                throw $error;
×
140
                            }
141

142
                            report($error);
×
143

144
                            $exception = $error->getMessage();
×
145
                        }
146

147
                        return response(
1✔
148
                            View::file(
1✔
149
                                package_path('vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'health-up.blade.php'),
1✔
150
                                ['exception' => $exception],
1✔
151
                            ),
1✔
152
                            status: $exception ? 500 : 200,
1✔
153
                        );
1✔
154
                    });
44✔
155
                }
156

157
                if (($discoversConfig['web'] ?? false) === true) {
44✔
158
                    if (is_file($route = workbench_path('routes', 'web.php'))) {
44✔
159
                        $router->middleware('web')->group($route);
44✔
160
                    }
161
                }
162
            });
44✔
163

164
            if ($app->runningInConsole() && ($discoversConfig['commands'] ?? false) === true) {
44✔
165
                static::discoverCommandsRoutes($app);
44✔
166
            }
167
        });
44✔
168

169
        after_resolving($app, 'translator', static function ($translator) {
44✔
170
            /** @var \Illuminate\Contracts\Translation\Loader $translator */
171
            $path = Collection::make([
2✔
172
                workbench_path('lang'),
2✔
173
                workbench_path('resources', 'lang'),
2✔
174
            ])->filter(static fn ($path) => is_dir($path))
2✔
175
                ->first();
2✔
176

177
            if (\is_null($path)) {
2✔
178
                return;
×
179
            }
180

181
            $translator->addNamespace('workbench', $path);
2✔
182
        });
44✔
183

184
        if (is_dir($workbenchViewPath = workbench_path('resources', 'views'))) {
44✔
185
            if (($discoversConfig['views'] ?? false) === true) {
44✔
186
                $app->booted(static function () use ($app, $workbenchViewPath) {
44✔
187
                    tap($app->make('config'), function ($config) use ($workbenchViewPath) {
44✔
188
                        /** @var \Illuminate\Contracts\Config\Repository $config */
189
                        $config->set('view.paths', array_merge(
44✔
190
                            $config->get('view.paths', []),
44✔
191
                            [$workbenchViewPath]
44✔
192
                        ));
44✔
193
                    });
44✔
194
                });
44✔
195
            }
196

197
            after_resolving($app, 'view', static function ($view, $app) use ($discoversConfig, $workbenchViewPath) {
44✔
198
                /** @var \Illuminate\Contracts\View\Factory|\Illuminate\View\Factory $view */
199
                if (($discoversConfig['views'] ?? false) === true && method_exists($view, 'addLocation')) {
14✔
200
                    $view->addLocation($workbenchViewPath);
14✔
201
                }
202

203
                $view->addNamespace('workbench', $workbenchViewPath);
14✔
204
            });
44✔
205
        }
206

207
        after_resolving($app, 'blade.compiler', static function ($blade) use ($discoversConfig) {
44✔
208
            /** @var \Illuminate\View\Compilers\BladeCompiler $blade */
209
            if (($discoversConfig['components'] ?? false) === false && is_dir(workbench_path('app', 'View', 'Components'))) {
7✔
210
                $blade->componentNamespace('Workbench\\App\\View\\Components', 'workbench');
7✔
211
            }
212
        });
44✔
213

214
        if (($discoversConfig['factories'] ?? false) === true) {
44✔
215
            Factory::guessFactoryNamesUsing(static function ($modelName) {
44✔
216
                /** @var class-string<\Illuminate\Database\Eloquent\Model> $modelName */
217
                $workbenchNamespace = 'Workbench\\App\\';
2✔
218

219
                $modelBasename = str_starts_with($modelName, $workbenchNamespace.'Models\\')
2✔
220
                    ? Str::after($modelName, $workbenchNamespace.'Models\\')
1✔
221
                    : Str::after($modelName, $workbenchNamespace);
1✔
222

223
                /** @var class-string<\Illuminate\Database\Eloquent\Factories\Factory> $factoryName */
224
                $factoryName = 'Workbench\\Database\\Factories\\'.$modelBasename.'Factory';
2✔
225

226
                return $factoryName;
2✔
227
            });
44✔
228

229
            Factory::guessModelNamesUsing(static function ($factory) {
44✔
230
                /** @var \Illuminate\Database\Eloquent\Factories\Factory $factory */
231
                $workbenchNamespace = 'Workbench\\App\\';
1✔
232

233
                $namespacedFactoryBasename = Str::replaceLast(
1✔
234
                    'Factory', '', Str::replaceFirst('Workbench\\Database\\Factories\\', '', \get_class($factory))
1✔
235
                );
1✔
236

237
                $factoryBasename = Str::replaceLast('Factory', '', class_basename($factory));
1✔
238

239
                /** @var class-string<\Illuminate\Database\Eloquent\Model> $modelName */
240
                $modelName = class_exists($workbenchNamespace.'Models\\'.$namespacedFactoryBasename)
1✔
241
                    ? $workbenchNamespace.'Models\\'.$namespacedFactoryBasename
1✔
242
                    : $workbenchNamespace.$factoryBasename;
×
243

244
                return $modelName;
1✔
245
            });
44✔
246
        }
247
    }
248

249
    /**
250
     * Discover Workbench command routes.
251
     *
252
     * @param  \Illuminate\Contracts\Foundation\Application  $app
253
     * @return void
254
     */
255
    public static function discoverCommandsRoutes(ApplicationContract $app): void
256
    {
257
        if (is_file($console = workbench_path('routes', 'console.php'))) {
44✔
258
            require $console;
44✔
259
        }
260

261
        if (! is_dir(workbench_path('app', 'Console', 'Commands'))) {
44✔
262
            return;
×
263
        }
264

265
        $namespace = rtrim((static::detectNamespace('app') ?? 'Workbench\App\\'), '\\');
44✔
266

267
        foreach ((new Finder)->in([workbench_path('app', 'Console', 'Commands')])->files() as $command) {
44✔
268
            $command = $namespace.str_replace(
44✔
269
                ['/', '.php'],
44✔
270
                ['\\', ''],
44✔
271
                Str::after($command->getRealPath(), (string) realpath(workbench_path('app').DIRECTORY_SEPARATOR))
44✔
272
            );
44✔
273

274
            if (
275
                is_subclass_of($command, Command::class) &&
44✔
276
                ! (new ReflectionClass($command))->isAbstract()
44✔
277
            ) {
278
                Artisan::starting(static function ($artisan) use ($command) {
44✔
279
                    $artisan->resolve($command);
11✔
280
                });
44✔
281
            }
282
        }
283
    }
284

285
    /**
286
     * Resolve the configuration.
287
     *
288
     * @return \Orchestra\Testbench\Contracts\Config
289
     *
290
     * @codeCoverageIgnore
291
     */
292
    public static function configuration(): ConfigContract
293
    {
294
        if (\is_null(static::$cachedConfiguration)) {
295
            static::$cachedConfiguration = Config::cacheFromYaml(package_path());
296
        }
297

298
        return static::$cachedConfiguration;
299
    }
300

301
    /**
302
     * Get application Console Kernel implementation.
303
     *
304
     * @return string|null
305
     */
306
    public static function applicationConsoleKernel(): ?string
307
    {
308
        if (! isset(static::$cachedCoreBindings['kernel']['console'])) {
51✔
309
            static::$cachedCoreBindings['kernel']['console'] = is_file(workbench_path('app', 'Console', 'Kernel.php'))
51✔
310
                ? \sprintf('%sConsole\Kernel', static::detectNamespace('app'))
×
311
                : null;
51✔
312
        }
313

314
        return static::$cachedCoreBindings['kernel']['console'];
51✔
315
    }
316

317
    /**
318
     * Get application HTTP Kernel implementation using Workbench.
319
     *
320
     * @return string|null
321
     */
322
    public static function applicationHttpKernel(): ?string
323
    {
324
        if (! isset(static::$cachedCoreBindings['kernel']['http'])) {
51✔
325
            static::$cachedCoreBindings['kernel']['http'] = is_file(workbench_path('app', 'Http', 'Kernel.php'))
51✔
326
                ? \sprintf('%sHttp\Kernel', static::detectNamespace('app'))
×
327
                : null;
51✔
328
        }
329

330
        return static::$cachedCoreBindings['kernel']['http'];
51✔
331
    }
332

333
    /**
334
     * Get application HTTP exception handler using Workbench.
335
     *
336
     * @return string|null
337
     */
338
    public static function applicationExceptionHandler(): ?string
339
    {
340
        if (! isset(static::$cachedCoreBindings['handler']['exception'])) {
44✔
341
            static::$cachedCoreBindings['handler']['exception'] = is_file(workbench_path('app', 'Exceptions', 'Handler.php'))
44✔
342
                ? \sprintf('%sExceptions\Handler', static::detectNamespace('app'))
×
343
                : null;
44✔
344
        }
345

346
        return static::$cachedCoreBindings['handler']['exception'];
44✔
347
    }
348

349
    /**
350
     * Get application User Model
351
     *
352
     * @return class-string<\Illuminate\Foundation\Auth\User>|null
353
     */
354
    public static function applicationUserModel(): ?string
355
    {
356
        if (! isset(static::$cachedUserModel)) {
52✔
357
            static::$cachedUserModel = match (true) {
2✔
358
                Env::has('AUTH_MODEL') => Env::get('AUTH_MODEL'),
2✔
359
                is_file(workbench_path('app', 'Models', 'User.php')) => \sprintf('%sModels\User', static::detectNamespace('app')),
2✔
360
                is_file(base_path(join_paths('Models', 'User.php'))) => 'App\Models\User',
×
361
                default => false,
×
362
            };
2✔
363
        }
364

365
        return static::$cachedUserModel != false ? static::$cachedUserModel : null;
52✔
366
    }
367

368
    /**
369
     * Detect namespace by type.
370
     *
371
     * @param  string  $type
372
     * @param  bool  $force
373
     * @return string|null
374
     */
375
    public static function detectNamespace(string $type, bool $force = false): ?string
376
    {
377
        $type = trim($type, '/');
45✔
378

379
        if (! isset(static::$cachedNamespaces[$type]) || $force === true) {
45✔
380
            static::$cachedNamespaces[$type] = null;
2✔
381

382
            /** @var array{'autoload-dev': array{'psr-4': array<string, array<int, string>|string>}} $composer */
383
            $composer = json_decode((string) file_get_contents(package_path('composer.json')), true);
2✔
384

385
            $collection = $composer['autoload-dev']['psr-4'] ?? [];
2✔
386

387
            $path = implode('/', ['workbench', $type]);
2✔
388

389
            foreach ((array) $collection as $namespace => $paths) {
2✔
390
                foreach ((array) $paths as $pathChoice) {
2✔
391
                    if (trim($pathChoice, '/') === $path) {
2✔
392
                        static::$cachedNamespaces[$type] = $namespace;
2✔
393
                    }
394
                }
395
            }
396
        }
397

398
        $defaults = [
45✔
399
            'app' => 'Workbench\App\\',
45✔
400
            'database/factories' => 'Workbench\Database\Factories\\',
45✔
401
            'database/seeders' => 'Workbench\Database\Seeders\\',
45✔
402
        ];
45✔
403

404
        return static::$cachedNamespaces[$type] ?? $defaults[$type] ?? null;
45✔
405
    }
406

407
    /**
408
     * Flush the cached configuration.
409
     *
410
     * @return void
411
     *
412
     * @codeCoverageIgnore
413
     */
414
    public static function flush(): void
415
    {
416
        static::$cachedConfiguration = null;
417

418
        static::flushCachedClassAndNamespaces();
419
    }
420

421
    /**
422
     * Flush the cached namespace configuration.
423
     *
424
     * @return void
425
     *
426
     * @codeCoverageIgnore
427
     */
428
    public static function flushCachedClassAndNamespaces(): void
429
    {
430
        static::$cachedUserModel = null;
431
        static::$cachedNamespaces = [];
432

433
        static::$cachedCoreBindings = [
434
            'kernel' => [],
435
            'handler' => [],
436
        ];
437
    }
438
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc