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

orchestral / testbench-core / 20293198810

17 Dec 2025 05:56AM UTC coverage: 91.517% (-0.5%) from 92.055%
20293198810

push

github

crynobone
Merge branch '10.x' into 11.x

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>

1532 of 1674 relevant lines covered (91.52%)

76.78 hits per line

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

84.06
/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\Sidekick\Env;
16
use Orchestra\Testbench\Contracts\Config as ConfigContract;
17
use Orchestra\Testbench\Foundation\Config;
18
use ReflectionClass;
19
use Symfony\Component\Finder\Finder;
20
use Throwable;
21

22
use function Orchestra\Sidekick\join_paths;
23
use function Orchestra\Testbench\after_resolving;
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
        (new Collection($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 ? '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;
×
134

135
                        try {
136
                            Event::dispatch(new DiagnosingHealth);
×
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(
×
148
                            View::file(
×
149
                                package_path('vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'health-up.blade.php'),
×
150
                                ['exception' => $exception],
×
151
                            ),
×
152
                            status: $exception ? 500 : 200,
×
153
                        );
×
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 = (new Collection([
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'))) {
6✔
210
                $blade->componentNamespace('Workbench\\App\\View\\Components', 'workbench');
6✔
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 = static::detectNamespace('app') ?? 'Workbench\\App\\';
2✔
218
                $factoryNamespace = static::detectNamespace('database/factories') ?? 'Workbench\\Database\\Factories\\';
2✔
219

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

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

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

230
            Factory::guessModelNamesUsing(static function ($factory) {
44✔
231
                /** @var \Illuminate\Database\Eloquent\Factories\Factory $factory */
232
                $workbenchNamespace = static::detectNamespace('app') ?? 'Workbench\\App\\';
1✔
233
                $factoryNamespace = static::detectNamespace('database/factories') ?? 'Workbench\\Database\\Factories\\';
1✔
234

235
                $namespacedFactoryBasename = Str::replaceLast(
1✔
236
                    'Factory', '', Str::replaceFirst($factoryNamespace, '', $factory::class)
1✔
237
                );
1✔
238

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

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

246
                return $modelName;
1✔
247
            });
44✔
248
        }
249
    }
250

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

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

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

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

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

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

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

312
        return static::$cachedCoreBindings['kernel']['console'];
60✔
313
    }
314

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

328
        return static::$cachedCoreBindings['kernel']['http'];
60✔
329
    }
330

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

344
        return static::$cachedCoreBindings['handler']['exception'];
44✔
345
    }
346

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

363
            static::$cachedUserModel = $userModel;
2✔
364
        }
365

366
        return static::$cachedUserModel != false ? static::$cachedUserModel : null;
61✔
367
    }
368

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

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

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

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

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

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

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

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

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

419
        static::flushCachedClassAndNamespaces();
420
    }
421

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

434
        static::$cachedCoreBindings = [
435
            'kernel' => [],
436
            'handler' => [],
437
        ];
438
    }
439
}
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