diff --git a/.lintmdrc b/.lintmdrc index bb04b94..50d263d 100644 --- a/.lintmdrc +++ b/.lintmdrc @@ -1,5 +1,9 @@ { "excludeFiles": [ + "art/", + "config/", + "resources/", + "routes/", "src/", "tests/", "vendor/" @@ -9,7 +13,7 @@ "no-long-code": [ 2, { - "length": 100, + "length": 150, "exclude": [ "dot" ] diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 5fcbb78..3769f43 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -3,22 +3,26 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ +use PhpCsFixer\Config; +use PhpCsFixer\Finder; + $header = << This source file is subject to the MIT license that is bundled. EOF; -$finder = PhpCsFixer\Finder::create() +/** @noinspection PhpParamsInspection */ +$finder = Finder::create() ->in([ __DIR__.'/config', __DIR__.'/routes', @@ -46,7 +50,7 @@ ->ignoreDotFiles(true) ->ignoreVCS(true); -return (new PhpCsFixer\Config()) +return (new Config()) ->setRules([ '@DoctrineAnnotation' => true, '@PHP80Migration:risky' => true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ff68d..c136c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,4 @@ # Changelog -All notable changes to `guanguans/laravel-web-tinker` will be documented in this file. +All notable changes to `guanguans/laravel-code-runner` will be documented in this file. -## 1.0.0 - 202X-XX-XX - -- Initial release. diff --git a/README-zh_CN.md b/README-zh_CN.md index 5239c32..44fa1b9 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -1,34 +1,75 @@ -# package-skeleton +# laravel-code-runner -[简体中文](README-zh_CN.md) | [ENGLISH](README.md) +[ENGLISH](README.md) | [简体中文](README-zh_CN.md) -> 一个 PHP 软件包模板存储库。- A PHP package template repository. +> Run the code in the browser. - 在浏览器中运行代码。 -[![tests](https://github.com/guanguans/laravel-web-tinker/workflows/tests/badge.svg)](https://github.com/guanguans/laravel-web-tinker/actions) -[![check & fix styling](https://github.com/guanguans/laravel-web-tinker/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-web-tinker/actions) -[![codecov](https://codecov.io/gh/guanguans/laravel-web-tinker/branch/main/graph/badge.svg?token=URGFAWS6S4)](https://codecov.io/gh/guanguans/laravel-web-tinker) -[![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-web-tinker/v)](//packagist.org/packages/guanguans/laravel-web-tinker) -[![Total Downloads](https://poser.pugx.org/guanguans/laravel-web-tinker/downloads)](//packagist.org/packages/guanguans/laravel-web-tinker) -[![License](https://poser.pugx.org/guanguans/laravel-web-tinker/license)](//packagist.org/packages/guanguans/laravel-web-tinker) -![GitHub repo size](https://img.shields.io/github/repo-size/guanguans/laravel-web-tinker) -![GitHub release (latest by date)](https://img.shields.io/github/v/release/guanguans/laravel-web-tinker) - -## 功能 - -* 功能 +[![tests](https://github.com/guanguans/laravel-code-runner/workflows/tests/badge.svg)](https://github.com/guanguans/laravel-code-runner/actions) +[![check & fix styling](https://github.com/guanguans/laravel-code-runner/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-code-runner/actions) +[![codecov](https://codecov.io/gh/guanguans/laravel-code-runner/branch/main/graph/badge.svg?token=URGFAWS6S4)](https://codecov.io/gh/guanguans/laravel-code-runner) +[![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-code-runner/v)](//packagist.org/packages/guanguans/laravel-code-runner) +[![Total Downloads](https://poser.pugx.org/guanguans/laravel-code-runner/downloads)](//packagist.org/packages/guanguans/laravel-code-runner) +[![License](https://poser.pugx.org/guanguans/laravel-code-runner/license)](//packagist.org/packages/guanguans/laravel-code-runner) +![GitHub repo size](https://img.shields.io/github/repo-size/guanguans/laravel-code-runner) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/guanguans/laravel-code-runner) ## 环境要求 * PHP >= 7.4 +* Laravel >= 7.0 ## 安装 +通过 Composer 安装该软件包。 + +```bash +$ composer require guanguans/laravel-code-runner --prefer-dist -vvv +``` + +运行此命令来发布资源文件。 + +```bash +$ php artisan code-runner:install +``` + +发布配置文件(可选的)。 + ```bash -$ composer require guanguans/laravel-web-tinker --prefer-dist -vvv +$ php artisan vendor:publish --provider="Guanguans\LaravelCodeRunner\WebTinkerServiceProvider" --tag="code-runner-config" ``` ## 使用 +默认情况下,此包仅在本地环境中运行。 + +访问 `/code-runner` 查看页面。 + +![](docs/usage.png) + +### Authorization + +如果您想在另一个环境中运行它(我们不建议这样做),您必须执行两个步骤。 + +1. 您必须将 `code-runner` 配置文件中的 `enabled` 值设置为 `true`。 + +2. 您必须注册一个 `view-code-runner` 的 `ability`。最好在 Laravel 附带的 `AuthServiceProvider` 中。 + +```php +use Illuminate\Contracts\Auth\Authenticatable; + +public function boot() +{ + $this->registerPolicies(); + + Gate::define('view-code-runner', function (?Authenticatable $user = null) { + // 如果允许访问 web tinker,则返回 true。这是一个例子: + return $user && in_array($user->email, [ + 'admin@example.com', + ]); + }); +} +``` + ## 测试 ```bash diff --git a/README.md b/README.md index 69b7cc9..6383d4d 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,75 @@ -# laravel-web-tinker +# laravel-code-runner -[简体中文](README-zh_CN.md) | [ENGLISH](README.md) +[ENGLISH](README.md) | [简体中文](README-zh_CN.md) -> A PHP package template repository. - 一个 PHP 软件包模板存储库。 +> Run the code in the browser. - 在浏览器中运行代码。 -[![tests](https://github.com/guanguans/laravel-web-tinker/workflows/tests/badge.svg)](https://github.com/guanguans/laravel-web-tinker/actions) -[![check & fix styling](https://github.com/guanguans/laravel-web-tinker/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-web-tinker/actions) -[![codecov](https://codecov.io/gh/guanguans/laravel-web-tinker/branch/main/graph/badge.svg?token=URGFAWS6S4)](https://codecov.io/gh/guanguans/laravel-web-tinker) -[![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-web-tinker/v)](//packagist.org/packages/guanguans/laravel-web-tinker) -[![Total Downloads](https://poser.pugx.org/guanguans/laravel-web-tinker/downloads)](//packagist.org/packages/guanguans/laravel-web-tinker) -[![License](https://poser.pugx.org/guanguans/laravel-web-tinker/license)](//packagist.org/packages/guanguans/laravel-web-tinker) -![GitHub repo size](https://img.shields.io/github/repo-size/guanguans/laravel-web-tinker) -![GitHub release (latest by date)](https://img.shields.io/github/v/release/guanguans/laravel-web-tinker) - -## Features - -* Feature +[![tests](https://github.com/guanguans/laravel-code-runner/workflows/tests/badge.svg)](https://github.com/guanguans/laravel-code-runner/actions) +[![check & fix styling](https://github.com/guanguans/laravel-code-runner/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-code-runner/actions) +[![codecov](https://codecov.io/gh/guanguans/laravel-code-runner/branch/main/graph/badge.svg?token=URGFAWS6S4)](https://codecov.io/gh/guanguans/laravel-code-runner) +[![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-code-runner/v)](//packagist.org/packages/guanguans/laravel-code-runner) +[![Total Downloads](https://poser.pugx.org/guanguans/laravel-code-runner/downloads)](//packagist.org/packages/guanguans/laravel-code-runner) +[![License](https://poser.pugx.org/guanguans/laravel-code-runner/license)](//packagist.org/packages/guanguans/laravel-code-runner) +![GitHub repo size](https://img.shields.io/github/repo-size/guanguans/laravel-code-runner) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/guanguans/laravel-code-runner) ## Requirement * PHP >= 7.4 +* Laravel >= 7.0 ## Installation +You can install the package via composer. + +```bash +$ composer require guanguans/laravel-code-runner --prefer-dist -vvv +``` + +Next, you must publish the assets from this package by running this command. + +```bash +$ php artisan code-runner:install +``` + +Optionally, you can publish the config file of the package. + ```bash -$ composer require guanguans/laravel-web-tinker --prefer-dist -vvv +$ php artisan vendor:publish --provider="Guanguans\LaravelCodeRunner\WebTinkerServiceProvider" --tag="code-runner-config" ``` ## Usage +![](docs/usage.png) + +By default this package will only run in a local environment. + +Visit `/code-runner` in your app to view page. + +### Authorization + +Should you want to run this in another environment (we do not recommend this), there are two steps you must perform. + +1. You must set the `enabled` variable in the `code-runner` config file to `true`. + +2. You must register a `view-code-runner` ability. A good place to do this is in the `AuthServiceProvider` that ships with Laravel. + +```php +use Illuminate\Contracts\Auth\Authenticatable; + +public function boot() +{ + $this->registerPolicies(); + + Gate::define('view-code-runner', function (?Authenticatable $user = null) { + // Return true if access to web tinker is allowed. Here's an example: + return $user && in_array($user->email, [ + 'admin@example.com', + ]); + }); +} +``` + ## Testing ```bash diff --git a/_ide_helper.php b/_ide_helper.php index 1b75651..b9718ce 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -1,7 +1,7 @@ * @@ -10,4 +10,7 @@ namespace { + class CodeRunner extends \Guanguans\LaravelCodeRunner\Facades\CodeRunner + { + } } diff --git a/composer.json b/composer.json index e584a53..b63ae3d 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,19 @@ { - "name": "guanguans/laravel-web-tinker", - "description": "Artisan Tinker in your browser", + "name": "guanguans/laravel-code-runner", + "description": "Run the code in the browser. - 在浏览器中运行代码。", "license": "MIT", "type": "library", "abandoned": false, "non-feature-branches": [], "keywords": [ - "web-tinker", "laravel", + "code-runner", + "code", + "runner", + "web", "tinker", + "artisan", + "browser", "debug", "development" ], @@ -21,10 +26,10 @@ "role": "developer" } ], - "homepage": "https://github.com/guanguans/laravel-web-tinker", + "homepage": "https://github.com/guanguans/laravel-code-runner", "support": { - "issues": "https://github.com/guanguans/laravel-web-tinker/issues", - "source": "https://github.com/guanguans/laravel-web-tinker" + "issues": "https://github.com/guanguans/laravel-code-runner/issues", + "source": "https://github.com/guanguans/laravel-code-runner" }, "funding": [ { @@ -75,7 +80,7 @@ "autoload": { "psr-0": {}, "psr-4": { - "Guanguans\\LaravelWebTinker\\": "src" + "Guanguans\\LaravelCodeRunner\\": "src" }, "classmap": [], "files": [], @@ -85,7 +90,7 @@ }, "autoload-dev": { "psr-4": { - "Guanguans\\LaravelWebTinkerTests\\": "tests" + "Guanguans\\LaravelCodeRunnerTests\\": "tests" } }, "bin": [], @@ -93,6 +98,7 @@ "config": { "allow-plugins": { "infection/extension-installer": true, + "kylekatarnls/update-helper": false, "pestphp/pest-plugin": true, "phpstan/extension-installer": true }, @@ -114,15 +120,18 @@ "composer checks" ] }, + "laravel": { + "aliases": { + "CodeRunner": "Guanguans\\LaravelCodeRunner\\Facades\\CodeRunner" + }, + "providers": [ + "Guanguans\\LaravelCodeRunner\\CodeRunnerServiceProvider" + ] + }, "phpstan": { "includes": [ "extension.neon" ] - }, - "laravel": { - "providers": [ - "Guanguans\\LaravelWebTinker\\WebTinkerServiceProvider" - ] } }, "scripts": { @@ -133,14 +142,15 @@ "post-update-cmd": [ "@cghooks update" ], - "post-autoload-dump": [ - ], + "post-autoload-dump": [], "cghooks": "./vendor/bin/cghooks", "checks": [ "@mark-start", "@style-lint", "@mark-separate", "@test", + "@mark-separate", + "@psalm", "@mark-finish" ], "infection": "./vendor/bin/infection --test-framework=pest --show-mutations --threads=4 --ansi", @@ -161,7 +171,7 @@ "rector": "./vendor/bin/rector --clear-cache --ansi -v", "rector-dry-run": "@rector --dry-run", "style-fix": "./vendor/bin/php-cs-fixer fix --using-cache=no --config=.php-cs-fixer.php --ansi", - "style-lint": "@style-fix --diff --dry-run", + "style-lint": "@style-fix --dry-run --diff", "test": "@pest", "test-coverage": "@pest-coverage" }, diff --git a/config/code-runner.php b/config/code-runner.php new file mode 100644 index 0000000..b0c4f76 --- /dev/null +++ b/config/code-runner.php @@ -0,0 +1,43 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +return [ + /** + * By default this package will only run in local development. + * Do not change this, unless you know what you are doing. + */ + 'enabled' => env('WEB_TINKER_ENABLED', 'local' === env('APP_ENV')), + + /** + * @see https://github.com/highlightjs/highlight.js/tree/main/src/styles + */ + 'theme' => env('WEB_TINKER_THEME', 'github-dark'), + + 'route' => [ + 'domain' => null, + 'middleware' => Guanguans\LaravelCodeRunner\Http\Middleware\Authorize::class, + 'as' => 'code-runner.', + 'name' => 'code-runner.', + 'prefix' => '/code-runner', + ], + + 'code_runner' => Guanguans\LaravelCodeRunner\CodeRunners\WebTinkerCodeRunner::class, + + 'result_modifier' => Guanguans\LaravelCodeRunner\ResultModifiers\PrefixDateTimeResultModifier::class, + + /* + * If you want to fine-tune PsySH configuration specify + * configuration file name, relative to the root of your + * application directory. + */ + 'config_file' => env('WEB_TINKER_CONFIG_FILE'), +]; diff --git a/config/web-tinker.php b/config/web-tinker.php deleted file mode 100644 index a590b1e..0000000 --- a/config/web-tinker.php +++ /dev/null @@ -1,13 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled. - */ - -return []; diff --git a/docs/.gitkeep b/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/usage.png b/docs/usage.png new file mode 100644 index 0000000..f791111 Binary files /dev/null and b/docs/usage.png differ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4156a99..6d5b256 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,36 +1,6 @@ parameters: ignoreErrors: - - message: "#^Call to an undefined method Illuminate\\\\Database\\\\ConnectionInterface\\:\\:getDriverName\\(\\)\\.$#" + message: "#^Should not use node with type \"Expr_Eval\", please change the code\\.$#" count: 1 - path: src/Exceptions/DatabaseNotSupported.php - - - - message: "#^Method Spatie\\\\Health\\\\Notifications\\\\CheckFailedNotification\\:\\:transParameters\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: src/Notifications/CheckFailedNotification.php - - - - message: "#^Unable to resolve the template type TKey in call to function collect$#" - count: 1 - path: src/ResultStores/ResultStores.php - - - - message: "#^Unable to resolve the template type TValue in call to function collect$#" - count: 1 - path: src/ResultStores/ResultStores.php - - - - message: "#^Property Spatie\\\\Health\\\\ResultStores\\\\StoredCheckResults\\\\StoredCheckResults\\:\\:\\$storedCheckResults \\(Illuminate\\\\Support\\\\Collection\\\\) does not accept Illuminate\\\\Support\\\\Collection\\\\|Illuminate\\\\Support\\\\Collection\\\\.$#" - count: 1 - path: src/ResultStores/StoredCheckResults/StoredCheckResults.php - - - - message: "#^Unable to resolve the template type TKey in call to function collect$#" - count: 2 - path: src/ResultStores/StoredCheckResults/StoredCheckResults.php - - - - message: "#^Unable to resolve the template type TValue in call to function collect$#" - count: 2 - path: src/ResultStores/StoredCheckResults/StoredCheckResults.php + path: src/CodeRunners/EvalCodeRunner.php diff --git a/phpstan.neon b/phpstan.neon index 1e86cb7..ebb134b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,19 +1,14 @@ includes: - phpstan-baseline.neon -# - vendor/roave/no-floaters/rules.neon -# - vendor/phpstan/phpstan-strict-rules/rules.neon -# - vendor/ergebnis/phpstan-rules/rules.neon -# - vendor/ekino/phpstan-banned-code/extension.neon parameters: - level: 8 + level: 5 paths: - src tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true checkMissingIterableValueType: true -# disallowFloatsEverywhere: true strictRules: allRules: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2795967..f0b0a37 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,48 +1,49 @@ - - - src/ - - - src/Skeleton.php - - - - - tests/ - vendor/ - - - - - - - - 500 - - - 10 - - - false - - - - - + + + src/ + + + src/Facades + src/WebTinkerServiceProvider.php + + + + + tests/ + vendor/ + + + + + + + + 500 + + + 10 + + + false + + + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 6e80e0e..9509bc4 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,2 +1,26 @@ - + + + + config('code-runner.config_file') + + + + + $authenticatable + + + + + View + + + + + View + + + view('code-runner::livewire.code-runner') + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 227c9d9..da1f1b9 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -16,7 +16,6 @@ - diff --git a/rector.php b/rector.php index 7ad48ee..d120810 100644 --- a/rector.php +++ b/rector.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * @@ -31,6 +31,8 @@ use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector; use Rector\EarlyReturn\Rector\If_\ChangeAndIfToEarlyReturnRector; use Rector\EarlyReturn\Rector\Return_\ReturnBinaryOrToEarlyReturnRector; +use Rector\Laravel\Set\LaravelLevelSetList; +use Rector\Laravel\Set\LaravelSetList; use Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector; use Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector; use Rector\PHPUnit\Set\PHPUnitLevelSetList; @@ -49,39 +51,44 @@ ]); $rectorConfig->paths([ + // __DIR__.'/config', + __DIR__.'/routes', __DIR__.'/src', __DIR__.'/tests', + __DIR__.'/.php-cs-fixer.php', + __DIR__.'/rector.php', ]); $rectorConfig->skip([ // rules - CallableThisArrayToAnonymousFunctionRector::class, - InlineIfToExplicitIfRector::class, - LogicalToBooleanRector::class, - SimplifyBoolIdenticalTrueRector::class, - RemoveEmptyMethodCallRector::class, - AddSeeTestAnnotationRector::class, - NormalizeNamespaceByPSR4ComposerAutoloadRector::class, - ChangeAndIfToEarlyReturnRector::class, - ReturnBinaryOrToEarlyReturnRector::class, - EncapsedStringsToSprintfRector::class, - WrapEncapsedVariableInCurlyBracesRector::class, + // CallableThisArrayToAnonymousFunctionRector::class, + // InlineIfToExplicitIfRector::class, + // LogicalToBooleanRector::class, + // SimplifyBoolIdenticalTrueRector::class, + // RemoveEmptyMethodCallRector::class, + // AddSeeTestAnnotationRector::class, + // NormalizeNamespaceByPSR4ComposerAutoloadRector::class, + // ChangeAndIfToEarlyReturnRector::class, + // ReturnBinaryOrToEarlyReturnRector::class, + // EncapsedStringsToSprintfRector::class, + // WrapEncapsedVariableInCurlyBracesRector::class, // optional rules // AddDefaultValueForUndefinedVariableRector::class, // RemoveUnusedVariableAssignRector::class, // UnSpreadOperatorRector::class, // ConsistentPregDelimiterRector::class, - // StaticClosureRector::class, + StaticClosureRector::class, // paths + '**/fixtures*', + '**/fixtures/*', '**/Fixture*', '**/Fixture/*', '**/Source*', '**/Source/*', '**/Expected/*', '**/Expected*', - __DIR__.'/src/foundation/tests/AppTest.php', ]); $rectorConfig->sets([ @@ -100,8 +107,15 @@ SetList::TYPE_DECLARATION_STRICT, SetList::EARLY_RETURN, - PHPUnitLevelSetList::UP_TO_PHPUNIT_70, - // PHPUnitSetList::PHPUNIT80_DMS, + LaravelLevelSetList::UP_TO_LARAVEL_70, + LaravelSetList::ARRAY_STR_FUNCTIONS_TO_STATIC_CALL, + // LaravelSetList::LARAVEL_STATIC_TO_INJECTION, + LaravelSetList::LARAVEL_CODE_QUALITY, + LaravelSetList::LARAVEL_ARRAY_STR_FUNCTION_TO_STATIC_CALL, + LaravelSetList::LARAVEL_LEGACY_FACTORIES_TO_CLASSES, + + PHPUnitLevelSetList::UP_TO_PHPUNIT_80, + PHPUnitSetList::PHPUNIT80_DMS, PHPUnitSetList::PHPUNIT_CODE_QUALITY, PHPUnitSetList::PHPUNIT_EXCEPTION, PHPUnitSetList::REMOVE_MOCKS, diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php new file mode 100644 index 0000000..aff105b --- /dev/null +++ b/resources/views/index.blade.php @@ -0,0 +1,111 @@ + + + + + + + + Code Runner + + + + +
+
+ +
+
+
+ + +
+
+ +
+
Result
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..8bb86dd --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,7 @@ + + @livewireStyles + + + {{ $slot }} + @livewireScripts + \ No newline at end of file diff --git a/resources/views/livewire/code-runner.blade.php b/resources/views/livewire/code-runner.blade.php new file mode 100644 index 0000000..2c522f7 --- /dev/null +++ b/resources/views/livewire/code-runner.blade.php @@ -0,0 +1,2 @@ + +code-runner \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 58aeb6e..541ef4c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,9 +3,20 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ + +use Illuminate\Routing\Router; +use Illuminate\Support\Facades\Route; + +Route::group( + ['namespace' => 'Guanguans\LaravelCodeRunner\Http\Controllers'] + config('code-runner.route'), + static function (Router $router): void { + Route::get('/', 'CodeRunnerController@index'); + Route::post('/run', 'CodeRunnerController@run')->name('run'); + } +); diff --git a/src/CodeRunnerServiceProvider.php b/src/CodeRunnerServiceProvider.php new file mode 100644 index 0000000..a8cab56 --- /dev/null +++ b/src/CodeRunnerServiceProvider.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner; + +use Guanguans\LaravelCodeRunner\Console\Commands\InstallCommand; +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Guanguans\LaravelCodeRunner\Contracts\ResultModifierContract; +use Guanguans\LaravelCodeRunner\Http\Livewire\CodeRunner; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Foundation\Console\AboutCommand; +use Illuminate\Support\Facades\Gate; +use Livewire\Livewire; +use Livewire\LivewireServiceProvider; +use Spatie\LaravelPackageTools\Package; +use Spatie\LaravelPackageTools\PackageServiceProvider; + +class CodeRunnerServiceProvider extends PackageServiceProvider +{ + public function configurePackage(Package $package): void + { + $package + ->name('laravel-code-runner') + ->hasConfigFile() + ->hasViews() + ->hasTranslations() + ->hasAssets() + ->hasRoute('web') + ->hasCommand(InstallCommand::class); + } + + public function registeringPackage(): void + { + $this->app->register(LivewireServiceProvider::class); + } + + public function packageRegistered(): void + { + $this->app->bind(CodeRunnerContract::class, config('code-runner.code_runner')); + $this->app->alias(CodeRunnerContract::class, 'laravel-code-runner.code-runner'); + + $this->app->bind(ResultModifierContract::class, config('code-runner.result_modifier')); + $this->app->alias(ResultModifierContract::class, 'laravel-code-runner.result-modifier'); + } + + public function packageBooted(): void + { + Livewire::component('code-runner::livewire.code-runner', CodeRunner::class); + + Gate::define('view-code-runner', static fn (?Authenticatable $authenticatable = null) => app()->environment('local')); + + if (class_exists(AboutCommand::class)) { + AboutCommand::add( + 'Laravel Code Runner', + [ + 'Author' => 'guanguans', + 'Homepage' => 'https://github.com/guanguans/laravel-code-runner', + 'License' => 'MIT', + ] + ); + } + } + + public function provides(): array + { + return [ + CodeRunnerContract::class, 'laravel-code-runner.code-runner', + ResultModifierContract::class, 'laravel-code-runner.result-modifier', + ]; + } +} diff --git a/src/CodeRunners/ArtisanTinkerCodeRunner.php b/src/CodeRunners/ArtisanTinkerCodeRunner.php new file mode 100644 index 0000000..808d450 --- /dev/null +++ b/src/CodeRunners/ArtisanTinkerCodeRunner.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\CodeRunners; + +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Illuminate\Contracts\Console\Kernel; +use Symfony\Component\Console\Output\BufferedOutput; +use Throwable; + +class ArtisanTinkerCodeRunner implements CodeRunnerContract +{ + protected Kernel $kernel; + + protected BufferedOutput $bufferedOutput; + + public function __construct(Kernel $kernel, BufferedOutput $bufferedOutput) + { + $this->kernel = $kernel; + $this->bufferedOutput = $bufferedOutput; + } + + public function run(string $code): string + { + try { + /** @noinspection PhpParamsInspection */ + $this->kernel->call( + 'tinker', + ['--execute' => $code], + $this->bufferedOutput + ); + } catch (Throwable $throwable) { + return $throwable->getTraceAsString(); + } + + return $this->bufferedOutput->fetch(); + } +} diff --git a/src/CodeRunners/EvalCodeRunner.php b/src/CodeRunners/EvalCodeRunner.php new file mode 100644 index 0000000..97e1e9c --- /dev/null +++ b/src/CodeRunners/EvalCodeRunner.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\CodeRunners; + +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Throwable; + +class EvalCodeRunner implements CodeRunnerContract +{ + public function run(string $code): string + { + ob_start(); + + try { + eval($code); + } catch (Throwable $throwable) { + ob_end_clean(); + + return $throwable->getTraceAsString(); + } + + return (string) ob_get_clean(); + } +} diff --git a/src/CodeRunners/WebTinkerCodeRunner.php b/src/CodeRunners/WebTinkerCodeRunner.php new file mode 100644 index 0000000..34b9f11 --- /dev/null +++ b/src/CodeRunners/WebTinkerCodeRunner.php @@ -0,0 +1,123 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\CodeRunners; + +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Application; +use Illuminate\Support\Collection; +use Laravel\Tinker\ClassAliasAutoloader; +use Psy\Configuration; +use Psy\ExecutionLoopClosure; +use Psy\Shell; +use Symfony\Component\Console\Output\BufferedOutput; + +/** + * This is modified from `https://github.com/spatie/laravel-web-tinker`. + */ +class WebTinkerCodeRunner implements CodeRunnerContract +{ + protected BufferedOutput $bufferedOutput; + + protected Shell $shell; + + public function __construct() + { + $this->bufferedOutput = new BufferedOutput(); + + $this->shell = $this->createShell($this->bufferedOutput); + } + + public function run(string $code): string + { + $code = $this->removeComments($code); + + $this->shell->addInput($code); + + $closure = new ExecutionLoopClosure($this->shell); + + $closure->execute(); + + return $this->bufferedOutput->fetch(); + } + + protected function createShell(BufferedOutput $bufferedOutput): Shell + { + $configuration = new Configuration([ + 'updateCheck' => 'never', + 'configFile' => null !== config('code-runner.config_file') ? base_path(config('code-runner.config_file')) : null, + ]); + + $configuration->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + + $configuration->getPresenter()->addCasters([ + Collection::class => 'Laravel\Tinker\TinkerCaster::castCollection', + Model::class => 'Laravel\Tinker\TinkerCaster::castModel', + Application::class => 'Laravel\Tinker\TinkerCaster::castApplication', + ]); + + $shell = new Shell($configuration); + + $shell->setOutput($bufferedOutput); + + $composerClassMap = base_path('vendor/composer/autoload_classmap.php'); + + if (file_exists($composerClassMap)) { + ClassAliasAutoloader::register($shell, $composerClassMap); + } + + return $shell; + } + + public function removeComments(string $code): string + { + return collect(token_get_all("'))->reduce( + /** + * @param string|array $token + */ + function (string $carry, $token): string { + if (is_string($token)) { + return $carry.$token; + } + + $text = $this->ignoreCommentsAndPhpTags($token); + + return $carry.$text; + }, + '' + ); + } + + protected function ignoreCommentsAndPhpTags(array $token): string + { + [$id, $text] = $token; + + if (T_COMMENT === $id) { + return ''; + } + + if (T_DOC_COMMENT === $id) { + return ''; + } + + if (T_OPEN_TAG === $id) { + return ''; + } + + if (T_CLOSE_TAG === $id) { + return ''; + } + + return $text; + } +} diff --git a/src/Console/Commands/InstallCommand.php b/src/Console/Commands/InstallCommand.php new file mode 100644 index 0000000..50ee4a3 --- /dev/null +++ b/src/Console/Commands/InstallCommand.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Console\Commands; + +use Illuminate\Console\Command; + +class InstallCommand extends Command +{ + /** + * @var string + */ + protected $signature = 'code-runner:install'; + + /** + * @var string + */ + protected $description = 'Install all of the `Code Runner` resources'; + + public function handle(): void + { + $this->comment('Publishing `Code Runner` Resources...'); + + /** @noinspection PhpParamsInspection */ + $this->call('vendor:publish', [ + '--tag' => [ + // 'code-runner-assets', + // 'code-runner-translations', + 'code-runner-views', + ], + ]); + + $this->info('`Code Runner` installed successfully.'); + } +} diff --git a/src/Contracts/CodeRunnerContract.php b/src/Contracts/CodeRunnerContract.php new file mode 100644 index 0000000..a8d08c8 --- /dev/null +++ b/src/Contracts/CodeRunnerContract.php @@ -0,0 +1,18 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Contracts; + +interface CodeRunnerContract +{ + public function run(string $code): string; +} diff --git a/src/Contracts/ResultModifierContract.php b/src/Contracts/ResultModifierContract.php new file mode 100644 index 0000000..bc0731a --- /dev/null +++ b/src/Contracts/ResultModifierContract.php @@ -0,0 +1,18 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Contracts; + +interface ResultModifierContract +{ + public function modify(string $result): string; +} diff --git a/src/Facades/CodeRunner.php b/src/Facades/CodeRunner.php new file mode 100644 index 0000000..8878580 --- /dev/null +++ b/src/Facades/CodeRunner.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Facades; + +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Illuminate\Support\Facades\Facade; + +class CodeRunner extends Facade +{ + /** + * {@inheritdoc} + */ + protected static function getFacadeAccessor(): string + { + return CodeRunnerContract::class; + } +} diff --git a/src/Http/Controllers/CodeRunnerController.php b/src/Http/Controllers/CodeRunnerController.php new file mode 100644 index 0000000..418bf77 --- /dev/null +++ b/src/Http/Controllers/CodeRunnerController.php @@ -0,0 +1,55 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Http\Controllers; + +use Guanguans\LaravelCodeRunner\Contracts\CodeRunnerContract; +use Guanguans\LaravelCodeRunner\Contracts\ResultModifierContract; +use Illuminate\Contracts\Routing\UrlGenerator; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Routing\Controller; +use Illuminate\Support\Str; +use Illuminate\View\View; + +class CodeRunnerController extends Controller +{ + /** + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress PossiblyInvalidCast + */ + public function index(UrlGenerator $urlGenerator): View + { + return view('code-runner::index', [ + 'theme' => config('code-runner.theme'), + 'path' => $urlGenerator->to( + Str::finish(config('code-runner.route.prefix'), '/').'run' + ), + ]); + } + + public function run( + Request $request, + CodeRunnerContract $codeRunnerContract, + ResultModifierContract $resultModifierContract + ): JsonResponse { + $validated = $request->validate([ + 'code' => 'required|string', + ]); + + $result = $codeRunnerContract->run($validated['code']); + + return response()->json([ + 'result' => $resultModifierContract->modify($result), + ]); + } +} diff --git a/src/Http/Livewire/CodeRunner.php b/src/Http/Livewire/CodeRunner.php new file mode 100644 index 0000000..78240a9 --- /dev/null +++ b/src/Http/Livewire/CodeRunner.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Http\Livewire; + +use Illuminate\View\View; +use Livewire\Component; + +class CodeRunner extends Component +{ + public function render(): View + { + return view('code-runner::livewire.code-runner') + // ->layout('code-runner::layouts.app') + // ->slot('main') + ; + } +} diff --git a/src/Http/Middleware/Authorize.php b/src/Http/Middleware/Authorize.php new file mode 100644 index 0000000..04676cd --- /dev/null +++ b/src/Http/Middleware/Authorize.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\Http\Middleware; + +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Gate; +use Symfony\Component\HttpFoundation\Response; + +class Authorize +{ + /** + * Handle the incoming request. + * + * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function handle(Request $request, callable $next): Response + { + return $this->allowedToUseTinker() ? $next($request) : abort(403); + } + + protected function allowedToUseTinker(): bool + { + if (! config('code-runner.enabled')) { + return false; + } + + return Gate::check('view-code-runner'); + } +} diff --git a/src/ResultModifiers/PrefixDateTimeResultModifier.php b/src/ResultModifiers/PrefixDateTimeResultModifier.php new file mode 100644 index 0000000..7babaad --- /dev/null +++ b/src/ResultModifiers/PrefixDateTimeResultModifier.php @@ -0,0 +1,30 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunner\ResultModifiers; + +use Guanguans\LaravelCodeRunner\Contracts\ResultModifierContract; + +class PrefixDateTimeResultModifier implements ResultModifierContract +{ + public function modify(string $result): string + { + return ''.now()->format('Y-m-d H:i:s').'
'.$this->clean($result); + } + + protected function clean(string $result): string + { + $result = preg_replace(/** @lang PhpRegExp */ '#(?s)()|Exit: {2}Ctrl\+D#ms', '$2', $result); + + return trim($result); + } +} diff --git a/src/WebTinkerServiceProvider.php b/src/WebTinkerServiceProvider.php deleted file mode 100644 index 8cd9629..0000000 --- a/src/WebTinkerServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled. - */ - -namespace Guanguans\LaravelWebTinker; - -use Spatie\LaravelPackageTools\Package; -use Spatie\LaravelPackageTools\PackageServiceProvider; - -class WebTinkerServiceProvider extends PackageServiceProvider -{ - public function configurePackage(Package $package): void - { - $package - ->name('laravel-web-tinker') - ->hasConfigFile() - ->hasTranslations() - ->hasViews(); - } -} diff --git a/tests/CodeRunners/EvalCodeRunnerTest.php b/tests/CodeRunners/EvalCodeRunnerTest.php new file mode 100644 index 0000000..cfc43f4 --- /dev/null +++ b/tests/CodeRunners/EvalCodeRunnerTest.php @@ -0,0 +1,21 @@ + + * + * This source file is subject to the MIT license that is bundled. + */ + +namespace Guanguans\LaravelCodeRunnerTests\CodeRunners; + +use Guanguans\LaravelCodeRunner\CodeRunners\EvalCodeRunner; + +it('will return a string to run the `run` method.', function (): void { + expect(new EvalCodeRunner()) + ->run('echo 1;')->toBe('1') + ->run('echo 1')->toBeString(); +})->group(__DIR__, __FILE__); diff --git a/tests/Datasets/Movies.php b/tests/Datasets/Movies.php index 07ebf27..ec4bc92 100644 --- a/tests/Datasets/Movies.php +++ b/tests/Datasets/Movies.php @@ -3,14 +3,14 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ -namespace Guanguans\LaravelWebTinkerTests\Datasets; +namespace Guanguans\LaravelCodeRunnerTests\Datasets; dataset('movies', [ '肖申克的救赎', diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 62e7f66..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled. - */ - -it('to be string', function ($movie): void { - expect($movie)->toBeString(); -})->group(__DIR__, __FILE__)->with('movies'); diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 72d8c96..9d0b234 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -3,15 +3,15 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ -namespace Guanguans\LaravelWebTinkerTests\Feature; +namespace Guanguans\LaravelCodeRunnerTests\Feature; -class TestCase extends \Guanguans\LaravelWebTinkerTests\TestCase +class TestCase extends \Guanguans\LaravelCodeRunnerTests\TestCase { } diff --git a/tests/Pest.php b/tests/Pest.php index ae244ef..81252ed 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -3,14 +3,14 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ -namespace Guanguans\LaravelWebTinkerTests; +namespace Guanguans\LaravelCodeRunnerTests; use Closure; use Pest\Expectation; diff --git a/tests/TestCase.php b/tests/TestCase.php index 485d83d..9308ef0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,17 +3,19 @@ declare(strict_types=1); /** - * This file is part of the guanguans/laravel-web-tinker. + * This file is part of the guanguans/laravel-code-runner. * * (c) guanguans * * This source file is subject to the MIT license that is bundled. */ -namespace Guanguans\LaravelWebTinkerTests; +namespace Guanguans\LaravelCodeRunnerTests; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; +use Guanguans\LaravelCodeRunner\CodeRunnerServiceProvider; use Illuminate\Database\Eloquent\Factories\Factory; +use Livewire\LivewireServiceProvider; use Mockery; class TestCase extends \Orchestra\Testbench\TestCase @@ -44,7 +46,7 @@ protected function setUp(): void // \DG\BypassFinals::enable(); Factory::guessFactoryNamesUsing( - static fn ($modelName): string => 'Guanguans\\LaravelWebTinker\\Database\\Factories\\'.class_basename($modelName).'Factory' + static fn ($modelName): string => 'Guanguans\\LaravelCodeRunner\\Database\\Factories\\'.class_basename($modelName).'Factory' ); } @@ -68,7 +70,8 @@ protected function finish(): void protected function getPackageProviders($app) { return [ - // SkeletonServiceProvider::class, + LivewireServiceProvider::class, + CodeRunnerServiceProvider::class, ]; }