├── docs ├── log.png ├── ray.png ├── dump.png ├── commands.gif ├── console.png ├── soar-bar.gif ├── soar-bar.png ├── clockwork.png ├── debug-bar.png ├── lara-dumps.png ├── telescope-log.png ├── test-coverage.gif ├── telescope-event.png └── json.json ├── src ├── Contracts │ ├── ThrowableContract.php │ ├── SanitizerContract.php │ └── OutputContract.php ├── Exceptions │ └── InvalidArgumentException.php ├── Outputs │ ├── Concerns │ │ ├── ScoresSanitizer.php │ │ ├── ScoresHydrator.php │ │ └── OutputConditions.php │ ├── ClockworkOutput.php │ ├── DumpOutput.php │ ├── RayOutput.php │ ├── AbstractOutput.php │ ├── LaraDumpsOutput.php │ ├── LogOutput.php │ ├── JsonOutput.php │ ├── DebugBarOutput.php │ └── ConsoleOutput.php ├── Listeners │ └── OutputScoresListener.php ├── Events │ ├── OutputtingEvent.php │ └── OutputtedEvent.php ├── Commands │ ├── RunCommand.php │ ├── ClearCommand.php │ ├── ScoreCommand.php │ └── Concerns │ │ └── WithSoarOptions.php ├── Soar.php ├── Middleware │ └── OutputScoresMiddleware.php ├── Mixins │ └── QueryBuilderMixin.php ├── OutputManager.php ├── Support │ ├── Utils.php │ └── helpers.php ├── Facades │ └── Soar.php ├── SoarServiceProvider.php └── Bootstrapper.php ├── LICENSE ├── _ide_helper.php ├── config └── soar.php ├── composer.json ├── README-zh_CN.md └── README.md /docs/log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/log.png -------------------------------------------------------------------------------- /docs/ray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/ray.png -------------------------------------------------------------------------------- /docs/dump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/dump.png -------------------------------------------------------------------------------- /docs/commands.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/commands.gif -------------------------------------------------------------------------------- /docs/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/console.png -------------------------------------------------------------------------------- /docs/soar-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/soar-bar.gif -------------------------------------------------------------------------------- /docs/soar-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/soar-bar.png -------------------------------------------------------------------------------- /docs/clockwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/clockwork.png -------------------------------------------------------------------------------- /docs/debug-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/debug-bar.png -------------------------------------------------------------------------------- /docs/lara-dumps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/lara-dumps.png -------------------------------------------------------------------------------- /docs/telescope-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/telescope-log.png -------------------------------------------------------------------------------- /docs/test-coverage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/test-coverage.gif -------------------------------------------------------------------------------- /docs/telescope-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guanguans/laravel-soar/HEAD/docs/telescope-event.png -------------------------------------------------------------------------------- /src/Contracts/ThrowableContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Contracts; 15 | 16 | interface ThrowableContract extends \Throwable {} 17 | -------------------------------------------------------------------------------- /src/Contracts/SanitizerContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Contracts; 15 | 16 | use Illuminate\Support\Collection; 17 | 18 | interface SanitizerContract 19 | { 20 | public function sanitize(Collection $scores): Collection; 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Exceptions; 15 | 16 | use Guanguans\LaravelSoar\Contracts\ThrowableContract; 17 | 18 | class InvalidArgumentException extends \InvalidArgumentException implements ThrowableContract {} 19 | -------------------------------------------------------------------------------- /src/Outputs/Concerns/ScoresSanitizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs\Concerns; 15 | 16 | use Illuminate\Support\Arr; 17 | use Illuminate\Support\Collection; 18 | 19 | trait ScoresSanitizer 20 | { 21 | protected array $except = []; 22 | 23 | public function sanitize(Collection $scores): Collection 24 | { 25 | return $scores->map(fn (array $score): array => Arr::except($score, $this->except)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/OutputContract.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Contracts; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | interface OutputContract 21 | { 22 | public function shouldOutput(CommandFinished|Response $outputter): bool; 23 | 24 | public function output(Collection $scores, CommandFinished|Response $outputter): mixed; 25 | } 26 | -------------------------------------------------------------------------------- /src/Listeners/OutputScoresListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Listeners; 15 | 16 | use Guanguans\LaravelSoar\Bootstrapper; 17 | use Illuminate\Console\Events\CommandFinished; 18 | 19 | class OutputScoresListener 20 | { 21 | public function __construct(private readonly Bootstrapper $bootstrapper) {} 22 | 23 | public function handle(CommandFinished $commandFinished): void 24 | { 25 | $this->bootstrapper->outputScores($commandFinished); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/OutputtingEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Events; 15 | 16 | use Guanguans\LaravelSoar\Contracts\OutputContract; 17 | use Illuminate\Console\Events\CommandFinished; 18 | use Illuminate\Support\Collection; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class OutputtingEvent 22 | { 23 | public function __construct( 24 | public readonly OutputContract $output, 25 | public readonly Collection $scores, 26 | public readonly CommandFinished|Response $outputter 27 | ) {} 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/OutputtedEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Events; 15 | 16 | use Guanguans\LaravelSoar\Contracts\OutputContract; 17 | use Illuminate\Console\Events\CommandFinished; 18 | use Illuminate\Support\Collection; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class OutputtedEvent 22 | { 23 | public function __construct( 24 | public readonly OutputContract $output, 25 | public readonly Collection $scores, 26 | public readonly CommandFinished|Response $outputter, 27 | public readonly mixed $result 28 | ) {} 29 | } 30 | -------------------------------------------------------------------------------- /src/Outputs/ClockworkOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | class ClockworkOutput extends AbstractOutput 21 | { 22 | /** 23 | * @noinspection PhpMissingParentCallCommonInspection 24 | */ 25 | public function shouldOutput(CommandFinished|Response $outputter): bool 26 | { 27 | return \function_exists('clock'); 28 | } 29 | 30 | public function output(Collection $scores, CommandFinished|Response $outputter): mixed 31 | { 32 | return clock(...$scores); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Outputs/DumpOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | class DumpOutput extends AbstractOutput 21 | { 22 | public function __construct(private readonly bool $exit = false) {} 23 | 24 | /** 25 | * @noinspection ForgottenDebugOutputInspection 26 | * @noinspection DebugFunctionUsageInspection 27 | * @noinspection PhpVoidFunctionResultUsedInspection 28 | */ 29 | public function output(Collection $scores, CommandFinished|Response $outputter): mixed 30 | { 31 | return $this->exit ? dd(...$scores) : dump(...$scores); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Commands/RunCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Commands; 15 | 16 | use Guanguans\LaravelSoar\Commands\Concerns\WithSoarOptions; 17 | use Illuminate\Console\Command; 18 | 19 | class RunCommand extends Command 20 | { 21 | use WithSoarOptions; 22 | 23 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 24 | protected $signature = 'soar:run'; 25 | 26 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 27 | protected $description = 'Run Soar with the given options'; 28 | 29 | /** 30 | * @throws \Guanguans\SoarPHP\Exceptions\InvalidOptionException 31 | */ 32 | public function handle(): void 33 | { 34 | $this->debugSoar()->run($this->callback()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Outputs/RayOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | class RayOutput extends AbstractOutput 21 | { 22 | public function __construct(private readonly string $label = 'Soar Scores') {} 23 | 24 | /** 25 | * @noinspection PhpMissingParentCallCommonInspection 26 | */ 27 | public function shouldOutput(CommandFinished|Response $outputter): bool 28 | { 29 | return \function_exists('ray'); 30 | } 31 | 32 | public function output(Collection $scores, CommandFinished|Response $outputter): mixed 33 | { 34 | return ray(...$scores)->label($this->label); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Outputs/AbstractOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Guanguans\LaravelSoar\Contracts\OutputContract; 17 | use Guanguans\LaravelSoar\Contracts\SanitizerContract; 18 | use Guanguans\LaravelSoar\Outputs\Concerns\OutputConditions; 19 | use Guanguans\LaravelSoar\Outputs\Concerns\ScoresHydrator; 20 | use Guanguans\LaravelSoar\Outputs\Concerns\ScoresSanitizer; 21 | use Illuminate\Console\Events\CommandFinished; 22 | use Symfony\Component\HttpFoundation\Response; 23 | 24 | abstract class AbstractOutput implements OutputContract, SanitizerContract 25 | { 26 | use OutputConditions; 27 | use ScoresHydrator; 28 | use ScoresSanitizer; 29 | 30 | public function shouldOutput(CommandFinished|Response $outputter): bool 31 | { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2025 guanguans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /src/Outputs/LaraDumpsOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use LaraDumps\LaraDumpsCore\LaraDumps; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class LaraDumpsOutput extends AbstractOutput 22 | { 23 | public function __construct(private readonly string $label = 'Soar Scores') {} 24 | 25 | /** 26 | * @noinspection PhpMissingParentCallCommonInspection 27 | */ 28 | public function shouldOutput(CommandFinished|Response $outputter): bool 29 | { 30 | return \function_exists('ds'); 31 | } 32 | 33 | public function output(Collection $scores, CommandFinished|Response $outputter): LaraDumps 34 | { 35 | return ds(...$scores)->label($this->label); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Outputs/LogOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Illuminate\Support\Facades\Log; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class LogOutput extends AbstractOutput 22 | { 23 | public function __construct( 24 | private readonly string $channel = 'daily', 25 | private readonly string $level = 'warning' 26 | ) {} 27 | 28 | /** 29 | * @throws \JsonException 30 | */ 31 | public function output(Collection $scores, CommandFinished|Response $outputter): CommandFinished|Response 32 | { 33 | $scores->each(fn (array $score): mixed => Log::channel($this->channel)->log( 34 | $this->level, 35 | $this->hydrateScore($score) 36 | )); 37 | 38 | return $outputter; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Soar.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar; 15 | 16 | use Illuminate\Support\Traits\Conditionable; 17 | use Illuminate\Support\Traits\ForwardsCalls; 18 | use Illuminate\Support\Traits\Localizable; 19 | use Illuminate\Support\Traits\Macroable; 20 | use Illuminate\Support\Traits\Tappable; 21 | 22 | class Soar extends \Guanguans\SoarPHP\Soar 23 | { 24 | use Conditionable; 25 | use ForwardsCalls; 26 | use Localizable; 27 | use Macroable { 28 | Macroable::__call as macroCall; 29 | } 30 | use Tappable; 31 | 32 | /** 33 | * @noinspection PhpParameterNameChangedDuringInheritanceInspection 34 | * @noinspection PhpHierarchyChecksInspection 35 | */ 36 | public function __call(string $method, array $parameters): mixed 37 | { 38 | if (self::hasMacro($method)) { 39 | return $this->macroCall($method, $parameters); 40 | } 41 | 42 | return parent::__call($method, $parameters); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Commands/ClearCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Commands; 15 | 16 | use Guanguans\LaravelSoar\Facades\Soar; 17 | use Illuminate\Console\Command; 18 | use Illuminate\Support\Facades\File; 19 | use function Illuminate\Filesystem\join_paths; 20 | 21 | class ClearCommand extends Command 22 | { 23 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 24 | protected $signature = 'soar:clear'; 25 | 26 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 27 | protected $description = 'Clear the Soar log file'; 28 | 29 | public function handle(): void 30 | { 31 | $this->components->info('⏳ Clearing Soar log file...'); 32 | 33 | File::delete($logFile = self::soarLogFile()); 34 | 35 | $this->components->info("✅ The Soar log file [$logFile] has been cleared."); 36 | } 37 | 38 | public static function soarLogFile(): string 39 | { 40 | return Soar::getLogOutput(join_paths(\dirname(Soar::getBinary()), 'soar.log')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Middleware/OutputScoresMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Middleware; 15 | 16 | use Guanguans\LaravelSoar\Bootstrapper; 17 | use Illuminate\Http\JsonResponse; 18 | use Illuminate\Http\RedirectResponse; 19 | use Illuminate\Http\Request; 20 | use Illuminate\Http\Response; 21 | use Illuminate\Support\Collection; 22 | use Symfony\Component\HttpFoundation\Response as SymfonyResponse; 23 | 24 | class OutputScoresMiddleware 25 | { 26 | public function __construct(private readonly Bootstrapper $bootstrapper) {} 27 | 28 | /** 29 | * @noinspection RedundantDocCommentTagInspection 30 | * 31 | * @param \Closure(\Illuminate\Http\Request): (JsonResponse|RedirectResponse|Response) $next 32 | */ 33 | public function handle(Request $request, \Closure $next): SymfonyResponse 34 | { 35 | return tap( 36 | $next($request), 37 | fn (SymfonyResponse $symfonyResponse): Collection => $this->bootstrapper->outputScores($symfonyResponse) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Outputs/Concerns/ScoresHydrator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-soar 14 | */ 15 | 16 | namespace Guanguans\LaravelSoar\Outputs\Concerns; 17 | 18 | use Illuminate\Support\Collection; 19 | use function Guanguans\LaravelSoar\Support\json_pretty_encode; 20 | 21 | trait ScoresHydrator 22 | { 23 | /** 24 | * @noinspection PhpStrictTypeCheckingInspection 25 | * @noinspection PhpIncompatibleReturnTypeInspection 26 | * 27 | * @throws \JsonException 28 | */ 29 | protected function hydrateScores(Collection $scores): string 30 | { 31 | return $scores->reduce( 32 | fn (string $carry, array $score): string => $carry.\PHP_EOL.$this->hydrateScore($score), 33 | '' 34 | ); 35 | } 36 | 37 | /** 38 | * @param array $score 39 | * 40 | * @throws \JsonException 41 | */ 42 | protected function hydrateScore(array $score): string 43 | { 44 | return ($score['Summary'] ?? '').\PHP_EOL.json_pretty_encode($score); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Mixins/QueryBuilderMixin.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view 14 | * the LICENSE file that was distributed with this source code. 15 | * 16 | * @see https://github.com/guanguans/laravel-soar 17 | */ 18 | 19 | namespace Guanguans\LaravelSoar\Mixins; 20 | 21 | use Guanguans\LaravelSoar\Facades\Soar; 22 | 23 | /** 24 | * @mixin \Illuminate\Database\Eloquent\Builder 25 | * @mixin \Illuminate\Database\Eloquent\Relations\Relation 26 | * @mixin \Illuminate\Database\Query\Builder 27 | */ 28 | class QueryBuilderMixin 29 | { 30 | public function ddSoarScore(): \Closure 31 | { 32 | return fn (int $depth = 512, int $options = 0): mixed => dd($this->toSoarScore($depth, $options)); // @codeCoverageIgnore 33 | } 34 | 35 | public function dumpSoarScore(): \Closure 36 | { 37 | return function (int $depth = 512, int $options = 0): self { 38 | dump($this->toSoarScore($depth, $options)); 39 | 40 | return $this; 41 | }; 42 | } 43 | 44 | public function toSoarScore(): \Closure 45 | { 46 | return fn (int $depth = 512, int $options = 0): array => Soar::arrayScores( 47 | $this->toRawSql(), 48 | $depth, 49 | $options 50 | )[0]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Outputs/JsonOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Arr; 18 | use Illuminate\Support\Collection; 19 | use Symfony\Component\HttpFoundation\JsonResponse; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | class JsonOutput extends AbstractOutput 23 | { 24 | public function __construct(private readonly string $key = 'soar_scores') {} 25 | 26 | /** 27 | * @noinspection PhpMissingParentCallCommonInspection 28 | */ 29 | public function shouldOutput(CommandFinished|Response $outputter): bool 30 | { 31 | return $this->isJsonResponse($outputter); 32 | } 33 | 34 | /** 35 | * @throws \JsonException 36 | */ 37 | public function output(Collection $scores, CommandFinished|Response $outputter): JsonResponse 38 | { 39 | \assert($outputter instanceof JsonResponse); 40 | 41 | // $data = $outputter->getData(true); 42 | $data = json_decode($outputter->getContent(), true, 512, \JSON_THROW_ON_ERROR); 43 | Arr::set($data, $this->key, $scores); 44 | 45 | // Update the new content and reset the content length 46 | $outputter->setData($data); 47 | $outputter->headers->remove('Content-Length'); 48 | 49 | return $outputter; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Outputs/Concerns/OutputConditions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-soar 14 | */ 15 | 16 | namespace Guanguans\LaravelSoar\Outputs\Concerns; 17 | 18 | use Illuminate\Console\Events\CommandFinished; 19 | use Symfony\Component\HttpFoundation\JsonResponse; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | trait OutputConditions 23 | { 24 | protected function isHtmlResponse(CommandFinished|Response $outputter): bool 25 | { 26 | return $outputter instanceof Response 27 | && \is_string($outputter->getContent()) 28 | && str($outputter->headers->get('Content-Type'))->contains('text/html') 29 | && !$this->isJsonResponse($outputter); 30 | } 31 | 32 | protected function isJsonResponse(CommandFinished|Response $outputter): bool 33 | { 34 | return $outputter instanceof JsonResponse 35 | && \is_string($outputter->getContent()) 36 | && str($outputter->headers->get('Content-Type'))->contains('application/json') 37 | && transform($outputter, static function (JsonResponse $jsonResponse): bool { 38 | try { 39 | return \is_array(json_decode($jsonResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR)); 40 | } catch (\JsonException) { 41 | return false; 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/ScoreCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Commands; 15 | 16 | use Guanguans\LaravelSoar\Commands\Concerns\WithSoarOptions; 17 | use Illuminate\Console\Command; 18 | 19 | class ScoreCommand extends Command 20 | { 21 | use WithSoarOptions; 22 | 23 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 24 | protected $signature = 'soar:score'; 25 | 26 | /** @noinspection ClassOverridesFieldOfSuperClassInspection */ 27 | protected $description = 'Get the Soar scores of the given SQL statements'; 28 | 29 | /** 30 | * @noinspection MethodShouldBeFinalInspection 31 | * @noinspection OffsetOperationsInspection 32 | * @noinspection SqlResolve 33 | * 34 | * @throws \Guanguans\SoarPHP\Exceptions\InvalidOptionException 35 | */ 36 | public function handle(): void 37 | { 38 | $query = $this->soar()->getQuery(); 39 | 40 | // If the query is not passed in, read from STDIN 41 | if (($fstat = fstat(\STDIN)) && 0 < $fstat['size']) { 42 | $query = trim(stream_get_contents(\STDIN)); 43 | fclose(\STDIN); 44 | } 45 | 46 | while (blank($query)) { 47 | if (filled($query = $this->ask('Please input the SQL statements', 'select * from foo;'))) { 48 | break; 49 | } 50 | } 51 | 52 | $this->debugSoar()->scores($query, $this->callback()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /_ide_helper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace { 15 | class Soar extends Guanguans\LaravelSoar\Facades\Soar {} 16 | } 17 | 18 | namespace Illuminate\Database\Query { 19 | /** 20 | * @method never ddSoarScore(int $depth = 512, int $options = 0) 21 | * @method self dumpSoarScore(int $depth = 512, int $options = 0) 22 | * @method array toSoarScore(int $depth = 512, int $options = 0) 23 | * 24 | * @mixin \Illuminate\Database\Eloquent\Builder 25 | * 26 | * @see \Guanguans\LaravelSoar\Mixins\QueryBuilderMixin 27 | * @see \Illuminate\Database\Query\Builder 28 | */ 29 | class Builder {} 30 | } 31 | 32 | namespace Illuminate\Database\Eloquent { 33 | /** 34 | * @method never ddSoarScore(int $depth = 512, int $options = 0) 35 | * @method self dumpSoarScore(int $depth = 512, int $options = 0) 36 | * @method array toSoarScore(int $depth = 512, int $options = 0) 37 | * 38 | * @mixin \Illuminate\Database\Query\Builder 39 | * 40 | * @see \Guanguans\LaravelSoar\Mixins\QueryBuilderMixin 41 | * @see \Illuminate\Database\Eloquent\Builder 42 | */ 43 | class Builder {} 44 | } 45 | 46 | namespace Illuminate\Database\Eloquent\Relations { 47 | /** 48 | * @method never ddSoarScore(int $depth = 512, int $options = 0) 49 | * @method self dumpSoarScore(int $depth = 512, int $options = 0) 50 | * @method array toSoarScore(int $depth = 512, int $options = 0) 51 | * 52 | * @mixin \Illuminate\Database\Eloquent\Builder 53 | * 54 | * @see \Guanguans\LaravelSoar\Mixins\QueryBuilderMixin 55 | * @see \Illuminate\Database\Eloquent\Relations\Relation 56 | */ 57 | class Relation {} 58 | } 59 | -------------------------------------------------------------------------------- /src/Outputs/DebugBarOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Barryvdh\Debugbar\LaravelDebugbar; 17 | use DebugBar\DataCollector\MessagesCollector; 18 | use Illuminate\Console\Events\CommandFinished; 19 | use Illuminate\Support\Collection; 20 | use Symfony\Component\HttpFoundation\Response; 21 | 22 | class DebugBarOutput extends AbstractOutput 23 | { 24 | public function __construct( 25 | private readonly string $name = 'Soar Scores', 26 | private readonly string $label = 'warning' 27 | ) {} 28 | 29 | /** 30 | * @noinspection PhpMissingParentCallCommonInspection 31 | */ 32 | public function shouldOutput(CommandFinished|Response $outputter): bool 33 | { 34 | return class_exists(LaravelDebugbar::class) 35 | && app()->has(LaravelDebugbar::class) 36 | // && resolve(LaravelDebugbar::class)->isEnabled() 37 | && $this->isHtmlResponse($outputter); 38 | } 39 | 40 | /** 41 | * @noinspection PhpPossiblePolymorphicInvocationInspection 42 | * 43 | * @throws \DebugBar\DebugBarException 44 | * @throws \JsonException 45 | */ 46 | public function output(Collection $scores, CommandFinished|Response $outputter): LaravelDebugbar 47 | { 48 | $debugBar = resolve(LaravelDebugbar::class); 49 | 50 | \assert($debugBar instanceof LaravelDebugbar); 51 | 52 | if (!$debugBar->hasCollector($this->name)) { 53 | $debugBar->addCollector(new MessagesCollector($this->name)); 54 | } 55 | 56 | $scores->each(fn (array $score) => $debugBar->getCollector($this->name)->addMessage( 57 | $this->hydrateScore($score), 58 | $this->label, 59 | false 60 | )); 61 | 62 | return $debugBar; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Outputs/ConsoleOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Outputs; 15 | 16 | use Illuminate\Console\Events\CommandFinished; 17 | use Illuminate\Support\Collection; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | class ConsoleOutput extends AbstractOutput 21 | { 22 | public function __construct(private readonly string $method = 'warn') {} 23 | 24 | /** 25 | * @noinspection PhpMissingParentCallCommonInspection 26 | */ 27 | public function shouldOutput(CommandFinished|Response $outputter): bool 28 | { 29 | return $this->isHtmlResponse($outputter); 30 | } 31 | 32 | /** 33 | * @throws \JsonException 34 | */ 35 | public function output(Collection $scores, CommandFinished|Response $outputter): Response 36 | { 37 | \assert($outputter instanceof Response); 38 | 39 | $js = $this->toJavaScript($scores); 40 | $content = $outputter->getContent(); 41 | 42 | // Try to put the widget at the end, directly before the 43 | $pos = strripos($content, ''); 44 | $content = false !== $pos ? substr($content, 0, $pos).$js.substr($content, $pos) : $content.$js; 45 | 46 | // Update the new content and reset the content length 47 | $outputter->setContent($content); 48 | $outputter->headers->remove('Content-Length'); 49 | 50 | return $outputter; 51 | } 52 | 53 | /** 54 | * @throws \JsonException 55 | */ 56 | private function toJavaScript(Collection $scores): string 57 | { 58 | return \sprintf( 59 | /** @lang JavaScript */ 60 | '', 61 | $this->method, 62 | str_replace('`', '\`', $this->hydrateScores($scores)), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Commands/Concerns/WithSoarOptions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-soar 14 | */ 15 | 16 | namespace Guanguans\LaravelSoar\Commands\Concerns; 17 | 18 | use Guanguans\LaravelSoar\Soar; 19 | use Illuminate\Support\Str; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Process\Process; 22 | 23 | /** 24 | * @mixin \Illuminate\Console\Command 25 | */ 26 | trait WithSoarOptions 27 | { 28 | protected function configure(): void 29 | { 30 | $this->setDefinition([ 31 | new InputOption( 32 | 'option', 33 | null, 34 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 35 | 'Specify Soar option. Example: `--option=-report-type=markdown` or `--option report-type=markdown`. Can be used multiple times.', 36 | ), 37 | ]); 38 | } 39 | 40 | protected function debugSoar(): Soar 41 | { 42 | $soar = $this->soar(); 43 | 44 | if ($this->output->isDebug()) { 45 | $soar->dump(); 46 | $this->output->newLine(); 47 | } 48 | 49 | return $soar; 50 | } 51 | 52 | protected function soar(): Soar 53 | { 54 | return resolve(Soar::class)->withOptions($this->normalizedOptions()); 55 | } 56 | 57 | protected function normalizedOptions(): array 58 | { 59 | return collect($this->option('option')) 60 | ->mapWithKeys(static function (string $option): array { 61 | [$key, $value] = str($option)->explode('=', 2)->pad(2, null)->all(); 62 | 63 | return [Str::start($key, '-') => $value]; 64 | }) 65 | ->all(); 66 | } 67 | 68 | protected function callback(): \Closure 69 | { 70 | return function (string $type, string $line): void { 71 | Process::ERR === $type ? $this->error($line) : $this->info($line); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/OutputManager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar; 15 | 16 | use Guanguans\LaravelSoar\Contracts\OutputContract; 17 | use Guanguans\LaravelSoar\Contracts\SanitizerContract; 18 | use Guanguans\LaravelSoar\Events\OutputtedEvent; 19 | use Guanguans\LaravelSoar\Events\OutputtingEvent; 20 | use Illuminate\Console\Events\CommandFinished; 21 | use Illuminate\Support\Collection; 22 | use Illuminate\Support\Facades\Request; 23 | use Illuminate\Support\Fluent; 24 | use Illuminate\Support\Str; 25 | use Symfony\Component\HttpFoundation\Response; 26 | 27 | class OutputManager extends Fluent implements OutputContract 28 | { 29 | public function shouldOutput(CommandFinished|Response $outputter): bool 30 | { 31 | $except = config('soar.except', []); 32 | 33 | if ($outputter instanceof CommandFinished) { 34 | return !Str::is($except, $outputter->command); 35 | } 36 | 37 | return !Request::is($except) && !Request::routeIs($except); 38 | } 39 | 40 | /** 41 | * @noinspection PhpUndefinedMethodInspection 42 | * @noinspection NullPointerExceptionInspection 43 | */ 44 | public function output(Collection $scores, CommandFinished|Response $outputter): Collection 45 | { 46 | if (!$this->shouldOutput($outputter)) { 47 | return collect(); 48 | } 49 | 50 | return collect($this->attributes) 51 | ->tap(fn () => event(new OutputtingEvent($this, $scores, $outputter))) 52 | ->reduce( 53 | static function (Collection $results, OutputContract $outputContract) use ($outputter, $scores): Collection { 54 | if (!$outputContract->shouldOutput($outputter)) { 55 | return $results; 56 | } 57 | 58 | if ($outputContract instanceof SanitizerContract) { 59 | $scores = $outputContract->sanitize($scores); 60 | } 61 | 62 | return $results->put($outputContract::class, $outputContract->output($scores, $outputter)); 63 | }, 64 | collect() 65 | ) 66 | ->tap(fn (Collection $results) => event(new OutputtedEvent($this, $scores, $outputter, $results))); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Support/Utils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Support; 15 | 16 | use Illuminate\Database\Events\QueryExecuted; 17 | use Illuminate\Support\Str; 18 | 19 | class Utils 20 | { 21 | /** 22 | * @noinspection DebugFunctionUsageInspection 23 | * 24 | * @param int|list $forgetLines 25 | */ 26 | public static function backtraces(int $limit = 0, array|int $forgetLines = 0): array 27 | { 28 | return collect(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $limit)) 29 | ->forget($forgetLines) 30 | ->filter( 31 | static fn (array $trace): bool => isset($trace['file'], $trace['line']) 32 | && !Str::contains($trace['file'], 'vendor') 33 | ) 34 | ->map(static fn (array $trace, int $index): string => \sprintf( 35 | '#%s %s:%s', 36 | $index, 37 | str_replace(Str::finish(base_path(), \DIRECTORY_SEPARATOR), '', $trace['file']), 38 | $trace['line'] 39 | )) 40 | ->all(); 41 | } 42 | 43 | public static function isExceptQuery(string $query): bool 44 | { 45 | return Str::is(config('soar.except_queries', []), $query); 46 | } 47 | 48 | public static function sanitizeExplain(?array $explain): array 49 | { 50 | return collect($explain) 51 | ->map(static function (array $explain): array { 52 | $explain['Content'] = str($explain['Content'])->explode(\PHP_EOL)->filter()->values()->all(); 53 | $explain['Case'] = str($explain['Case'])->explode(\PHP_EOL)->filter()->values()->all(); 54 | 55 | return $explain; 56 | }) 57 | ->all(); 58 | } 59 | 60 | public static function star(int $score): string 61 | { 62 | return str_repeat('★', $good = (int) round($score / 100 * 5)).str_repeat('☆', 5 - $good); 63 | } 64 | 65 | /** 66 | * @see \Illuminate\Database\Query\Builder::toRawSql() 67 | * @see https://github.com/laravel/framework/blob/12.x/src/Illuminate/Database/Events/QueryExecuted.php 68 | * @see \Laravel\Telescope\Watchers\QueryWatcher::replaceBindings() 69 | * @see addslashes() 70 | * @see addcslashes() 71 | * @see stripslashes() 72 | * @see stripcslashes() 73 | * @see quotemeta() 74 | * @see mysqli_real_escape_string() 75 | * @see PDO::quote() 76 | * @see var_export() 77 | * @see json_encode() 78 | */ 79 | public static function toRawSql(QueryExecuted $queryExecuted): string 80 | { 81 | if (method_exists($queryExecuted, 'toRawSql')) { 82 | return $queryExecuted->toRawSql(); // @codeCoverageIgnore 83 | } 84 | 85 | return $queryExecuted->connection 86 | ->query() 87 | ->getGrammar() 88 | ->substituteBindingsIntoRawSql( 89 | $queryExecuted->sql, 90 | $queryExecuted->connection->prepareBindings($queryExecuted->bindings) 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Facades/Soar.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | * 13 | * @see https://github.com/guanguans/laravel-soar 14 | */ 15 | 16 | namespace Guanguans\LaravelSoar\Facades; 17 | 18 | use Illuminate\Support\Facades\Facade; 19 | 20 | /** 21 | * @method static string help(null|callable $callback = null) 22 | * @method static string version(null|callable $callback = null) 23 | * @method static \Guanguans\SoarPHP\Soar clone() 24 | * @method static array arrayScores(array|string $queries, int $depth = 512, int $options = 0) 25 | * @method static string jsonScores(array|string $queries) 26 | * @method static string htmlScores(array|string $queries) 27 | * @method static string markdownScores(array|string $queries) 28 | * @method static string scores(array|string $queries, null|callable $callback = null) 29 | * @method static string getBinary() 30 | * @method static \Guanguans\SoarPHP\Soar withBinary(string $binary) 31 | * @method static string defaultBinary() 32 | * @method static \Guanguans\SoarPHP\Soar flushOptions() 33 | * @method static \Guanguans\SoarPHP\Soar setOption(string $name, mixed $value) 34 | * @method static \Guanguans\SoarPHP\Soar setOptions(array $options) 35 | * @method static \Guanguans\SoarPHP\Soar withOption(string $name, mixed $value) 36 | * @method static \Guanguans\SoarPHP\Soar withOptions(array $options) 37 | * @method static mixed getOption(string $name, mixed $default = null) 38 | * @method static array getOptions() 39 | * @method static \Guanguans\SoarPHP\Soar onlyDsn() 40 | * @method static \Guanguans\SoarPHP\Soar onlyOption(string $name) 41 | * @method static \Guanguans\SoarPHP\Soar onlyOptions(array $names) 42 | * @method static \Guanguans\SoarPHP\Soar exceptOption(string $name) 43 | * @method static \Guanguans\SoarPHP\Soar exceptOptions(array $names) 44 | * @method static string|null getSudoPassword() 45 | * @method static \Guanguans\SoarPHP\Soar withoutSudoPassword() 46 | * @method static \Guanguans\SoarPHP\Soar withSudoPassword(string|null $sudoPassword) 47 | * @method static \Guanguans\SoarPHP\Soar make(mixed ...$parameters) 48 | * @method static void dd(mixed ...$args) 49 | * @method static \Guanguans\SoarPHP\Soar dump(mixed ...$args) 50 | * @method static \Guanguans\SoarPHP\Soar withPipe(\Closure|null $pipe) 51 | * @method static \Guanguans\SoarPHP\Soar withTap(\Closure|null $tap) 52 | * @method static string run(null|callable $callback = null) 53 | * @method static \Guanguans\LaravelSoar\Soar|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) 54 | * @method static \Guanguans\LaravelSoar\Soar|mixed unless(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null) 55 | * @method static mixed withLocale(string $locale, \Closure $callback) 56 | * @method static void macro(string $name, object|callable $macro) 57 | * @method static void mixin(object $mixin, bool $replace = true) 58 | * @method static bool hasMacro(string $name) 59 | * @method static void flushMacros() 60 | * @method static mixed macroCall(string $method, array $parameters) 61 | * @method static \Guanguans\LaravelSoar\Soar|\Illuminate\Support\HigherOrderTapProxy tap(callable|null $callback = null) 62 | * 63 | * @see \Guanguans\LaravelSoar\Soar 64 | */ 65 | class Soar extends Facade 66 | { 67 | /** 68 | * @noinspection PhpMissingParentCallCommonInspection 69 | * @noinspection MethodVisibilityInspection 70 | */ 71 | protected static function getFacadeAccessor(): string 72 | { 73 | return \Guanguans\LaravelSoar\Soar::class; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Support/helpers.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar\Support; 15 | 16 | use Carbon\CarbonInterval; 17 | use Composer\Autoload\ClassLoader; 18 | use Illuminate\Support\Collection; 19 | 20 | if (!\function_exists('Guanguans\LaravelSoar\Support\classes')) { 21 | /** 22 | * @see https://github.com/illuminate/collections 23 | * @see https://github.com/alekitto/class-finder 24 | * @see https://github.com/ergebnis/classy 25 | * @see https://gitlab.com/hpierce1102/ClassFinder 26 | * @see https://packagist.org/packages/haydenpierce/class-finder 27 | * @see \get_declared_classes() 28 | * @see \get_declared_interfaces() 29 | * @see \get_declared_traits() 30 | * @see \DG\BypassFinals::enable() 31 | * @see \Composer\Util\ErrorHandler 32 | * @see \Monolog\ErrorHandler 33 | * @see \PhpCsFixer\ExecutorWithoutErrorHandler 34 | * @see \Phrity\Util\ErrorHandler 35 | * 36 | * @noinspection RedundantDocCommentTagInspection 37 | * @noinspection PhpUndefinedNamespaceInspection 38 | * 39 | * @param null|(callable(class-string, string): bool) $filter 40 | * 41 | * @return \Illuminate\Support\Collection 42 | */ 43 | function classes(?callable $filter = null): Collection 44 | { 45 | static $classes; 46 | $classes ??= collect(spl_autoload_functions())->flatMap( 47 | static fn (mixed $loader): array => \is_array($loader) && $loader[0] instanceof ClassLoader 48 | ? $loader[0]->getClassMap() 49 | : [] 50 | ); 51 | 52 | return $classes 53 | ->when( 54 | \is_callable($filter), 55 | static fn (Collection $classes): Collection => $classes->filter( 56 | static fn (string $file, string $class) => $filter($class, $file) 57 | ) 58 | ) 59 | ->mapWithKeys(static function (string $file, string $class): array { 60 | try { 61 | return [$class => new \ReflectionClass($class)]; 62 | } catch (\Throwable $throwable) { 63 | return [$class => $throwable]; 64 | } 65 | }); 66 | } 67 | } 68 | 69 | if (!\function_exists('Guanguans\LaravelSoar\Support\env_explode')) { 70 | /** 71 | * @noinspection LaravelFunctionsInspection 72 | */ 73 | function env_explode(string $key, mixed $default = null, string $delimiter = ',', int $limit = \PHP_INT_MAX): mixed 74 | { 75 | $env = env($key, $default); 76 | 77 | if (\is_string($env)) { 78 | return $env ? explode($delimiter, $env, $limit) : []; 79 | } 80 | 81 | return $env; 82 | } 83 | } 84 | 85 | if (!\function_exists('Guanguans\LaravelSoar\Support\human_milliseconds')) { 86 | /** 87 | * @noinspection PhpUnhandledExceptionInspection 88 | */ 89 | function human_milliseconds(float|int $milliseconds, array $syntax = []): string 90 | { 91 | return CarbonInterval::microseconds($milliseconds * 1000) 92 | ->cascade() 93 | ->forHumans($syntax + [ 94 | 'join' => ', ', 95 | 'locale' => 'en', 96 | // 'locale' => 'zh_CN', 97 | 'minimumUnit' => 'us', 98 | 'short' => true, 99 | ]); 100 | } 101 | } 102 | 103 | if (!\function_exists('Guanguans\LaravelSoar\Support\json_pretty_encode')) { 104 | /** 105 | * @param int<1, 4194304> $depth 106 | * 107 | * @throws \JsonException 108 | */ 109 | function json_pretty_encode(mixed $value, int $options = 0, int $depth = 512): string 110 | { 111 | return json_encode( 112 | $value, 113 | \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | $options, 114 | $depth 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/SoarServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar; 15 | 16 | use Composer\InstalledVersions; 17 | use Guanguans\LaravelSoar\Commands\ClearCommand; 18 | use Guanguans\LaravelSoar\Commands\RunCommand; 19 | use Guanguans\LaravelSoar\Commands\ScoreCommand; 20 | use Guanguans\LaravelSoar\Mixins\QueryBuilderMixin; 21 | use Illuminate\Database\Eloquent\Builder as EloquentBuilder; 22 | use Illuminate\Database\Eloquent\Relations\Relation as RelationBuilder; 23 | use Illuminate\Database\Query\Builder as QueryBuilder; 24 | use Illuminate\Foundation\Application; 25 | use Illuminate\Foundation\Console\AboutCommand; 26 | use Illuminate\Support\Collection; 27 | use Illuminate\Support\ServiceProvider; 28 | 29 | class SoarServiceProvider extends ServiceProvider 30 | { 31 | /** @var list */ 32 | public array $singletons = [ 33 | Bootstrapper::class, 34 | ]; 35 | 36 | /** 37 | * @noinspection PhpMissingParentCallCommonInspection 38 | * 39 | * @throws \ReflectionException 40 | */ 41 | public function register(): void 42 | { 43 | $this->setupConfig(); 44 | $this->registerMixins(); 45 | $this->registerOutputManager(); 46 | $this->registerSoar(); 47 | } 48 | 49 | public function boot(): void 50 | { 51 | $this->registerCommands(); 52 | 53 | if (config('soar.enabled', false)) { 54 | $this->app->booted(static fn (Application $application) => $application->make(Bootstrapper::class)->boot()); 55 | } 56 | } 57 | 58 | /** 59 | * @noinspection PhpMissingParentCallCommonInspection 60 | */ 61 | public function provides(): array 62 | { 63 | return [ 64 | Bootstrapper::class, 65 | OutputManager::class, 66 | Soar::class, 67 | ]; 68 | } 69 | 70 | /** 71 | * @noinspection RealpathInStreamContextInspection 72 | */ 73 | private function setupConfig(): void 74 | { 75 | $source = realpath($raw = __DIR__.'/../config/soar.php') ?: $raw; 76 | 77 | if ($this->app->runningInConsole()) { 78 | $this->publishes([$source => config_path('soar.php')], 'laravel-soar'); 79 | } 80 | 81 | $this->mergeConfigFrom($source, 'soar'); 82 | } 83 | 84 | /** 85 | * @throws \ReflectionException 86 | */ 87 | private function registerMixins(): void 88 | { 89 | $queryBuilderMixin = new QueryBuilderMixin; 90 | EloquentBuilder::mixin($queryBuilderMixin); 91 | QueryBuilder::mixin($queryBuilderMixin); 92 | RelationBuilder::mixin($queryBuilderMixin); 93 | } 94 | 95 | /** 96 | * @noinspection PhpIncompatibleReturnTypeInspection 97 | */ 98 | private function registerOutputManager(): void 99 | { 100 | $this->app->singleton( 101 | OutputManager::class, 102 | static fn (Application $application): OutputManager => collect(config('soar.outputs')) 103 | ->mapWithKeys(static function (array|string $parameters, int|string $class) use ($application): array { 104 | if (!\is_array($parameters)) { 105 | [$parameters, $class] = [(array) $class, $parameters]; 106 | } 107 | 108 | return [$class => $application->make($class, $parameters)]; 109 | }) 110 | ->pipe(static fn (Collection $outputs): OutputManager => new OutputManager($outputs->all())) 111 | ); 112 | } 113 | 114 | private function registerSoar(): void 115 | { 116 | $this->app->singleton( 117 | Soar::class, 118 | static fn (): Soar => Soar::make( 119 | config('soar.options', []), 120 | config('soar.binary') 121 | )->withSudoPassword(config('soar.sudo_password')) 122 | ); 123 | } 124 | 125 | private function registerCommands(): void 126 | { 127 | if ($this->app->runningInConsole()) { 128 | $this->commands([ 129 | ClearCommand::class, 130 | RunCommand::class, 131 | ScoreCommand::class, 132 | ]); 133 | 134 | $this->addSectionToAboutCommand(); 135 | } 136 | } 137 | 138 | private function addSectionToAboutCommand(): void 139 | { 140 | AboutCommand::add( 141 | str($package = 'guanguans/laravel-soar')->headline()->toString(), 142 | static fn (): array => collect(['Homepage' => "https://github.com/$package"]) 143 | ->when( 144 | class_exists(InstalledVersions::class), 145 | static fn (Collection $data): Collection => $data->put( 146 | 'Version', 147 | InstalledVersions::getPrettyVersion($package) 148 | ) 149 | ) 150 | ->all() 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Bootstrapper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | namespace Guanguans\LaravelSoar; 15 | 16 | use Guanguans\LaravelSoar\Facades\Soar; 17 | use Guanguans\LaravelSoar\Listeners\OutputScoresListener; 18 | use Guanguans\LaravelSoar\Middleware\OutputScoresMiddleware; 19 | use Guanguans\LaravelSoar\Support\Utils; 20 | use Illuminate\Console\Events\CommandFinished; 21 | use Illuminate\Contracts\Http\Kernel; 22 | use Illuminate\Database\Events\QueryExecuted; 23 | use Illuminate\Foundation\Application; 24 | use Illuminate\Support\Collection; 25 | use Illuminate\Support\Facades\Event; 26 | use Symfony\Component\HttpFoundation\Response; 27 | use function Guanguans\LaravelSoar\Support\human_milliseconds; 28 | 29 | class Bootstrapper 30 | { 31 | private bool $booted = false; 32 | private static Collection $queries; 33 | private static Collection $scores; 34 | 35 | public function __construct(private readonly Application $application) 36 | { 37 | self::$queries = collect(); 38 | self::$scores = collect(); 39 | } 40 | 41 | public function boot(): void 42 | { 43 | if ($this->booted) { 44 | return; 45 | } 46 | 47 | $this->booted = true; 48 | $this->logQueries(); 49 | $this->registerOutputter(); 50 | } 51 | 52 | public function outputScores(CommandFinished|Response $outputter): Collection 53 | { 54 | return $this->application->make(OutputManager::class)->output($this->getScores(), $outputter); 55 | } 56 | 57 | private function logQueries(): void 58 | { 59 | Event::listen(QueryExecuted::class, static function (QueryExecuted $queryExecuted): void { 60 | if ( 61 | self::$queries->has($queryExecuted->sql) 62 | || Utils::isExceptQuery($queryExecuted->sql) 63 | || Utils::isExceptQuery($rawSql = Utils::toRawSql($queryExecuted)) 64 | ) { 65 | return; 66 | } 67 | 68 | self::$queries->put($queryExecuted->sql, [ 69 | 'sql' => $rawSql, 70 | 'time' => human_milliseconds($queryExecuted->time), 71 | 'connection' => $queryExecuted->connectionName, 72 | 'driver' => $queryExecuted->connection->getDriverName(), 73 | 'backtraces' => Utils::backtraces(), 74 | ]); 75 | }); 76 | } 77 | 78 | private function registerOutputter(): void 79 | { 80 | Event::listen(CommandFinished::class, OutputScoresListener::class); 81 | $this->application->make(Kernel::class)->prependMiddleware(OutputScoresMiddleware::class); 82 | } 83 | 84 | private function getScores(): Collection 85 | { 86 | return self::$scores = self::$scores->whenEmpty(fn (): Collection => $this->toScores()); 87 | } 88 | 89 | private function toScores(): Collection 90 | { 91 | return self::$queries->whenNotEmpty( 92 | fn (Collection $queries): Collection => collect(Soar::arrayScores($queries->pluck('sql')->all())) 93 | ->sortBy(['Score', 'Fingerprint']) 94 | ->map(fn (array $score): array => $this->hydrateScore($score)) 95 | ->values() 96 | ); 97 | } 98 | 99 | /** 100 | * @param array $score 101 | * 102 | * @return array 103 | */ 104 | private function hydrateScore(array $score): array 105 | { 106 | $query = $this->matchQuery($score); 107 | 108 | return [ 109 | 'Summary' => \sprintf( 110 | '[%s|%d分|%s|%s]', 111 | $star = Utils::star($score['Score']), 112 | $score['Score'], 113 | $query['time'], 114 | $query['sql'] 115 | ), 116 | 'Basic' => [ 117 | 'Sample' => $query['sql'], 118 | 'Score' => $score['Score'], 119 | 'Star' => $star, 120 | 'Time' => $query['time'], 121 | 'Connection' => $query['connection'], 122 | 'Driver' => $query['driver'], 123 | 'Tables' => (array) $score['Tables'], 124 | ], 125 | 'HeuristicRules' => (array) $score['HeuristicRules'], 126 | 'IndexRules' => (array) $score['IndexRules'], 127 | 'Explain' => Utils::sanitizeExplain($score['Explain']), 128 | 'Backtraces' => $query['backtraces'], 129 | ]; 130 | } 131 | 132 | /** 133 | * @noinspection PhpIncompatibleReturnTypeInspection 134 | * 135 | * @param array $score 136 | * 137 | * @return array{ 138 | * sql: string, 139 | * time: string, 140 | * connection: string, 141 | * driver: string, 142 | * backtraces: list 143 | * } 144 | */ 145 | private function matchQuery(array $score): array 146 | { 147 | return self::$queries->first( 148 | static fn (array $query): bool => $score['Sample'] === $query['sql'], 149 | static fn (): array => self::$queries 150 | ->map(static function (array $query) use ($score): array { 151 | $query['similarity'] = similar_text((string) $score['Sample'], (string) $query['sql']); 152 | 153 | return $query; 154 | }) 155 | ->sortByDesc('similarity') 156 | ->first() 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /config/soar.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | * 11 | * @see https://github.com/guanguans/laravel-soar 12 | */ 13 | 14 | use Guanguans\LaravelSoar\Outputs; 15 | use function Guanguans\LaravelSoar\Support\env_explode; 16 | 17 | return [ 18 | // 是否启用自动评分 19 | 'enabled' => (bool) env('SOAR_ENABLED', false), 20 | 21 | // 二进制文件 22 | 'binary' => env('SOAR_BINARY'), 23 | 24 | // sudo 密码(在 unix 操作系统非 cli 环境中运行时需要设置) 25 | 'sudo_password' => env('SOAR_SUDO_PASSWORD'), 26 | 27 | // 排除评分的命令、请求路径、请求路由 28 | 'except' => env_explode('SOAR_EXCEPT', [ 29 | 'telescope:*', 30 | '*telescope*', 31 | ]), 32 | 33 | // 排除评分的查询 34 | 'except_queries' => env_explode('SOAR_EXCEPT_QUERIES', [ 35 | '^use \?$', 36 | '^set.*', 37 | '^show.*', 38 | '^select \?$', 39 | '^\/\*.*\*\/$', 40 | '^drop.*', 41 | '^lock.*', 42 | '^unlock.*', 43 | 44 | '*telescope*', 45 | '*horizon*', 46 | // 'create table*', 47 | ]), 48 | 49 | // 评分输出器 50 | 'outputs' => [ 51 | // Outputs\ClockworkOutput::class, 52 | // Outputs\ConsoleOutput::class => ['method' => 'warn'], 53 | // Outputs\DebugBarOutput::class => ['name' => 'Soar Scores', 'label' => 'warning'], 54 | // Outputs\DumpOutput::class => ['exit' => false], 55 | // Outputs\JsonOutput::class => ['key' => 'soar_scores'], 56 | // Outputs\LaraDumpsOutput::class => ['label' => 'Soar Scores'], 57 | Outputs\LogOutput::class => ['channel' => 'daily', 'level' => 'warning'], 58 | // Outputs\RayOutput::class => ['label' => 'Soar Scores'], 59 | ], 60 | 61 | /** 62 | * @see https://github.com/guanguans/soar-php/blob/master/examples/options-example.php 63 | * @see https://github.com/guanguans/soar-php/blob/master/examples/options.php 64 | * @see https://github.com/XiaoMi/soar 65 | */ 66 | 'options' => [ 67 | // // Config file path 68 | // '-config' => null, 69 | 70 | // TestDSN, 测试环境数据库配置, username:********@tcp(ip:port)/schema (default "tcp/information_schema?timeout=3s&charset=utf8") 71 | '-test-dsn' => [ 72 | 'host' => env('SOAR_TEST_DSN_HOST', config('database.connections.mysql.host', 'you_host')), 73 | 'port' => env('SOAR_TEST_DSN_PORT', config('database.connections.mysql.port', 'you_port')), 74 | 'schema' => env('SOAR_TEST_DSN_SCHEMA', config('database.connections.mysql.database', 'you_schema')), 75 | 'user' => env('SOAR_TEST_DSN_USER', config('database.connections.mysql.username', 'you_user')), 76 | 'password' => env('SOAR_TEST_DSN_PASSWORD', config('database.connections.mysql.password', 'you_password')), 77 | 'disable' => env('SOAR_TEST_DSN_DISABLE', false), 78 | ], 79 | 80 | // OnlineDSN, 线上环境数据库配置(数据库用户只需 select 权限), username:********@tcp(ip:port)/schema (default "tcp/information_schema?timeout=3s&charset=utf8") 81 | '-online-dsn' => [ 82 | 'host' => env('SOAR_ONLINE_DSN_HOST', config('database.connections.mysql.host', 'you_host')), 83 | 'port' => env('SOAR_ONLINE_DSN_PORT', config('database.connections.mysql.port', 'you_port')), 84 | 'schema' => env('SOAR_ONLINE_DSN_SCHEMA', config('database.connections.mysql.database', 'you_schema')), 85 | 'user' => env('SOAR_ONLINE_DSN_USER', config('database.connections.mysql.username', 'you_user')), 86 | 'password' => env('SOAR_ONLINE_DSN_PASSWORD', config('database.connections.mysql.password', 'you_password')), 87 | 'disable' => env('SOAR_ONLINE_DSN_DISABLE', true), 88 | ], 89 | 90 | // AllowOnlineAsTest, 允许线上环境也可以当作测试环境 91 | '-allow-online-as-test' => env('SOAR_ALLOW_ONLINE_AS_TEST', true), 92 | 93 | // 指定 blacklist 配置文件的位置,文件中的 SQL 不会被评审。一行一条SQL,可以是指纹,也可以是正则 94 | '-blacklist' => env('SOAR_BLACKLIST', base_path('vendor/guanguans/soar-php/examples/blacklist.example')), 95 | 96 | // Explain, 是否开启Explain执行计划分析 (default true) 97 | '-explain' => env('SOAR_EXPLAIN', true), 98 | 99 | // IgnoreRules, 忽略的优化建议规则 (default "COL.011") 100 | '-ignore-rules' => env_explode('SOAR_IGNORE_RULES', [ 101 | 'COL.011', 102 | ]), 103 | 104 | // LogLevel, 日志级别, [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug] (default 3) 105 | '-log-level' => env('SOAR_LOG_LEVEL', 3), 106 | 107 | // LogOutput, 日志输出位置 (default "soar.log") 108 | '-log-output' => env('SOAR_LOG_OUTPUT', storage_path('logs/soar.log')), 109 | 110 | // ReportType, 优化建议输出格式,目前支持: json, text, markdown, html等 (default "markdown") 111 | '-report-type' => env('SOAR_REPORT_TYPE', 'json'), 112 | 113 | // // AllowCharsets (default "utf8,utf8mb4") 114 | // '-allow-charsets' => [ 115 | // 'utf8', 116 | // 'utf8mb4', 117 | // ], 118 | // 119 | // // AllowCollates 120 | // '-allow-collates' => [ 121 | // ], 122 | // 123 | // // AllowDropIndex, 允许输出删除重复索引的建议 124 | // '-allow-drop-index' => false, 125 | // 126 | // // AllowEngines (default "innodb") 127 | // '-allow-engines' => [ 128 | // 'innodb', 129 | // ], 130 | // 131 | // // Check configs 132 | // '-check-config' => null, 133 | // 134 | // // 单次运行清理历史1小时前残余的测试库。 135 | // '-cleanup-test-database' => false, 136 | // 137 | // // ColumnNotAllowType (default "boolean") 138 | // '-column-not-allow-type' => [ 139 | // 'boolean', 140 | // ], 141 | // 142 | // // Delimiter, SQL分隔符 (default ";") 143 | // '-delimiter' => ';', 144 | // 145 | // // DropTestTemporary, 是否清理测试环境产生的临时库表 (default true) 146 | // '-drop-test-temporary' => true, 147 | // 148 | // // 是否在预演环境执行 (default true) 149 | // '-dry-run' => true, 150 | // 151 | // // ExplainFormat [json, traditional] (default "traditional") 152 | // '-explain-format' => 'traditional', 153 | // 154 | // // ExplainMaxFiltered, filtered大于该配置给出警告 (default 100) 155 | // '-explain-max-filtered' => 100, 156 | // 157 | // // ExplainMaxKeyLength, 最大key_len (default 3) 158 | // '-explain-max-keys' => 3, 159 | // 160 | // // ExplainMaxRows, 最大扫描行数警告 (default 10000) 161 | // '-explain-max-rows' => 10000, 162 | // 163 | // // ExplainMinPossibleKeys, 最小possible_keys警告 164 | // '-explain-min-keys' => 0, 165 | // 166 | // // ExplainSQLReportType [pretty, sample, fingerprint] (default "pretty") 167 | // '-explain-sql-report-type' => 'pretty', 168 | // 169 | // // ExplainType [extended, partitions, traditional] (default "extended") 170 | // '-explain-type' => 'extended', 171 | // 172 | // // ExplainWarnAccessType, 哪些access type不建议使用 (default "ALL") 173 | // '-explain-warn-access-type' => [ 174 | // 'ALL', 175 | // ], 176 | // 177 | // // ExplainWarnExtra, 哪些extra信息会给警告 (default "Using temporary,Using filesort") 178 | // '-explain-warn-extra' => [ 179 | // 'Using temporary', 180 | // 'Using filesort', 181 | // ], 182 | // 183 | // // ExplainWarnScalability, 复杂度警告名单, 支持O(n),O(log n),O(1),O(?) (default "O(n)") 184 | // '-explain-warn-scalability' => [ 185 | // 'O(n)', 186 | // ], 187 | // 188 | // // ExplainWarnSelectType, 哪些select_type不建议使用 189 | // '-explain-warn-select-type' => [ 190 | // '', 191 | // ], 192 | // 193 | // // Help 194 | // '-help' => null, 195 | // 196 | // // IdxPrefix (default "idx_") 197 | // '-index-prefix' => 'idx_', 198 | // 199 | // // ListHeuristicRules, 打印支持的评审规则列表 200 | // '-list-heuristic-rules' => false, 201 | // 202 | // // ListReportTypes, 打印支持的报告输出类型 203 | // '-list-report-types' => false, 204 | // 205 | // // ListRewriteRules, 打印支持的重写规则列表 206 | // '-list-rewrite-rules' => false, 207 | // 208 | // // ListTestSqls, 打印测试case用于测试 209 | // '-list-test-sqls' => false, 210 | // 211 | // // log stack traces for errors 212 | // '-log_err_stacks' => null, 213 | // 214 | // // size in bytes at which logs are rotated (glog.MaxSize) (default 1887436800) 215 | // '-log_rotate_max_size' => '1887436800', 216 | // 217 | // // MarkdownExtensions, markdown 转 html支持的扩展包, 参考blackfriday (default 94) 218 | // '-markdown-extensions' => 94, 219 | // 220 | // // MarkdownHTMLFlags, markdown 转 html 支持的 flag, 参考blackfriday 221 | // '-markdown-html-flags' => 0, 222 | // 223 | // // MaxColCount, 单表允许的最大列数 (default 40) 224 | // '-max-column-count' => 40, 225 | // 226 | // // MaxDistinctCount, 单条 SQL 中 Distinct 的最大数量 (default 5) 227 | // '-max-distinct-count' => 5, 228 | // 229 | // // MaxGroupByColsCount, 单条 SQL 中 GroupBy 包含列的最大数量 (default 5) 230 | // '-max-group-by-cols-count' => 5, 231 | // 232 | // // MaxInCount, IN()最大数量 (default 10) 233 | // '-max-in-count' => 10, 234 | // 235 | // // MaxIdxBytes, 索引总长度限制 (default 3072) 236 | // '-max-index-bytes' => 3072, 237 | // 238 | // // MaxIdxBytesPerColumn, 索引中单列最大字节数 (default 767) 239 | // '-max-index-bytes-percolumn' => 767, 240 | // 241 | // // MaxIdxColsCount, 复合索引中包含列的最大数量 (default 5) 242 | // '-max-index-cols-count' => 5, 243 | // 244 | // // MaxIdxCount, 单表最大索引个数 (default 10) 245 | // '-max-index-count' => 10, 246 | // 247 | // // MaxJoinTableCount, 单条 SQL 中 JOIN 表的最大数量 (default 5) 248 | // '-max-join-table-count' => 5, 249 | // 250 | // // MaxPrettySQLLength, 超出该长度的SQL会转换成指纹输出 (default 1024) 251 | // '-max-pretty-sql-length' => 1024, 252 | // 253 | // // MaxQueryCost, last_query_cost 超过该值时将给予警告 (default 9999) 254 | // '-max-query-cost' => 9999, 255 | // 256 | // // MaxSubqueryDepth (default 5) 257 | // '-max-subquery-depth' => 5, 258 | // 259 | // // MaxTextColsCount, 表中含有的 text/blob 列的最大数量 (default 2) 260 | // '-max-text-cols-count' => 2, 261 | // 262 | // // MaxTotalRows, 计算散粒度时,当数据行数大于MaxTotalRows即开启数据库保护模式,不计算散粒度 (default 9999999) 263 | // '-max-total-rows' => 9999999, 264 | // 265 | // // MaxValueCount, INSERT/REPLACE 单次批量写入允许的行数 (default 100) 266 | // '-max-value-count' => 100, 267 | // 268 | // // MaxVarcharLength (default 1024) 269 | // '-max-varchar-length' => 1024, 270 | // 271 | // // MinCardinality,索引列散粒度最低阈值,散粒度低于该值的列不添加索引,建议范围0.0 ~ 100.0 272 | // '-min-cardinality' => 0, 273 | // 274 | // // OnlySyntaxCheck, 只做语法检查不输出优化建议 275 | // '-only-syntax-check' => false, 276 | // 277 | // // Print configs 278 | // '-print-config' => null, 279 | // 280 | // // Profiling, 开启数据采样的情况下在测试环境执行Profile 281 | // '-profiling' => false, 282 | // 283 | // // 待评审的 SQL 或 SQL 文件,如 SQL 中包含特殊字符建议使用文件名。 284 | // '-query' => '', 285 | // 286 | // // ReportCSS, 当 ReportType 为 html 格式时使用的 css 风格,如不指定会提供一个默认风格。CSS可以是本地文件,也可以是一个URL 287 | // '-report-css' => '', 288 | // 289 | // // ReportJavascript, 当 ReportType 为 html 格式时使用的javascript脚本,如不指定默认会加载SQL pretty 使用的 javascript。像CSS一样可以是本地文件,也可以是一个URL 290 | // '-report-javascript' => '', 291 | // 292 | // // ReportTitle, 当 ReportType 为 html 格式时,HTML 的 title (default "SQL优化分析报告") 293 | // '-report-title' => 'SQL优化分析报告', 294 | // 295 | // // RewriteRules, 生效的重写规则 (default "delimiter,orderbynull,groupbyconst,dmlorderby,having,star2columns,insertcolumns,distinctstar") 296 | // '-rewrite-rules' => [ 297 | // 'delimiter', 298 | // 'orderbynull', 299 | // 'groupbyconst', 300 | // 'dmlorderby', 301 | // 'having', 302 | // 'star2columns', 303 | // 'insertcolumns', 304 | // 'distinctstar', 305 | // ], 306 | // 307 | // // Sampling, 数据采样开关 308 | // '-sampling' => false, 309 | // 310 | // // SamplingCondition, 数据采样条件,如: WHERE xxx LIMIT xxx 311 | // '-sampling-condition' => '', 312 | // 313 | // // SamplingStatisticTarget, 数据采样因子,对应 PostgreSQL 的 default_statistics_target (default 100) 314 | // '-sampling-statistic-target' => 100, 315 | // 316 | // // ShowLastQueryCost 317 | // '-show-last-query-cost' => false, 318 | // 319 | // // ShowWarnings 320 | // '-show-warnings' => false, 321 | // 322 | // // SpaghettiQueryLength, SQL最大长度警告,超过该长度会给警告 (default 2048) 323 | // '-spaghetti-query-length' => 2048, 324 | // 325 | // // Trace, 开启数据采样的情况下在测试环境执行Trace 326 | // '-trace' => false, 327 | // 328 | // // UkPrefix (default "uk_") 329 | // '-unique-key-prefix' => 'uk_', 330 | // 331 | // // Verbose 332 | // '-verbose' => false, 333 | // 334 | // // Print version info 335 | // '-version' => null, 336 | ], 337 | ]; 338 | -------------------------------------------------------------------------------- /docs/json.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "ok", 3 | "soar_scores": [ 4 | { 5 | "Summary": "[☆☆☆☆☆|0分|9.17ms|select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30']", 6 | "Basic": { 7 | "Sample": "select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30'", 8 | "Score": 0, 9 | "Star": "☆☆☆☆☆", 10 | "Time": "9.17ms", 11 | "Connection": "mysql", 12 | "Driver": "mysql", 13 | "Tables": [ 14 | "`laravel`.`users`" 15 | ] 16 | }, 17 | "HeuristicRules": [ 18 | { 19 | "Item": "CLA.008", 20 | "Severity": "L2", 21 | "Summary": "请为 GROUP BY 显示添加 ORDER BY 条件", 22 | "Content": "默认 MySQL 会对 'GROUP BY col1, col2, ...' 请求按如下顺序排序 'ORDER BY col1, col2, ...'。如果 GROUP BY 语句不指定 ORDER BY 条件会导致无谓的排序产生,如果不需要排序建议添加 'ORDER BY NULL'。", 23 | "Case": "select c1,c2,c3 from t1 where c1='foo' group by c2", 24 | "Position": 0 25 | }, 26 | { 27 | "Item": "CLA.013", 28 | "Severity": "L3", 29 | "Summary": "不建议使用 HAVING 子句", 30 | "Content": "将查询的 HAVING 子句改写为 WHERE 中的查询条件,可以在查询处理期间使用索引。", 31 | "Case": "SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id", 32 | "Position": 0 33 | }, 34 | { 35 | "Item": "COL.001", 36 | "Severity": "L1", 37 | "Summary": "不建议使用 SELECT * 类型查询", 38 | "Content": "当表结构变更时,使用 * 通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。", 39 | "Case": "select * from tbl where id=1", 40 | "Position": 0 41 | }, 42 | { 43 | "Item": "ERR.002", 44 | "Severity": "L8", 45 | "Summary": "MySQL execute failed", 46 | "Content": "Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_230605111934_bbpxve0adj2dgrcs.users.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by", 47 | "Case": "", 48 | "Position": 0 49 | }, 50 | { 51 | "Item": "GRP.001", 52 | "Severity": "L2", 53 | "Summary": "不建议对等值查询列使用 GROUP BY", 54 | "Content": "GROUP BY 中的列在前面的 WHERE 条件中使用了等值查询,对这样的列进行 GROUP BY 意义不大。", 55 | "Case": "select film_id, title from film where release_year='2006' group by release_year", 56 | "Position": 0 57 | }, 58 | { 59 | "Item": "RES.001", 60 | "Severity": "L4", 61 | "Summary": "非确定性的 GROUP BY", 62 | "Content": "SQL返回的列既不在聚合函数中也不是 GROUP BY 表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo=\"bar\" group by a,该 SQL 返回的结果就是不确定的。", 63 | "Case": "select c1,c2,c3 from t1 where c2='foo' group by c2", 64 | "Position": 0 65 | } 66 | ], 67 | "IndexRules": [ 68 | { 69 | "Item": "IDX.001", 70 | "Severity": "L2", 71 | "Summary": "为laravel库的users表添加索引", 72 | "Content": "为列name添加索引;为列created_at添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 73 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name_created_at` (`name`(191),`created_at`) ;\n", 74 | "Position": 0 75 | } 76 | ], 77 | "Explain": [], 78 | "Backtraces": [ 79 | "#13 /routes/web.php:53", 80 | "#38 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 81 | "#59 /public/index.php:55", 82 | "#60 /server.php:21" 83 | ] 84 | }, 85 | { 86 | "Summary": "[★★★★☆|75分|205.25ms|CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;]", 87 | "Basic": { 88 | "Sample": "CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 89 | "Score": 75, 90 | "Star": "★★★★☆", 91 | "Time": "205.25ms", 92 | "Connection": "mysql", 93 | "Driver": "mysql", 94 | "Tables": [ 95 | "`laravel`.`users`" 96 | ] 97 | }, 98 | "HeuristicRules": [ 99 | { 100 | "Item": "CLA.011", 101 | "Severity": "L1", 102 | "Summary": "建议为表添加注释", 103 | "Content": "为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。", 104 | "Case": "CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8", 105 | "Position": 0 106 | }, 107 | { 108 | "Item": "COL.004", 109 | "Severity": "L1", 110 | "Summary": "请为列添加默认值", 111 | "Content": "请为列添加默认值,如果是 ALTER 操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。", 112 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 113 | "Position": 0 114 | }, 115 | { 116 | "Item": "COL.005", 117 | "Severity": "L1", 118 | "Summary": "列未添加注释", 119 | "Content": "建议对表中每个列添加注释,来明确每个列在表中的含义及作用。", 120 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 121 | "Position": 0 122 | }, 123 | { 124 | "Item": "KWR.003", 125 | "Severity": "L1", 126 | "Summary": "不建议使用复数做列名或表名", 127 | "Content": "表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。", 128 | "Case": "CREATE TABLE tbl ( `books` int )", 129 | "Position": 0 130 | }, 131 | { 132 | "Item": "SEC.002", 133 | "Severity": "L0", 134 | "Summary": "不使用明文存储密码", 135 | "Content": "使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。", 136 | "Case": "create table test(id int,name varchar(20) not null,password varchar(200)not null)", 137 | "Position": 0 138 | }, 139 | { 140 | "Item": "STA.003", 141 | "Severity": "L1", 142 | "Summary": "索引起名不规范", 143 | "Content": "建议普通二级索引以idx_为前缀,唯一索引以uk_为前缀。", 144 | "Case": "select col from now where type!=0", 145 | "Position": 0 146 | } 147 | ], 148 | "IndexRules": [], 149 | "Explain": [], 150 | "Backtraces": [ 151 | "#9 /routes/web.php:22", 152 | "#34 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 153 | "#55 /public/index.php:55", 154 | "#56 /server.php:21" 155 | ] 156 | }, 157 | { 158 | "Summary": "[★★★★☆|80分|1.72ms|update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30']", 159 | "Basic": { 160 | "Sample": "update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30'", 161 | "Score": 80, 162 | "Star": "★★★★☆", 163 | "Time": "1.72ms", 164 | "Connection": "mysql", 165 | "Driver": "mysql", 166 | "Tables": [ 167 | "`laravel`.`users`" 168 | ] 169 | }, 170 | "HeuristicRules": [ 171 | { 172 | "Item": "CLA.015", 173 | "Severity": "L4", 174 | "Summary": "UPDATE 未指定 WHERE 条件", 175 | "Content": "UPDATE 不指定 WHERE 条件一般是致命的,请您三思后行", 176 | "Case": "update tbl set col=1", 177 | "Position": 0 178 | } 179 | ], 180 | "IndexRules": [], 181 | "Explain": [ 182 | { 183 | "Item": "EXP.000", 184 | "Severity": "L0", 185 | "Summary": "Explain信息", 186 | "Content": [ 187 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 188 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 189 | "| 1 | UPDATE | *users* | NULL | index | NULL | PRIMARY | 8 | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL |" 190 | ], 191 | "Case": [ 192 | "### Explain信息解读", 193 | "#### Type信息解读", 194 | "* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大." 195 | ], 196 | "Position": 0 197 | } 198 | ], 199 | "Backtraces": [ 200 | "#10 /routes/web.php:48", 201 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 202 | "#56 /public/index.php:55", 203 | "#57 /server.php:21" 204 | ] 205 | }, 206 | { 207 | "Summary": "[★★★★★|90分|940μs|delete from `users` where `name` = 'soar']", 208 | "Basic": { 209 | "Sample": "delete from `users` where `name` = 'soar'", 210 | "Score": 90, 211 | "Star": "★★★★★", 212 | "Time": "940μs", 213 | "Connection": "mysql", 214 | "Driver": "mysql", 215 | "Tables": [ 216 | "`laravel`.`users`" 217 | ] 218 | }, 219 | "HeuristicRules": [ 220 | { 221 | "Item": "SEC.003", 222 | "Severity": "L0", 223 | "Summary": "使用DELETE/DROP/TRUNCATE等操作时注意备份", 224 | "Content": "在执行高危操作之前对数据进行备份是十分有必要的。", 225 | "Case": "delete from table where col = 'condition'", 226 | "Position": 0 227 | } 228 | ], 229 | "IndexRules": [ 230 | { 231 | "Item": "IDX.001", 232 | "Severity": "L2", 233 | "Summary": "为laravel库的users表添加索引", 234 | "Content": "为列name添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 235 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name` (`name`(191)) ;\n", 236 | "Position": 0 237 | } 238 | ], 239 | "Explain": [ 240 | { 241 | "Item": "EXP.000", 242 | "Severity": "L0", 243 | "Summary": "Explain信息", 244 | "Content": [ 245 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 246 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 247 | "| 1 | DELETE | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where |" 248 | ], 249 | "Case": [ 250 | "### Explain信息解读", 251 | "#### Type信息解读", 252 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描.", 253 | "#### Extra信息解读", 254 | "* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的." 255 | ], 256 | "Position": 0 257 | } 258 | ], 259 | "Backtraces": [ 260 | "#10 /routes/web.php:56", 261 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 262 | "#56 /public/index.php:55", 263 | "#57 /server.php:21" 264 | ] 265 | }, 266 | { 267 | "Summary": "[★★★★★|100分|9.59ms|insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')]", 268 | "Basic": { 269 | "Sample": "insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')", 270 | "Score": 100, 271 | "Star": "★★★★★", 272 | "Time": "9.59ms", 273 | "Connection": "mysql", 274 | "Driver": "mysql", 275 | "Tables": [ 276 | "`laravel`.`users`" 277 | ] 278 | }, 279 | "HeuristicRules": [], 280 | "IndexRules": [], 281 | "Explain": [ 282 | { 283 | "Item": "EXP.000", 284 | "Severity": "L0", 285 | "Summary": "Explain信息", 286 | "Content": [ 287 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 288 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 289 | "| 1 | INSERT | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 0 | 0.00% | ☠️ **O(n)** | NULL |" 290 | ], 291 | "Case": [ 292 | "### Explain信息解读", 293 | "#### Type信息解读", 294 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描." 295 | ], 296 | "Position": 0 297 | } 298 | ], 299 | "Backtraces": [ 300 | "#10 /routes/web.php:43", 301 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 302 | "#56 /public/index.php:55", 303 | "#57 /server.php:21" 304 | ] 305 | } 306 | ] 307 | } 308 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guanguans/laravel-soar", 3 | "description": "SQL optimizer and rewriter for laravel. - laravel 的 SQL 优化器和重写器。", 4 | "license": "MIT", 5 | "type": "laravel", 6 | "keywords": [ 7 | "dev", 8 | "testing", 9 | "static analysis", 10 | "soar", 11 | "SQL", 12 | "sql", 13 | "mysql", 14 | "debug", 15 | "laravel", 16 | "statement", 17 | "重写器", 18 | "优化器", 19 | "rewriter", 20 | "optimizer", 21 | "optimize", 22 | "rewrite", 23 | "clockwork", 24 | "console", 25 | "debugbar", 26 | "dump", 27 | "json", 28 | "errorlog", 29 | "laradumps", 30 | "log", 31 | "syslog", 32 | "telescope", 33 | "trap", 34 | "ray" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "guanguans", 39 | "email": "ityaozm@gmail.com", 40 | "homepage": "https://github.com/guanguans", 41 | "role": "developer" 42 | } 43 | ], 44 | "homepage": "https://github.com/guanguans/laravel-soar", 45 | "support": { 46 | "issues": "https://github.com/guanguans/laravel-soar/issues", 47 | "source": "https://github.com/guanguans/laravel-soar", 48 | "discussions": "https://github.com/guanguans/laravel-soar/discussions" 49 | }, 50 | "funding": [ 51 | { 52 | "type": "sponsors", 53 | "url": "https://guanguans.github.io/sponsors" 54 | } 55 | ], 56 | "require": { 57 | "php": ">=8.1", 58 | "guanguans/soar-php": "^7.0", 59 | "laravel/framework": "^10.50 || ^11.0 || ^12.0" 60 | }, 61 | "require-dev": { 62 | "adamwojs/php-cs-fixer-phpdoc-force-fqcn": "^2.0", 63 | "bamarni/composer-bin-plugin": "^1.8", 64 | "barryvdh/laravel-debugbar": "^3.16", 65 | "brainmaestro/composer-git-hooks": "^3.0", 66 | "driftingly/rector-laravel": "^2.1", 67 | "ergebnis/composer-normalize": "^2.48", 68 | "ergebnis/license": "^2.7", 69 | "ergebnis/php-cs-fixer-config": "^6.57", 70 | "ergebnis/rector-rules": "^1.7", 71 | "guanguans/php-cs-fixer-custom-fixers": "^1.0", 72 | "itsgoingd/clockwork": "^5.3", 73 | "laradumps/laradumps": "^4.6", 74 | "laravel/facade-documenter": "dev-main", 75 | "laravel/telescope": "^5.15", 76 | "mockery/mockery": "^1.6", 77 | "nette/utils": "^4.0", 78 | "orchestra/testbench": "^8.36 || ^9.0 || ^10.0", 79 | "pestphp/pest": "^2.36 || ^3.0 || ^4.0", 80 | "pestphp/pest-plugin-arch": "^2.7 || ^3.0 || ^4.0", 81 | "pestphp/pest-plugin-laravel": "^2.4 || ^3.0 || ^4.0", 82 | "pestphp/pest-plugin-profanity": "^1.7 || ^2.0 || ^3.0 || ^4.0", 83 | "php-mock/php-mock-phpunit": "^2.14", 84 | "phpstan/extension-installer": "^1.4", 85 | "phpstan/phpstan": "^2.1", 86 | "phpstan/phpstan-deprecation-rules": "^2.0", 87 | "phpstan/phpstan-mockery": "^2.0", 88 | "phpstan/phpstan-strict-rules": "^2.0", 89 | "phpstan/phpstan-webmozart-assert": "^2.0", 90 | "povils/phpmnd": "^3.6", 91 | "rector/jack": "^0.4", 92 | "rector/rector": "^2.2", 93 | "rector/swiss-knife": "^2.3", 94 | "rector/type-perfect": "^2.1", 95 | "shipmonk/composer-dependency-analyser": "^1.8", 96 | "shipmonk/dead-code-detector": "^0.14", 97 | "shipmonk/name-collision-detector": "^2.1", 98 | "shipmonk/phpstan-baseline-per-identifier": "^2.2", 99 | "spatie/laravel-image-optimizer": "^1.8", 100 | "spatie/laravel-ray": "^1.43", 101 | "spaze/phpstan-disallowed-calls": "^4.7", 102 | "staabm/phpstan-todo-by": "^0.3", 103 | "symfony/thanks": "^1.4", 104 | "symplify/phpstan-rules": "^14.9", 105 | "tomasvotruba/class-leak": "^2.1", 106 | "tomasvotruba/cognitive-complexity": "^1.0", 107 | "tomasvotruba/type-coverage": "^2.0", 108 | "yamadashy/phpstan-friendly-formatter": "^1.3" 109 | }, 110 | "conflict": { 111 | "pestphp/pest": "^4.0" 112 | }, 113 | "suggest": { 114 | "barryvdh/laravel-debugbar": "Output SQL scores to Laravel DebugBar.", 115 | "buggregator/trap": "Output SQL scores to Trap.", 116 | "itsgoingd/clockwork": "Output SQL scores to Clockwork.", 117 | "laradumps/laradumps": "Output SQL scores to Laradumps.", 118 | "laravel/telescope": "Output SQL scores to Telescope.", 119 | "spatie/laravel-ray": "Output SQL scores to Laravel Ray." 120 | }, 121 | "repositories": { 122 | "facade-documenter": { 123 | "type": "vcs", 124 | "url": "git@github.com:laravel/facade-documenter.git" 125 | } 126 | }, 127 | "minimum-stability": "dev", 128 | "prefer-stable": true, 129 | "autoload": { 130 | "psr-4": { 131 | "Guanguans\\LaravelSoar\\": "src/" 132 | }, 133 | "files": [ 134 | "src/Support/helpers.php" 135 | ] 136 | }, 137 | "autoload-dev": { 138 | "psr-4": { 139 | "Guanguans\\LaravelSoarTests\\": "tests/", 140 | "Workbench\\App\\": "workbench/app/", 141 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 142 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 143 | } 144 | }, 145 | "config": { 146 | "allow-plugins": { 147 | "bamarni/composer-bin-plugin": true, 148 | "ergebnis/composer-normalize": true, 149 | "pestphp/pest-plugin": true, 150 | "phpstan/extension-installer": true, 151 | "symfony/thanks": true 152 | }, 153 | "optimize-autoloader": true, 154 | "preferred-install": "dist", 155 | "sort-packages": true 156 | }, 157 | "extra": { 158 | "bamarni-bin": { 159 | "bin-links": true, 160 | "forward-command": true, 161 | "target-directory": "vendor-bin" 162 | }, 163 | "branch-alias": { 164 | "dev-master": "5.x-dev" 165 | }, 166 | "composer-normalize": { 167 | "indent-size": 4, 168 | "indent-style": "space" 169 | }, 170 | "hooks": { 171 | "post-merge": [ 172 | "composer install --ansi -vv", 173 | "composer checks:required" 174 | ], 175 | "pre-commit": [ 176 | "composer checks:required" 177 | ] 178 | }, 179 | "laravel": { 180 | "aliases": { 181 | "Soar": "Guanguans\\LaravelSoar\\Facades\\Soar" 182 | }, 183 | "providers": [ 184 | "Guanguans\\LaravelSoar\\SoarServiceProvider" 185 | ] 186 | } 187 | }, 188 | "scripts": { 189 | "post-install-cmd": [ 190 | "@cghooks:upsert", 191 | "@composer:normalize" 192 | ], 193 | "post-update-cmd": [ 194 | "@cghooks:upsert", 195 | "@composer:normalize" 196 | ], 197 | "post-autoload-dump": [ 198 | "@testbench:clear", 199 | "@testbench:prepare" 200 | ], 201 | "actionlint": "actionlint -ignore=SC2035 -ignore=SC2086 -color -oneline -verbose", 202 | "blade-formatter": "blade-formatter resources/views/*.blade.php resources/views/**/*.blade.php --ignore-path= --php-version=8.1 --progress", 203 | "blade-formatter:check-formatted": "@blade-formatter --check-formatted", 204 | "blade-formatter:write": "@blade-formatter --write", 205 | "cghooks": "@php vendor/bin/cghooks --ansi -vv", 206 | "cghooks:upsert": [ 207 | "@cghooks add --ignore-lock", 208 | "@cghooks update" 209 | ], 210 | "checks": [ 211 | "@checks:required", 212 | "@checks:optional" 213 | ], 214 | "checks:optional": [ 215 | "@putenv:xdebug-off", 216 | "@class-leak:check", 217 | "@composer-dependency-analyser", 218 | "@composer:normalize-dry-run", 219 | "@detect-collisions", 220 | "@facade:lint", 221 | "@jack:breakpoint", 222 | "@jack:open-versions-dry-run", 223 | "@jack:raise-to-installed-dry-run", 224 | "@phpmnd", 225 | "@phpstan:analyse", 226 | "@rector:process-dry-run", 227 | "@sk:check-commented-code", 228 | "@sk:check-conflicts", 229 | "@sk:find-multi-classes", 230 | "@sk:namespace-to-psr-4", 231 | "@sk:privatize-constants", 232 | "@sk:spot-lazy-traits", 233 | "@todo-lint", 234 | "@yaml-lint", 235 | "@actionlint", 236 | "@gitleaks", 237 | "@lint-md", 238 | "@peck", 239 | "@zhlint" 240 | ], 241 | "checks:required": [ 242 | "@putenv:xdebug-off", 243 | "@php-cs-fixer:fix-dry-run", 244 | "@pest" 245 | ], 246 | "class-leak": "@php vendor/bin/class-leak --ansi -vv", 247 | "class-leak:check": "@class-leak check config/ src/ --skip-type=Guanguans\\LaravelSoar\\Contracts\\OutputContract --skip-suffix=InvalidArgumentException --skip-path=Support/Rectors/", 248 | "composer-bump": [ 249 | "@putenv:php", 250 | "@composer-config:disable-process-timeout", 251 | "@php composer-bump --highest-php-binary=$PHP85 --except-package=foo/bar --ansi" 252 | ], 253 | "composer-bump:all": [ 254 | "@composer-bump", 255 | "@composer-bump:vendor-bin-common", 256 | "@composer-bump:vendor-bin-php82" 257 | ], 258 | "composer-bump:vendor-bin-common": [ 259 | "@putenv:php", 260 | "ln -f composer-bump vendor-bin/common/composer-bump", 261 | "@php vendor-bin/common/composer-bump --highest-php-binary=$PHP85 --ansi" 262 | ], 263 | "composer-bump:vendor-bin-php82": [ 264 | "@putenv:php", 265 | "ln -f composer-bump vendor-bin/php82/composer-bump", 266 | "$PHP82 vendor-bin/php82/composer-bump --php-binary=$PHP82 --highest-php-binary=$PHP85 --ansi" 267 | ], 268 | "composer-config:disable-process-timeout": "Composer\\Config::disableProcessTimeout", 269 | "composer-dependency-analyser": "@php vendor/bin/composer-dependency-analyser --verbose", 270 | "composer:audit": "@composer audit --ansi -vv", 271 | "composer:bin-all-update": "@composer bin all update --ansi -vv", 272 | "composer:check-platform-reqs": "@composer check-platform-reqs --lock --ansi -vv", 273 | "composer:diff": "@composer diff --with-platform --ansi -vv", 274 | "composer:normalize": "@composer normalize --diff --ansi -vv", 275 | "composer:normalize-dry-run": "@composer:normalize --dry-run", 276 | "composer:validate": "@composer validate --check-lock --strict --ansi -vv", 277 | "detect-collisions": "@php vendor/bin/detect-collisions config/ src/", 278 | "facade:lint": "@facade:update --lint", 279 | "facade:update": "@php -f vendor/bin/facade.php -- Guanguans\\\\LaravelSoar\\\\Facades\\\\Soar", 280 | "git-chglog": "git-chglog $(git describe --tags $(git rev-list --tags --max-count=1))", 281 | "gitleaks": "gitleaks git --report-path=.build/gitleaks-report.json -v", 282 | "gitleaks:generate-baseline": "gitleaks git --report-path=gitleaks-baseline.json -v", 283 | "grumphp": [ 284 | "@putenv:php", 285 | "$PHP82 vendor/bin/grumphp run --ansi -vv" 286 | ], 287 | "jack": "@php vendor/bin/jack --ansi -vv", 288 | "jack:breakpoint": "@jack breakpoint --limit=7", 289 | "jack:breakpoint-dev": "@jack:breakpoint --dev", 290 | "jack:open-versions": "@jack open-versions --limit=99", 291 | "jack:open-versions-dev": "@jack:open-versions --dev", 292 | "jack:open-versions-dev-dry-run": "@jack:open-versions-dev --dry-run", 293 | "jack:open-versions-dry-run": "@jack:open-versions --dry-run", 294 | "jack:raise-to-installed": "@jack raise-to-installed", 295 | "jack:raise-to-installed-dry-run": "@jack:raise-to-installed --dry-run", 296 | "jsonlint": "@php vendor/bin/jsonlint *.json .*rc", 297 | "laravel-soar": "@php laravel-soar --ansi -vv", 298 | "laravel-soar:app-build": [ 299 | "@composer install --no-dev --no-scripts --ansi -vv", 300 | "@php laravel-soar app:build laravel-soar.phar --build-version=master --ansi", 301 | "@php builds/laravel-soar.phar list --ansi -vv", 302 | "ls -lh builds/laravel-soar.phar", 303 | "ls -lr builds/laravel-soar.phar" 304 | ], 305 | "lint-md": [ 306 | "if ! command -v lint-md >/dev/null 2>&1; then echo 'lint-md not found, installing...'; npm install -g @lint-md/cli; fi", 307 | "@lint-md:prototype" 308 | ], 309 | "lint-md:fix": "@lint-md:prototype --fix", 310 | "lint-md:prototype": "lint-md --suppress-warnings *.md .github/ docs/", 311 | "mago": "@php vendor/bin/mago", 312 | "mago:format": "@mago format", 313 | "mago:format-dry-run": "@mago:format --dry-run", 314 | "mago:init": "@mago init", 315 | "mago:lint": "@mago lint --sort --fix --unsafe --potentially-unsafe", 316 | "mago:lint-compilation": "@mago lint --compilation", 317 | "mago:lint-dry-run": "@mago:lint --dry-run", 318 | "mago:lint-list-rules": "@mago lint --list-rules", 319 | "mago:lint-semantics-only": "@mago lint --semantics-only", 320 | "monorepo-builder": [ 321 | "@putenv:php", 322 | "$PHP82 vendor/bin/monorepo-builder --ansi -vv" 323 | ], 324 | "monorepo-builder:release": "@monorepo-builder release", 325 | "monorepo-builder:release-1.0.0-BETA1": "@monorepo-builder:release 1.0.0-BETA1", 326 | "monorepo-builder:release-1.0.0-BETA1-dry-run": "@monorepo-builder:release-1.0.0-BETA1 --dry-run", 327 | "monorepo-builder:release-major": "@monorepo-builder:release major", 328 | "monorepo-builder:release-major-dry-run": "@monorepo-builder:release-major --dry-run", 329 | "monorepo-builder:release-minor": "@monorepo-builder:release minor", 330 | "monorepo-builder:release-minor-dry-run": "@monorepo-builder:release-minor --dry-run", 331 | "monorepo-builder:release-patch": "@monorepo-builder:release patch", 332 | "monorepo-builder:release-patch-dry-run": "@monorepo-builder:release-patch --dry-run", 333 | "neon-lint": "@php vendor/bin/neon-lint *.neon", 334 | "peck": [ 335 | "@putenv:php", 336 | "$PHP82 vendor/bin/peck check --path=src/ --config=../../peck.json --ansi -vv" 337 | ], 338 | "peck:ignore-all": "@peck --ignore-all", 339 | "peck:init": "@peck --init", 340 | "pest": [ 341 | "@putenv:xdebug-on", 342 | "@php vendor/bin/pest --colors=always --min=80 --coverage --profile", 343 | "@putenv:xdebug-off" 344 | ], 345 | "pest:coverage": "@pest --coverage-html=.build/phpunit/ --coverage-clover=.build/phpunit/clover.xml", 346 | "pest:highest": [ 347 | "@putenv:php", 348 | "@putenv:xdebug-on", 349 | "$PHP85 vendor/bin/pest --colors=always --min=80 --coverage", 350 | "@putenv:xdebug-off" 351 | ], 352 | "pest:migrate-configuration": "@pest --migrate-configuration", 353 | "pest:parallel": "@pest --parallel", 354 | "pest:profanity": "@pest --profanity --language=en", 355 | "pest:profile": "@pest --profile", 356 | "pest:type-coverage": "@pest --type-coverage", 357 | "pest:update-snapshots": "@pest --update-snapshots", 358 | "php-cs-fixer": "@php vendor/bin/php-cs-fixer --ansi -vv", 359 | "php-cs-fixer:custom": "@php-cs-fixer --config=.php-cs-fixer-custom.php", 360 | "php-cs-fixer:custom-check": "@php-cs-fixer:custom check --show-progress=dots --diff", 361 | "php-cs-fixer:custom-fix": "@php-cs-fixer:custom fix --show-progress=dots --diff", 362 | "php-cs-fixer:custom-fix-dry-run": "@php-cs-fixer:custom-fix --dry-run", 363 | "php-cs-fixer:custom-list-files": "@php-cs-fixer:custom list-files", 364 | "php-cs-fixer:fix": "@php-cs-fixer fix --show-progress=dots --diff", 365 | "php-cs-fixer:fix-dry-run": "@php-cs-fixer:fix --dry-run", 366 | "php-cs-fixer:list-files": "@php-cs-fixer list-files", 367 | "php-cs-fixer:list-sets": "@php-cs-fixer list-sets --ansi -vv", 368 | "php-lint": [ 369 | "@putenv:php", 370 | "for DIR in .; do find $DIR -maxdepth 1 -type f -name '*.php' -type f ! -name 'xxx.php' -exec $PHP81 -l {} \\; 2>&1 | (! grep -v '^No syntax errors detected'); done", 371 | "for DIR in src/ tests/; do find $DIR -type f -name '*.php' -type f ! -name 'xxx.php' -exec $PHP81 -l {} \\; 2>&1 | (! grep -v '^No syntax errors detected'); done" 372 | ], 373 | "phpbench": "@php vendor/bin/phpbench run --report=aggregate --ansi -vv", 374 | "phpmnd": "@php vendor/bin/phpmnd src/ --exclude-path=Support/helpers.phpp --ignore-numbers=2,-1 --hint --progress --ansi -vv", 375 | "phpstan": "@php vendor/bin/phpstan --ansi -vv", 376 | "phpstan:analyse": "@phpstan analyse", 377 | "phpstan:analyse-generate-baseline": "@phpstan:analyse --generate-baseline --allow-empty-baseline", 378 | "phpstan:analyse-split-baseline": [ 379 | "@phpstan:analyse --generate-baseline=baselines/loader.neon --allow-empty-baseline", 380 | "find baselines/ -type f -not -name loader.neon -delete", 381 | "@php vendor/bin/split-phpstan-baseline baselines/loader.neon" 382 | ], 383 | "phpstan:diagnose": "@phpstan diagnose", 384 | "phpstan:dump-parameters": "@phpstan dump-parameters", 385 | "pint": [ 386 | "@putenv:xdebug-off", 387 | "@php vendor/bin/pint --ansi -vv" 388 | ], 389 | "pint:test": "@pint --test", 390 | "putenv:composer-memory-unlimited": "@putenv COMPOSER_MEMORY_LIMIT=-1", 391 | "putenv:php": [ 392 | "@putenv PHP74=/opt/homebrew/opt/php@7.4/bin/php", 393 | "@putenv PHP80=/opt/homebrew/opt/php@8.0/bin/php", 394 | "@putenv PHP81=/opt/homebrew/opt/php@8.1/bin/php", 395 | "@putenv PHP82=/opt/homebrew/opt/php@8.2/bin/php", 396 | "@putenv PHP83=/opt/homebrew/opt/php@8.3/bin/php", 397 | "@putenv PHP84=/opt/homebrew/opt/php@8.4/bin/php", 398 | "@putenv PHP85=/opt/homebrew/opt/php@8.5/bin/php" 399 | ], 400 | "putenv:xdebug-off": "@putenv XDEBUG_MODE=off", 401 | "putenv:xdebug-on": [ 402 | "@putenv XDEBUG_MODE=coverage,debug", 403 | "@putenv XDEBUG_SESSION=1" 404 | ], 405 | "rector": "@php vendor/bin/rector", 406 | "rector:custom-rule": "@rector custom-rule", 407 | "rector:list-rules": "@rector list-rules", 408 | "rector:process": "@rector process", 409 | "rector:process-clear-cache": "@rector:process --clear-cache", 410 | "rector:process-clear-cache-dry-run": "@rector:process-clear-cache --dry-run", 411 | "rector:process-dry-run": "@rector:process --dry-run", 412 | "rector:process-only": "@rector:process-clear-cache --only=Guanguans\\LaravelSoar\\Support\\Rectors\\AddHasOptionsDocCommentRector", 413 | "rector:process-only-dry-run": "@rector:process-only --dry-run", 414 | "rule-doc-generator": [ 415 | "@putenv:php", 416 | "$PHP82 rule-doc-generator --ansi -vv" 417 | ], 418 | "rule-doc-generator:rector-generate": "@rule-doc-generator generate src/Support/Rectors/ --output-file=src/Support/Rectors/rector-rules-overview.md --categorize=rector", 419 | "rule-doc-generator:rector-validate": "@rule-doc-generator validate src/Support/Rectors/", 420 | "sk": "@php vendor/bin/swiss-knife --ansi -vv", 421 | "sk:alice-yaml-fixtures-to-php": "@sk alice-yaml-fixtures-to-php --help", 422 | "sk:check-commented-code": "@sk check-commented-code src/ --line-limit=5 --skip-file=src/Support/Utils.php", 423 | "sk:check-conflicts": "@sk check-conflicts config/ src/", 424 | "sk:dump-editorconfig": "@sk dump-editorconfig", 425 | "sk:finalize-classes": "@sk finalize-classes config/ src/", 426 | "sk:finalize-classes-dry-run": "@sk:finalize-classes --dry-run", 427 | "sk:find-multi-classes": "@sk find-multi-classes config/ src/", 428 | "sk:generate-symfony-config-builders": "@sk generate-symfony-config-builders --help", 429 | "sk:namespace-to-psr-4": "@sk namespace-to-psr-4 src/ --namespace-root=Guanguans\\LaravelSoar\\", 430 | "sk:pretty-json": "@sk pretty-json .lintmdrc", 431 | "sk:pretty-json-dry-run": "@sk:pretty-json --dry-run", 432 | "sk:privatize-constants": "@sk privatize-constants config/ src/", 433 | "sk:search-regex": "@sk search-regex 'Guanguans.*ValetDrivers'", 434 | "sk:split-config-per-package": "@sk split-config-per-package monorepo-builder.php", 435 | "sk:spot-lazy-traits": "@sk spot-lazy-traits src/ --max-used=2", 436 | "testbench": "@php vendor/bin/testbench --ansi -vv", 437 | "testbench:build": "@testbench workbench:build", 438 | "testbench:clear": "@testbench package:purge-skeleton", 439 | "testbench:optimize:image": "@testbench optimize:image", 440 | "testbench:optimize:image-dry-run": "@testbench:optimize:image --dry-run", 441 | "testbench:prepare": "@testbench package:discover", 442 | "testbench:serve": [ 443 | "@composer-config:disable-process-timeout", 444 | "@testbench:build", 445 | "@testbench serve" 446 | ], 447 | "testbench:test": [ 448 | "@testbench:clear", 449 | "@pest" 450 | ], 451 | "testbench:user-serve": [ 452 | "@composer-config:disable-process-timeout", 453 | "@testbench:build", 454 | "@php -S localhost:8123 workbench/public/index.php" 455 | ], 456 | "testbench:workbench:install": "@testbench workbench:install", 457 | "todo-lint": "! git --no-pager grep --extended-regexp --ignore-case 'todo|fixme' -- '*.php' ':!*.blade.php' ':(exclude)resources/'", 458 | "touch:database-sqlite": "@php -r \"file_exists('vendor/orchestra/testbench-core/laravel/database/database.sqlite') || touch('vendor/orchestra/testbench-core/laravel/database/database.sqlite');\"", 459 | "trufflehog": "trufflehog git https://github.com/guanguans/laravel-soar --only-verified", 460 | "var-dump-server:cli": "@php vendor/bin/var-dump-server --ansi -vv", 461 | "var-dump-server:html": [ 462 | "@composer-config:disable-process-timeout", 463 | "[ -d .build ] || mkdir -p .build/", 464 | "[ -f .build/dump.html ] || touch .build/dump.html", 465 | "open .build/dump.html", 466 | "@php vendor/bin/var-dump-server --ansi -v --format=html > .build/dump.html" 467 | ], 468 | "vendor-patches": "@php vendor/bin/vendor-patches generate --ansi -vv", 469 | "vhs": "vhs < laravel-soar.tape", 470 | "yaml-lint": "@php vendor/bin/yaml-lint .github/ *.yaml --ansi -vv", 471 | "zhlint": [ 472 | "if ! command -v zhlint >/dev/null 2>&1; then echo 'zhlint not found, installing...'; npm install -g zhlint; fi", 473 | "@zhlint:prototype" 474 | ], 475 | "zhlint:fix": "@zhlint:prototype --fix", 476 | "zhlint:prototype": "zhlint {,docs/,docs/**/}*-zh_CN.md", 477 | "zizmor": "zizmor .github/ --verbose" 478 | }, 479 | "scripts-aliases": { 480 | "pest": [ 481 | "test" 482 | ], 483 | "pest:coverage": [ 484 | "test:coverage" 485 | ], 486 | "phpbench": [ 487 | "benchmark" 488 | ] 489 | }, 490 | "$schema": "https://getcomposer.org/schema.json" 491 | } 492 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | | ![](docs/soar-bar.gif) | ![](docs/commands.gif) | 2 | |------------------------|------------------------| 3 | 4 | # laravel-soar 5 | 6 | > SQL optimizer and rewriter for laravel. - laravel 的 SQL 优化器和重写器。 7 | 8 | [简体中文](README-zh_CN.md) | [ENGLISH](README.md) 9 | 10 | [![tests](https://github.com/guanguans/laravel-soar/actions/workflows/tests.yml/badge.svg)](https://github.com/guanguans/laravel-soar/actions/workflows/tests.yml) 11 | [![php-cs-fixer](https://github.com/guanguans/laravel-soar/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-soar/actions/workflows/php-cs-fixer.yml) 12 | [![codecov](https://codecov.io/gh/guanguans/laravel-soar/graph/badge.svg?token=EWBG8GV4JD)](https://codecov.io/gh/guanguans/laravel-soar) 13 | [![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-soar/v)](https://packagist.org/packages/guanguans/laravel-soar) 14 | [![GitHub release (with filter)](https://img.shields.io/github/v/release/guanguans/laravel-soar)](https://github.com/guanguans/laravel-soar/releases) 15 | [![Total Downloads](https://poser.pugx.org/guanguans/laravel-soar/downloads)](https://packagist.org/packages/guanguans/laravel-soar) 16 | [![License](https://poser.pugx.org/guanguans/laravel-soar/license)](https://packagist.org/packages/guanguans/laravel-soar) 17 | 18 | ## 功能 19 | 20 | * 支持启发式规则建议、索引规则建议、`EXPLAIN` 信息解读 21 | * 支持调用查询构建器 `Mixin` 方法便捷的打印规则建议 22 | * 自动监控输出规则建议到配置的输出器 23 | 24 | ## 相关项目 25 | 26 | * [https://github.com/XiaoMi/soar](https://github.com/XiaoMi/soar) 27 | * [https://github.com/guanguans/soar-php](https://github.com/guanguans/soar-php) 28 | 29 | ## 环境要求 30 | 31 | * PHP >= 8.1 32 | 33 | ## 安装 34 | 35 | ```shell 36 | composer require guanguans/laravel-soar --dev --ansi -v 37 | ``` 38 | 39 | ## 配置 40 | 41 | ### 发布文件(可选的) 42 | 43 | ```shell 44 | php artisan vendor:publish --provider="Guanguans\\LaravelSoar\\SoarServiceProvider" 45 | ``` 46 | 47 | ### :warning: 在 unix 操作系统非 cli 环境中运行时,可能会抛出 Fatal error: ...Exit Code: 2(Misuse of shell builtins) 48 | 49 | #### 配置 sudo 密码 50 | 51 | ```shell 52 | # Fatal error: Uncaught Guanguans\SoarPHP\Exceptions\ProcessFailedException: The command "'/Users/yaozm/Documents/develop/soar-php/bin/soar.darwin-amd64' '-report-type=json' '-query=select * from users;'" failed. Exit Code: 2(Misuse of shell builtins) Working directory: /Users/yaozm/Documents/develop/soar-php Output: ================ Error Output: ================ panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1938665] goroutine 1 [running]: github.com/pingcap/tidb/util/memory.MemTotalNormal() pkg/mod/github.com/pingcap/tidb@v1.1.0-beta.0.20210601085537-5d7c852770eb/util/memory/meminfo.go:41 +0x65 github.com/pingcap/tidb/util/memory.init.0() pkg/mod/github.com/pingcap/tidb@v1.1.0-beta.0.20210601085537-5d7c852770eb/util/memory/meminfo.go:134 +0x175 in /Users/yaozm/Documents/develop/soar-php/src/Concerns/WithRunable.php:36 Stack trace: #0 /Users/yaozm/Documents/develop/soar-php/test.php(163): Guanguans\SoarPHP\Soar->run() #1 /User in /Users/yaozm/Documents/develop/soar-php/src/Concerns/WithRunable.php on line 36 53 | SOAR_SUDO_PASSWORD='your sudo password' # 设置 sudo 密码,以 sudo 运行 soar 命令,避免出现上述错误。 54 | ``` 55 | 56 | #### [或者配置 sudoers](https://github.com/guanguans/soar-php#or-configure-sudoers) 57 | 58 | ## 使用 59 | 60 | ### 启用自动输出 SQL 优化建议 61 | 62 |
63 | details 64 | 65 | #### 配置 `SOAR_ENABLED` 66 | 67 | ```dotenv 68 | SOAR_ENABLED=true 69 | ``` 70 | 71 | #### 或者配置 [`soar.enabled`](config/soar.php) 72 | 73 | ```php 74 | (bool) env('SOAR_ENABLED', env('APP_ENV') === 'local'), 78 | 79 | // ... 80 | ]; 81 | ``` 82 |
83 | 84 | ### 安装、配置输出器(可选的) 85 | 86 |
87 | Clockwork 88 | 89 | 1. 安装 [itsgoingd/clockwork](https://github.com/itsgoingd/clockwork) 90 | 2. 配置 [soar.outputs.Outputs\ClockworkOutput::class](config/soar.php) 91 | 92 | ![Clockwork](docs/clockwork.png) 93 |
94 | 95 |
96 | Console 97 | 98 | 1. 配置 [soar.outputs.Outputs\ConsoleOutput::class](config/soar.php) 99 | 100 | ![Console](docs/console.png) 101 |
102 | 103 |
104 | Debug bar 105 | 106 | 1. 安装 [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) 107 | 2. 配置 [soar.outputs.Outputs\DebugBarOutput::class](config/soar.php) 108 | 109 | ![DebugBar](docs/debug-bar.png) 110 |
111 | 112 |
113 | Dump 114 | 115 | 1. 配置 [soar.outputs.Outputs\DumpOutput::class](config/soar.php) 116 | 117 | ![Dump](docs/dump.png) 118 |
119 | 120 |
121 | Json 122 | 123 | 1. 配置 [soar.outputs.Outputs\JsonOutput::class](config/soar.php) 124 | 125 | ```json 126 | { 127 | "message": "ok", 128 | "soar_scores": [ 129 | { 130 | "Summary": "[☆☆☆☆☆|0分|9.17ms|select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30']", 131 | "Basic": { 132 | "Sample": "select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30'", 133 | "Score": 0, 134 | "Star": "☆☆☆☆☆", 135 | "Time": "9.17ms", 136 | "Connection": "mysql", 137 | "Driver": "mysql", 138 | "Tables": [ 139 | "`laravel`.`users`" 140 | ] 141 | }, 142 | "HeuristicRules": [ 143 | { 144 | "Item": "CLA.008", 145 | "Severity": "L2", 146 | "Summary": "请为 GROUP BY 显示添加 ORDER BY 条件", 147 | "Content": "默认 MySQL 会对 'GROUP BY col1, col2, ...' 请求按如下顺序排序 'ORDER BY col1, col2, ...'。如果 GROUP BY 语句不指定 ORDER BY 条件会导致无谓的排序产生,如果不需要排序建议添加 'ORDER BY NULL'。", 148 | "Case": "select c1,c2,c3 from t1 where c1='foo' group by c2", 149 | "Position": 0 150 | }, 151 | { 152 | "Item": "CLA.013", 153 | "Severity": "L3", 154 | "Summary": "不建议使用 HAVING 子句", 155 | "Content": "将查询的 HAVING 子句改写为 WHERE 中的查询条件,可以在查询处理期间使用索引。", 156 | "Case": "SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id", 157 | "Position": 0 158 | }, 159 | { 160 | "Item": "COL.001", 161 | "Severity": "L1", 162 | "Summary": "不建议使用 SELECT * 类型查询", 163 | "Content": "当表结构变更时,使用 * 通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。", 164 | "Case": "select * from tbl where id=1", 165 | "Position": 0 166 | }, 167 | { 168 | "Item": "ERR.002", 169 | "Severity": "L8", 170 | "Summary": "MySQL execute failed", 171 | "Content": "Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_230605111934_bbpxve0adj2dgrcs.users.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by", 172 | "Case": "", 173 | "Position": 0 174 | }, 175 | { 176 | "Item": "GRP.001", 177 | "Severity": "L2", 178 | "Summary": "不建议对等值查询列使用 GROUP BY", 179 | "Content": "GROUP BY 中的列在前面的 WHERE 条件中使用了等值查询,对这样的列进行 GROUP BY 意义不大。", 180 | "Case": "select film_id, title from film where release_year='2006' group by release_year", 181 | "Position": 0 182 | }, 183 | { 184 | "Item": "RES.001", 185 | "Severity": "L4", 186 | "Summary": "非确定性的 GROUP BY", 187 | "Content": "SQL返回的列既不在聚合函数中也不是 GROUP BY 表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo=\"bar\" group by a,该 SQL 返回的结果就是不确定的。", 188 | "Case": "select c1,c2,c3 from t1 where c2='foo' group by c2", 189 | "Position": 0 190 | } 191 | ], 192 | "IndexRules": [ 193 | { 194 | "Item": "IDX.001", 195 | "Severity": "L2", 196 | "Summary": "为laravel库的users表添加索引", 197 | "Content": "为列name添加索引;为列created_at添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 198 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name_created_at` (`name`(191),`created_at`) ;\n", 199 | "Position": 0 200 | } 201 | ], 202 | "Explain": [], 203 | "Backtraces": [ 204 | "#13 /routes/web.php:53", 205 | "#38 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 206 | "#59 /public/index.php:55", 207 | "#60 /server.php:21" 208 | ] 209 | }, 210 | { 211 | "Summary": "[★★★★☆|75分|205.25ms|CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;]", 212 | "Basic": { 213 | "Sample": "CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 214 | "Score": 75, 215 | "Star": "★★★★☆", 216 | "Time": "205.25ms", 217 | "Connection": "mysql", 218 | "Driver": "mysql", 219 | "Tables": [ 220 | "`laravel`.`users`" 221 | ] 222 | }, 223 | "HeuristicRules": [ 224 | { 225 | "Item": "CLA.011", 226 | "Severity": "L1", 227 | "Summary": "建议为表添加注释", 228 | "Content": "为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。", 229 | "Case": "CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8", 230 | "Position": 0 231 | }, 232 | { 233 | "Item": "COL.004", 234 | "Severity": "L1", 235 | "Summary": "请为列添加默认值", 236 | "Content": "请为列添加默认值,如果是 ALTER 操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。", 237 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 238 | "Position": 0 239 | }, 240 | { 241 | "Item": "COL.005", 242 | "Severity": "L1", 243 | "Summary": "列未添加注释", 244 | "Content": "建议对表中每个列添加注释,来明确每个列在表中的含义及作用。", 245 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 246 | "Position": 0 247 | }, 248 | { 249 | "Item": "KWR.003", 250 | "Severity": "L1", 251 | "Summary": "不建议使用复数做列名或表名", 252 | "Content": "表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。", 253 | "Case": "CREATE TABLE tbl ( `books` int )", 254 | "Position": 0 255 | }, 256 | { 257 | "Item": "SEC.002", 258 | "Severity": "L0", 259 | "Summary": "不使用明文存储密码", 260 | "Content": "使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。", 261 | "Case": "create table test(id int,name varchar(20) not null,password varchar(200)not null)", 262 | "Position": 0 263 | }, 264 | { 265 | "Item": "STA.003", 266 | "Severity": "L1", 267 | "Summary": "索引起名不规范", 268 | "Content": "建议普通二级索引以idx_为前缀,唯一索引以uk_为前缀。", 269 | "Case": "select col from now where type!=0", 270 | "Position": 0 271 | } 272 | ], 273 | "IndexRules": [], 274 | "Explain": [], 275 | "Backtraces": [ 276 | "#9 /routes/web.php:22", 277 | "#34 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 278 | "#55 /public/index.php:55", 279 | "#56 /server.php:21" 280 | ] 281 | }, 282 | { 283 | "Summary": "[★★★★☆|80分|1.72ms|update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30']", 284 | "Basic": { 285 | "Sample": "update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30'", 286 | "Score": 80, 287 | "Star": "★★★★☆", 288 | "Time": "1.72ms", 289 | "Connection": "mysql", 290 | "Driver": "mysql", 291 | "Tables": [ 292 | "`laravel`.`users`" 293 | ] 294 | }, 295 | "HeuristicRules": [ 296 | { 297 | "Item": "CLA.015", 298 | "Severity": "L4", 299 | "Summary": "UPDATE 未指定 WHERE 条件", 300 | "Content": "UPDATE 不指定 WHERE 条件一般是致命的,请您三思后行", 301 | "Case": "update tbl set col=1", 302 | "Position": 0 303 | } 304 | ], 305 | "IndexRules": [], 306 | "Explain": [ 307 | { 308 | "Item": "EXP.000", 309 | "Severity": "L0", 310 | "Summary": "Explain信息", 311 | "Content": [ 312 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 313 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 314 | "| 1 | UPDATE | *users* | NULL | index | NULL | PRIMARY | 8 | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL |" 315 | ], 316 | "Case": [ 317 | "### Explain信息解读", 318 | "#### Type信息解读", 319 | "* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大." 320 | ], 321 | "Position": 0 322 | } 323 | ], 324 | "Backtraces": [ 325 | "#10 /routes/web.php:48", 326 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 327 | "#56 /public/index.php:55", 328 | "#57 /server.php:21" 329 | ] 330 | }, 331 | { 332 | "Summary": "[★★★★★|90分|940μs|delete from `users` where `name` = 'soar']", 333 | "Basic": { 334 | "Sample": "delete from `users` where `name` = 'soar'", 335 | "Score": 90, 336 | "Star": "★★★★★", 337 | "Time": "940μs", 338 | "Connection": "mysql", 339 | "Driver": "mysql", 340 | "Tables": [ 341 | "`laravel`.`users`" 342 | ] 343 | }, 344 | "HeuristicRules": [ 345 | { 346 | "Item": "SEC.003", 347 | "Severity": "L0", 348 | "Summary": "使用DELETE/DROP/TRUNCATE等操作时注意备份", 349 | "Content": "在执行高危操作之前对数据进行备份是十分有必要的。", 350 | "Case": "delete from table where col = 'condition'", 351 | "Position": 0 352 | } 353 | ], 354 | "IndexRules": [ 355 | { 356 | "Item": "IDX.001", 357 | "Severity": "L2", 358 | "Summary": "为laravel库的users表添加索引", 359 | "Content": "为列name添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 360 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name` (`name`(191)) ;\n", 361 | "Position": 0 362 | } 363 | ], 364 | "Explain": [ 365 | { 366 | "Item": "EXP.000", 367 | "Severity": "L0", 368 | "Summary": "Explain信息", 369 | "Content": [ 370 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 371 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 372 | "| 1 | DELETE | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where |" 373 | ], 374 | "Case": [ 375 | "### Explain信息解读", 376 | "#### Type信息解读", 377 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描.", 378 | "#### Extra信息解读", 379 | "* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的." 380 | ], 381 | "Position": 0 382 | } 383 | ], 384 | "Backtraces": [ 385 | "#10 /routes/web.php:56", 386 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 387 | "#56 /public/index.php:55", 388 | "#57 /server.php:21" 389 | ] 390 | }, 391 | { 392 | "Summary": "[★★★★★|100分|9.59ms|insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')]", 393 | "Basic": { 394 | "Sample": "insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')", 395 | "Score": 100, 396 | "Star": "★★★★★", 397 | "Time": "9.59ms", 398 | "Connection": "mysql", 399 | "Driver": "mysql", 400 | "Tables": [ 401 | "`laravel`.`users`" 402 | ] 403 | }, 404 | "HeuristicRules": [], 405 | "IndexRules": [], 406 | "Explain": [ 407 | { 408 | "Item": "EXP.000", 409 | "Severity": "L0", 410 | "Summary": "Explain信息", 411 | "Content": [ 412 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 413 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 414 | "| 1 | INSERT | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 0 | 0.00% | ☠️ **O(n)** | NULL |" 415 | ], 416 | "Case": [ 417 | "### Explain信息解读", 418 | "#### Type信息解读", 419 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描." 420 | ], 421 | "Position": 0 422 | } 423 | ], 424 | "Backtraces": [ 425 | "#10 /routes/web.php:43", 426 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 427 | "#56 /public/index.php:55", 428 | "#57 /server.php:21" 429 | ] 430 | } 431 | ] 432 | } 433 | ``` 434 |
435 | 436 |
437 | LaraDumps 438 | 439 | 1. 安装 [laradumps/laradumps](https://github.com/laradumps/laradumps) 440 | 2. 配置 [soar.outputs.Outputs\LaraDumpsOutput::class](config/soar.php) 441 | 442 | ![LaraDumps](docs/lara-dumps.png) 443 |
444 | 445 |
446 | Log 447 | 448 | 1. 配置 [soar.outputs.Outputs\LogOutput::class](config/soar.php) 449 | 450 | ![Log](docs/log.png) 451 |
452 | 453 |
454 | Ray 455 | 456 | 1. 安装 [spatie/laravel-ray](https://github.com/spatie/laravel-ray) 457 | 2. 配置 [soar.outputs.Outputs\RayOutput::class](config/soar.php) 458 | 459 | ![Ray](docs/ray.png) 460 |
461 | 462 |
463 | Telescope 464 | 465 | Telescope 的 `EventWatcher` 和 `LogWatcher` 可观察到 Soar 评分的输出。 466 | 467 | 1. 安装 [laravel/telescope](https://github.com/laravel/telescope) 468 | 2. 配置 `telescope.watchers`: 469 | 470 | ```php 471 | [ 479 | // ... 480 | 481 | Watchers\EventWatcher::class => [ 482 | 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), 483 | 'ignore' => [ 484 | Guanguans\LaravelSoar\Events\OutputtedEvent::class, // ignore `OutputtedEvent` 485 | ], 486 | ], 487 | 488 | // ... 489 | 490 | Watchers\LogWatcher::class => [ 491 | 'enabled' => env('TELESCOPE_LOG_WATCHER', true), 492 | 'level' => 'warning', // `warning` level 493 | ], 494 | 495 | // ... 496 | ], 497 | ]; 498 | ``` 499 | 500 | | ![Telescope event](docs/telescope-event.png) | ![Telescope log](docs/telescope-log.png) | 501 | |----------------------------------------------|------------------------------------------| 502 |
503 | 504 |
505 | 自定义输出器 506 | 507 | 1. 实现 [OutputContract](src/Contracts/OutputContract.php) 508 | 2. 配置 [soar.outputs.Outputs\CustomOutput::class](config/soar.php) 509 |
510 | 511 | ### Soar 命令 512 | 513 |
514 | details 515 | 516 | ```shell 517 | ╰─ php artisan ─╯ 518 | ... 519 | Available commands: 520 | ... 521 | soar 522 | soar:clear Clear the Soar log file 523 | soar:run Run Soar with the given options 524 | soar:score Get the Soar scores of the given SQL statements 525 | ... 526 | ``` 527 | 528 | #### 使用示例(支持标准输入) 529 | 530 | ```shell 531 | echo 'select * from foo; select * from bar;' | php artisan soar:score --ansi 532 | php artisan soar:score --ansi 533 | php artisan soar:score --ansi --option=-query='select * from foo; select * from bar;' 534 | php artisan soar:score --ansi < tests/Fixtures/queries.sql 535 | ``` 536 | 537 | ![commands](docs/commands.gif) 538 |
539 | 540 | ### [Soar 门面及方法](src/Facades/Soar.php) 541 | 542 | ### 查询构建器 [`Mixin`](src/Mixins/QueryBuilderMixin.php) 的[方法](_ide_helper.php) 543 | 544 | ## Composer 脚本 545 | 546 | ```shell 547 | composer checks:required 548 | composer php-cs-fixer:fix 549 | composer test 550 | composer testbench soar:run 551 | composer testbench soar:score 552 | composer testbench:serve 553 | composer testbench:test 554 | composer testbench:user-serve 555 | ``` 556 | 557 | ## 变更日志 558 | 559 | 请参阅 [CHANGELOG](CHANGELOG.md) 获取最近有关更改的更多信息。 560 | 561 | ## 贡献指南 562 | 563 | 请参阅 [CONTRIBUTING](.github/CONTRIBUTING.md) 有关详细信息。 564 | 565 | ## 安全漏洞 566 | 567 | 请查看[我们的安全政策](../../security/policy)了解如何报告安全漏洞。 568 | 569 | ## 贡献者 570 | 571 | * [guanguans](https://github.com/guanguans) 572 | * [所有贡献者](../../contributors) 573 | 574 | ## 协议 575 | 576 | MIT 许可证(MIT)。有关更多信息,请参见[协议文件](LICENSE)。 577 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | ![](docs/soar-bar.gif) | ![](docs/commands.gif) | 2 | |------------------------|------------------------| 3 | 4 | # laravel-soar 5 | 6 | > SQL optimizer and rewriter for laravel. - laravel 的 SQL 优化器和重写器。 7 | 8 | [简体中文](README-zh_CN.md) | [ENGLISH](README.md) 9 | 10 | [![tests](https://github.com/guanguans/laravel-soar/actions/workflows/tests.yml/badge.svg)](https://github.com/guanguans/laravel-soar/actions/workflows/tests.yml) 11 | [![php-cs-fixer](https://github.com/guanguans/laravel-soar/actions/workflows/php-cs-fixer.yml/badge.svg)](https://github.com/guanguans/laravel-soar/actions/workflows/php-cs-fixer.yml) 12 | [![codecov](https://codecov.io/gh/guanguans/laravel-soar/graph/badge.svg?token=EWBG8GV4JD)](https://codecov.io/gh/guanguans/laravel-soar) 13 | [![Latest Stable Version](https://poser.pugx.org/guanguans/laravel-soar/v)](https://packagist.org/packages/guanguans/laravel-soar) 14 | [![GitHub release (with filter)](https://img.shields.io/github/v/release/guanguans/laravel-soar)](https://github.com/guanguans/laravel-soar/releases) 15 | [![Total Downloads](https://poser.pugx.org/guanguans/laravel-soar/downloads)](https://packagist.org/packages/guanguans/laravel-soar) 16 | [![License](https://poser.pugx.org/guanguans/laravel-soar/license)](https://packagist.org/packages/guanguans/laravel-soar) 17 | 18 | ## Features 19 | 20 | * Supports heuristic rule suggestions, index rule suggestions, and `EXPLAIN` information interpretation 21 | * Support calling query builder `Mixin` methods for convenient dumping of rule suggestions 22 | * Automatically monitor output rule suggestions to configured outputs 23 | 24 | ## Related Links 25 | 26 | * [https://github.com/XiaoMi/soar](https://github.com/XiaoMi/soar) 27 | * [https://github.com/guanguans/soar-php](https://github.com/guanguans/soar-php) 28 | 29 | ## Requirement 30 | 31 | * PHP >= 8.1 32 | 33 | ## Installation 34 | 35 | ```shell 36 | composer require guanguans/laravel-soar --dev --ansi -v 37 | ``` 38 | 39 | ## Configuration 40 | 41 | ### Publish files(optional) 42 | 43 | ```shell 44 | php artisan vendor:publish --provider="Guanguans\\LaravelSoar\\SoarServiceProvider" 45 | ``` 46 | 47 | ### :warning: When running in a unix OS non-cli environment, may throw Fatal error: ...Exit Code: 2(Misuse of shell builtins) 48 | 49 | #### Configure sudo password 50 | 51 | ```shell 52 | # Fatal error: Uncaught Guanguans\SoarPHP\Exceptions\ProcessFailedException: The command "'/Users/yaozm/Documents/develop/soar-php/bin/soar.darwin-amd64' '-report-type=json' '-query=select * from users;'" failed. Exit Code: 2(Misuse of shell builtins) Working directory: /Users/yaozm/Documents/develop/soar-php Output: ================ Error Output: ================ panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1938665] goroutine 1 [running]: github.com/pingcap/tidb/util/memory.MemTotalNormal() pkg/mod/github.com/pingcap/tidb@v1.1.0-beta.0.20210601085537-5d7c852770eb/util/memory/meminfo.go:41 +0x65 github.com/pingcap/tidb/util/memory.init.0() pkg/mod/github.com/pingcap/tidb@v1.1.0-beta.0.20210601085537-5d7c852770eb/util/memory/meminfo.go:134 +0x175 in /Users/yaozm/Documents/develop/soar-php/src/Concerns/WithRunable.php:36 Stack trace: #0 /Users/yaozm/Documents/develop/soar-php/test.php(163): Guanguans\SoarPHP\Soar->run() #1 /User in /Users/yaozm/Documents/develop/soar-php/src/Concerns/WithRunable.php on line 36 53 | SOAR_SUDO_PASSWORD='your sudo password' # Set a sudo password to run the soar command with sudo to avoid the above errors. 54 | ``` 55 | 56 | #### [Or configure sudoers](https://github.com/guanguans/soar-php#or-configure-sudoers) 57 | 58 | ## Usage 59 | 60 | ### Enable to automatically output SQL optimization suggestions 61 | 62 |
63 | details 64 | 65 | #### Configure `SOAR_ENABLED` 66 | 67 | ```dotenv 68 | SOAR_ENABLED=true 69 | ``` 70 | 71 | #### Or configure [`soar.enabled`](config/soar.php) 72 | 73 | ```php 74 | (bool) env('SOAR_ENABLED', env('APP_ENV') === 'local'), 78 | 79 | // ... 80 | ]; 81 | ``` 82 |
83 | 84 | ### Install and configure outputs(optional) 85 | 86 |
87 | Clockwork 88 | 89 | 1. Install [itsgoingd/clockwork](https://github.com/itsgoingd/clockwork) 90 | 2. Configure [soar.outputs.Outputs\ClockworkOutput::class](config/soar.php) 91 | 92 | ![Clockwork](docs/clockwork.png) 93 |
94 | 95 |
96 | Console 97 | 98 | 1. Configure [soar.outputs.Outputs\ConsoleOutput::class](config/soar.php) 99 | 100 | ![Console](docs/console.png) 101 |
102 | 103 |
104 | DebugBar 105 | 106 | 1. Install [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) 107 | 2. Configure [soar.outputs.Outputs\DebugBarOutput::class](config/soar.php) 108 | 109 | ![DebugBar](docs/debug-bar.png) 110 |
111 | 112 |
113 | Dump 114 | 115 | 1. Configure [soar.outputs.Outputs\DumpOutput::class](config/soar.php) 116 | 117 | ![Dump](docs/dump.png) 118 |
119 | 120 |
121 | Json 122 | 123 | 1. Configure [soar.outputs.Outputs\JsonOutput::class](config/soar.php) 124 | 125 | ```json 126 | { 127 | "message": "ok", 128 | "soar_scores": [ 129 | { 130 | "Summary": "[☆☆☆☆☆|0分|9.17ms|select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30']", 131 | "Basic": { 132 | "Sample": "select * from `users` where `name` = 'soar' group by `name` having `created_at` > '2023-06-05 03:19:30'", 133 | "Score": 0, 134 | "Star": "☆☆☆☆☆", 135 | "Time": "9.17ms", 136 | "Connection": "mysql", 137 | "Driver": "mysql", 138 | "Tables": [ 139 | "`laravel`.`users`" 140 | ] 141 | }, 142 | "HeuristicRules": [ 143 | { 144 | "Item": "CLA.008", 145 | "Severity": "L2", 146 | "Summary": "请为 GROUP BY 显示添加 ORDER BY 条件", 147 | "Content": "默认 MySQL 会对 'GROUP BY col1, col2, ...' 请求按如下顺序排序 'ORDER BY col1, col2, ...'。如果 GROUP BY 语句不指定 ORDER BY 条件会导致无谓的排序产生,如果不需要排序建议添加 'ORDER BY NULL'。", 148 | "Case": "select c1,c2,c3 from t1 where c1='foo' group by c2", 149 | "Position": 0 150 | }, 151 | { 152 | "Item": "CLA.013", 153 | "Severity": "L3", 154 | "Summary": "不建议使用 HAVING 子句", 155 | "Content": "将查询的 HAVING 子句改写为 WHERE 中的查询条件,可以在查询处理期间使用索引。", 156 | "Case": "SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id", 157 | "Position": 0 158 | }, 159 | { 160 | "Item": "COL.001", 161 | "Severity": "L1", 162 | "Summary": "不建议使用 SELECT * 类型查询", 163 | "Content": "当表结构变更时,使用 * 通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。", 164 | "Case": "select * from tbl where id=1", 165 | "Position": 0 166 | }, 167 | { 168 | "Item": "ERR.002", 169 | "Severity": "L8", 170 | "Summary": "MySQL execute failed", 171 | "Content": "Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_230605111934_bbpxve0adj2dgrcs.users.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by", 172 | "Case": "", 173 | "Position": 0 174 | }, 175 | { 176 | "Item": "GRP.001", 177 | "Severity": "L2", 178 | "Summary": "不建议对等值查询列使用 GROUP BY", 179 | "Content": "GROUP BY 中的列在前面的 WHERE 条件中使用了等值查询,对这样的列进行 GROUP BY 意义不大。", 180 | "Case": "select film_id, title from film where release_year='2006' group by release_year", 181 | "Position": 0 182 | }, 183 | { 184 | "Item": "RES.001", 185 | "Severity": "L4", 186 | "Summary": "非确定性的 GROUP BY", 187 | "Content": "SQL返回的列既不在聚合函数中也不是 GROUP BY 表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo=\"bar\" group by a,该 SQL 返回的结果就是不确定的。", 188 | "Case": "select c1,c2,c3 from t1 where c2='foo' group by c2", 189 | "Position": 0 190 | } 191 | ], 192 | "IndexRules": [ 193 | { 194 | "Item": "IDX.001", 195 | "Severity": "L2", 196 | "Summary": "为laravel库的users表添加索引", 197 | "Content": "为列name添加索引;为列created_at添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 198 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name_created_at` (`name`(191),`created_at`) ;\n", 199 | "Position": 0 200 | } 201 | ], 202 | "Explain": [], 203 | "Backtraces": [ 204 | "#13 /routes/web.php:53", 205 | "#38 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 206 | "#59 /public/index.php:55", 207 | "#60 /server.php:21" 208 | ] 209 | }, 210 | { 211 | "Summary": "[★★★★☆|75分|205.25ms|CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;]", 212 | "Basic": { 213 | "Sample": "CREATE TABLE `users` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `email_verified_at` timestamp NULL DEFAULT NULL,\n `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `users_email_unique` (`email`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;", 214 | "Score": 75, 215 | "Star": "★★★★☆", 216 | "Time": "205.25ms", 217 | "Connection": "mysql", 218 | "Driver": "mysql", 219 | "Tables": [ 220 | "`laravel`.`users`" 221 | ] 222 | }, 223 | "HeuristicRules": [ 224 | { 225 | "Item": "CLA.011", 226 | "Severity": "L1", 227 | "Summary": "建议为表添加注释", 228 | "Content": "为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。", 229 | "Case": "CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8", 230 | "Position": 0 231 | }, 232 | { 233 | "Item": "COL.004", 234 | "Severity": "L1", 235 | "Summary": "请为列添加默认值", 236 | "Content": "请为列添加默认值,如果是 ALTER 操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。", 237 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 238 | "Position": 0 239 | }, 240 | { 241 | "Item": "COL.005", 242 | "Severity": "L1", 243 | "Summary": "列未添加注释", 244 | "Content": "建议对表中每个列添加注释,来明确每个列在表中的含义及作用。", 245 | "Case": "CREATE TABLE tbl (col int) ENGINE=InnoDB;", 246 | "Position": 0 247 | }, 248 | { 249 | "Item": "KWR.003", 250 | "Severity": "L1", 251 | "Summary": "不建议使用复数做列名或表名", 252 | "Content": "表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。", 253 | "Case": "CREATE TABLE tbl ( `books` int )", 254 | "Position": 0 255 | }, 256 | { 257 | "Item": "SEC.002", 258 | "Severity": "L0", 259 | "Summary": "不使用明文存储密码", 260 | "Content": "使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。", 261 | "Case": "create table test(id int,name varchar(20) not null,password varchar(200)not null)", 262 | "Position": 0 263 | }, 264 | { 265 | "Item": "STA.003", 266 | "Severity": "L1", 267 | "Summary": "索引起名不规范", 268 | "Content": "建议普通二级索引以idx_为前缀,唯一索引以uk_为前缀。", 269 | "Case": "select col from now where type!=0", 270 | "Position": 0 271 | } 272 | ], 273 | "IndexRules": [], 274 | "Explain": [], 275 | "Backtraces": [ 276 | "#9 /routes/web.php:22", 277 | "#34 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 278 | "#55 /public/index.php:55", 279 | "#56 /server.php:21" 280 | ] 281 | }, 282 | { 283 | "Summary": "[★★★★☆|80分|1.72ms|update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30']", 284 | "Basic": { 285 | "Sample": "update `users` set `name` = 'name', `password` = 'password', `users`.`updated_at` = '2023-06-05 03:19:30'", 286 | "Score": 80, 287 | "Star": "★★★★☆", 288 | "Time": "1.72ms", 289 | "Connection": "mysql", 290 | "Driver": "mysql", 291 | "Tables": [ 292 | "`laravel`.`users`" 293 | ] 294 | }, 295 | "HeuristicRules": [ 296 | { 297 | "Item": "CLA.015", 298 | "Severity": "L4", 299 | "Summary": "UPDATE 未指定 WHERE 条件", 300 | "Content": "UPDATE 不指定 WHERE 条件一般是致命的,请您三思后行", 301 | "Case": "update tbl set col=1", 302 | "Position": 0 303 | } 304 | ], 305 | "IndexRules": [], 306 | "Explain": [ 307 | { 308 | "Item": "EXP.000", 309 | "Severity": "L0", 310 | "Summary": "Explain信息", 311 | "Content": [ 312 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 313 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 314 | "| 1 | UPDATE | *users* | NULL | index | NULL | PRIMARY | 8 | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL |" 315 | ], 316 | "Case": [ 317 | "### Explain信息解读", 318 | "#### Type信息解读", 319 | "* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大." 320 | ], 321 | "Position": 0 322 | } 323 | ], 324 | "Backtraces": [ 325 | "#10 /routes/web.php:48", 326 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 327 | "#56 /public/index.php:55", 328 | "#57 /server.php:21" 329 | ] 330 | }, 331 | { 332 | "Summary": "[★★★★★|90分|940μs|delete from `users` where `name` = 'soar']", 333 | "Basic": { 334 | "Sample": "delete from `users` where `name` = 'soar'", 335 | "Score": 90, 336 | "Star": "★★★★★", 337 | "Time": "940μs", 338 | "Connection": "mysql", 339 | "Driver": "mysql", 340 | "Tables": [ 341 | "`laravel`.`users`" 342 | ] 343 | }, 344 | "HeuristicRules": [ 345 | { 346 | "Item": "SEC.003", 347 | "Severity": "L0", 348 | "Summary": "使用DELETE/DROP/TRUNCATE等操作时注意备份", 349 | "Content": "在执行高危操作之前对数据进行备份是十分有必要的。", 350 | "Case": "delete from table where col = 'condition'", 351 | "Position": 0 352 | } 353 | ], 354 | "IndexRules": [ 355 | { 356 | "Item": "IDX.001", 357 | "Severity": "L2", 358 | "Summary": "为laravel库的users表添加索引", 359 | "Content": "为列name添加索引; 由于未开启数据采样,各列在索引中的顺序需要自行调整。", 360 | "Case": "ALTER TABLE `laravel`.`users` add index `idx_name` (`name`(191)) ;\n", 361 | "Position": 0 362 | } 363 | ], 364 | "Explain": [ 365 | { 366 | "Item": "EXP.000", 367 | "Severity": "L0", 368 | "Summary": "Explain信息", 369 | "Content": [ 370 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 371 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 372 | "| 1 | DELETE | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where |" 373 | ], 374 | "Case": [ 375 | "### Explain信息解读", 376 | "#### Type信息解读", 377 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描.", 378 | "#### Extra信息解读", 379 | "* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的." 380 | ], 381 | "Position": 0 382 | } 383 | ], 384 | "Backtraces": [ 385 | "#10 /routes/web.php:56", 386 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 387 | "#56 /public/index.php:55", 388 | "#57 /server.php:21" 389 | ] 390 | }, 391 | { 392 | "Summary": "[★★★★★|100分|9.59ms|insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')]", 393 | "Basic": { 394 | "Sample": "insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`) values ('soar', 'soar@soar.com', '2023-06-05 03:19:30', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'lEtsoV3wHW')", 395 | "Score": 100, 396 | "Star": "★★★★★", 397 | "Time": "9.59ms", 398 | "Connection": "mysql", 399 | "Driver": "mysql", 400 | "Tables": [ 401 | "`laravel`.`users`" 402 | ] 403 | }, 404 | "HeuristicRules": [], 405 | "IndexRules": [], 406 | "Explain": [ 407 | { 408 | "Item": "EXP.000", 409 | "Severity": "L0", 410 | "Summary": "Explain信息", 411 | "Content": [ 412 | "| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |", 413 | "|---|---|---|---|---|---|---|---|---|---|---|---|---|", 414 | "| 1 | INSERT | *users* | NULL | ALL | NULL | NULL | NULL | NULL | 0 | 0.00% | ☠️ **O(n)** | NULL |" 415 | ], 416 | "Case": [ 417 | "### Explain信息解读", 418 | "#### Type信息解读", 419 | "* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描." 420 | ], 421 | "Position": 0 422 | } 423 | ], 424 | "Backtraces": [ 425 | "#10 /routes/web.php:43", 426 | "#35 /Users/yaozm/Documents/develop/laravel-soar/src/Http/Middleware/OutputSoarScoreMiddleware.php:37", 427 | "#56 /public/index.php:55", 428 | "#57 /server.php:21" 429 | ] 430 | } 431 | ] 432 | } 433 | ``` 434 |
435 | 436 |
437 | LaraDumps 438 | 439 | 1. Install [laradumps/laradumps](https://github.com/laradumps/laradumps) 440 | 2. Configure [soar.outputs.Outputs\LaraDumpsOutput::class](config/soar.php) 441 | 442 | ![LaraDumps](docs/lara-dumps.png) 443 |
444 | 445 |
446 | Log 447 | 448 | 1. Configure [soar.outputs.Outputs\LogOutput::class](config/soar.php) 449 | 450 | ![Log](docs/log.png) 451 |
452 | 453 |
454 | Ray 455 | 456 | 1. Install [spatie/laravel-ray](https://github.com/spatie/laravel-ray) 457 | 2. Configure [soar.outputs.Outputs\RayOutput::class](config/soar.php) 458 | 459 | ![Ray](docs/ray.png) 460 |
461 | 462 |
463 | Telescope 464 | 465 | Telescope's `EventWatcher` and `LogWatcher` can watch the output of Soar scores. 466 | 467 | 1. Install [laravel/telescope](https://github.com/laravel/telescope) 468 | 2. Configure `telescope.watchers`: 469 | 470 | ```php 471 | [ 479 | // ... 480 | 481 | Watchers\EventWatcher::class => [ 482 | 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), 483 | 'ignore' => [ 484 | Guanguans\LaravelSoar\Events\OutputtedEvent::class, // ignore `OutputtedEvent` 485 | ], 486 | ], 487 | 488 | // ... 489 | 490 | Watchers\LogWatcher::class => [ 491 | 'enabled' => env('TELESCOPE_LOG_WATCHER', true), 492 | 'level' => 'warning', // `warning` level 493 | ], 494 | 495 | // ... 496 | ], 497 | ]; 498 | ``` 499 | 500 | | ![Telescope event](docs/telescope-event.png) | ![Telescope log](docs/telescope-log.png) | 501 | |----------------------------------------------|------------------------------------------| 502 |
503 | 504 |
505 | Custom output 506 | 507 | 1. Implement [OutputContract](src/Contracts/OutputContract.php) 508 | 2. Configure [soar.outputs.Outputs\CustomOutput::class](config/soar.php) 509 |
510 | 511 | ### Soar commands 512 | 513 |
514 | details 515 | 516 | ```shell 517 | ╰─ php artisan ─╯ 518 | ... 519 | Available commands: 520 | ... 521 | soar 522 | soar:clear Clear the Soar log file 523 | soar:run Run Soar with the given options 524 | soar:score Get the Soar scores of the given SQL statements 525 | ... 526 | ``` 527 | 528 | #### Usage example(support standard input) 529 | 530 | ```shell 531 | echo 'select * from foo; select * from bar;' | php artisan soar:score --ansi 532 | php artisan soar:score --ansi 533 | php artisan soar:score --ansi --option=-query='select * from foo; select * from bar;' 534 | php artisan soar:score --ansi < tests/Fixtures/queries.sql 535 | ``` 536 | 537 | ![commands](docs/commands.gif) 538 |
539 | 540 | ### [Soar facade and methods](src/Facades/Soar.php) 541 | 542 | ### [Methods](_ide_helper.php) of the query builder [`Mixin`](src/Mixins/QueryBuilderMixin.php) 543 | 544 | ## Composer scripts 545 | 546 | ```shell 547 | composer checks:required 548 | composer php-cs-fixer:fix 549 | composer test 550 | composer testbench soar:run 551 | composer testbench soar:score 552 | composer testbench:serve 553 | composer testbench:test 554 | composer testbench:user-serve 555 | ``` 556 | 557 | ## Changelog 558 | 559 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 560 | 561 | ## Contributing 562 | 563 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 564 | 565 | ## Security Vulnerabilities 566 | 567 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 568 | 569 | ## Credits 570 | 571 | * [guanguans](https://github.com/guanguans) 572 | * [All Contributors](../../contributors) 573 | 574 | ## License 575 | 576 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 577 | --------------------------------------------------------------------------------