From e0695a13cbbe9e9d0953808e9126b24db75891d5 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 21 Jul 2025 13:25:50 +0100 Subject: [PATCH] feat: adds shell --- composer.json | 3 +- src/Concerns/Testable.php | 9 ++++ src/Support/Shell.php | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/Support/Shell.php diff --git a/composer.json b/composer.json index 26d92d49..55bd94ee 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,8 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0" + "pestphp/pest-plugin-type-coverage": "^4.0.0", + "psy/psysh": "^0.12.9" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index f233b6ac..326115ea 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -10,6 +10,7 @@ use Pest\Preset; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\Support\Reflection; +use Pest\Support\Shell; use Pest\TestSuite; use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\TestCase; @@ -477,4 +478,12 @@ trait Testable 'notes' => self::$__latestNotes, ]; } + + /** + * Opens a shell for the test case. + */ + public function shell(): void + { + Shell::open(); + } } diff --git a/src/Support/Shell.php b/src/Support/Shell.php new file mode 100644 index 00000000..d7fbafd2 --- /dev/null +++ b/src/Support/Shell.php @@ -0,0 +1,99 @@ +setUpdateCheck(Checker::NEVER); + + $config->getPresenter()->addCasters(self::casters()); + + $shell = new PsyShell($config); + + $loader = self::tinkered($shell); + + try { + $shell->run(); + } finally { + $loader?->unregister(); + } + } + + /** + * Returns the casters for the Psy Shell. + * + * @return array + */ + private static function casters(): array + { + $casters = [ + 'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection', + 'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString', + 'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable', + ]; + + if (class_exists('Illuminate\Database\Eloquent\Model')) { + $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel'; + } + + if (class_exists('Illuminate\Process\ProcessResult')) { + $casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult'; + } + + if (class_exists('Illuminate\Foundation\Application')) { + $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication'; + } + + if (function_exists('app') === false) { + return $casters; // @phpstan-ignore-line + } + + $config = app()->make('config'); + + return array_merge($casters, (array) $config->get('tinker.casters', [])); + } + + /** + * Tinkers the current shell, if the Tinker package is available. + */ + private static function tinkered(PsyShell $shell): ?ClassLoader + { + if (function_exists('app') === false + || ! class_exists(Env::class) + || ! class_exists(ClassAliasAutoloader::class) + ) { + return null; + } + + $path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor'); + + $path .= '/composer/autoload_classmap.php'; + + $config = app()->make('config'); + + $loader = ClassAliasAutoloader::register( + $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) + ); + + return $loader; + } +}