├── .php-cs-fixer.php ├── .phpunit.cache └── test-results ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── config └── enlighten.php ├── database └── migrations │ ├── 2020_09_20_000000_create_enlighten_runs_table.php │ ├── 2020_09_21_000000_create_enlighten_example_groups_table.php │ ├── 2020_09_22_000000_create_enlighten_examples_table.php │ ├── 2020_09_23_000000_create_enlighten_example_requests_table.php │ ├── 2020_09_24_000000_create_enlighten_exceptions_table.php │ ├── 2020_10_18_000000_create_enlighten_example_snippets_table.php │ └── 2020_10_23_000000_create_enlighten_example_queries_table.php ├── dist ├── css │ └── app.css └── js │ ├── app.js │ ├── build.js │ └── prism.js ├── docs └── areas.md ├── preview.png ├── rector.php ├── resources ├── css │ ├── app.css │ ├── code-snippets.css │ └── prism.css ├── lang │ ├── en │ │ └── messages.php │ └── es │ │ └── messages.php └── views │ ├── area │ ├── endpoints.blade.php │ ├── features.blade.php │ └── modules.blade.php │ ├── components │ ├── app-layout.blade.php │ ├── area-module-panel.blade.php │ ├── breadcrumbs.blade.php │ ├── content-table.blade.php │ ├── dynamic-tabs-menu.blade.php │ ├── dynamic-tabs.blade.php │ ├── edit-button.blade.php │ ├── example-breadcrumbs.blade.php │ ├── example-snippets.blade.php │ ├── example-tabs.blade.php │ ├── exception-info.blade.php │ ├── expansible-section.blade.php │ ├── group-breadcrumbs.blade.php │ ├── html-response.blade.php │ ├── iframe.blade.php │ ├── info-panel.blade.php │ ├── json-response.blade.php │ ├── key-value.blade.php │ ├── panel-title.blade.php │ ├── pre.blade.php │ ├── queries-info.blade.php │ ├── request-info.blade.php │ ├── request-input-table.blade.php │ ├── response-info.blade.php │ ├── response-preview.blade.php │ ├── route-parameters-table.blade.php │ ├── runs-table.blade.php │ ├── scroll-to-top.blade.php │ ├── search-box-static.blade.php │ ├── search-box.blade.php │ ├── stats-badge.blade.php │ ├── status-badge.blade.php │ └── svg-logo.blade.php │ ├── example │ └── show.blade.php │ ├── group │ └── show.blade.php │ ├── intro.blade.php │ ├── layout │ └── main.blade.php │ ├── run │ └── index.blade.php │ └── search │ └── results.blade.php └── src ├── CodeExamples ├── BaseCodeResultFormat.php ├── CodeExampleCreator.php ├── CodeInspector.php ├── CodeResultExporter.php ├── CodeResultFormat.php ├── CodeResultTransformer.php ├── HtmlResultFormat.php └── PlainCodeResultFormat.php ├── Console ├── Commands │ ├── ExportDocumentationCommand.php │ ├── FreshCommand.php │ ├── GenerateDocumentationCommand.php │ ├── InstallCommand.php │ ├── MigrateCommand.php │ └── stubs │ │ ├── BaseTestCase.php.stub │ │ └── EnlightenTestCase.php.stub ├── ContentRequest.php └── DocumentationExporter.php ├── Contracts ├── Example.php ├── ExampleBuilder.php ├── ExampleGroupBuilder.php ├── Run.php ├── RunBuilder.php └── VersionControl.php ├── Drivers ├── BaseExampleBuilder.php ├── BaseExampleGroupBuilder.php ├── DatabaseExampleBuilder.php ├── DatabaseExampleGroupBuilder.php └── DatabaseRunBuilder.php ├── Enlighten.php ├── ExampleCreator.php ├── ExampleProfile.php ├── ExceptionInfo.php ├── Exceptions ├── InvalidDriverException.php └── LaravelNotPresent.php ├── Facades ├── Settings.php └── VersionControl.php ├── Http ├── Controllers │ ├── ListRunsController.php │ ├── SearchController.php │ ├── ShowAreaController.php │ ├── ShowExampleController.php │ ├── ShowExampleGroupController.php │ └── WelcomeController.php └── routes │ ├── api.php │ └── web.php ├── HttpExamples ├── HttpExampleCreator.php ├── HttpExampleCreatorMiddleware.php ├── RequestInfo.php ├── RequestInspector.php ├── ResponseInfo.php ├── ResponseInspector.php ├── RouteInfo.php ├── RouteInspector.php └── SessionInspector.php ├── Models ├── Area.php ├── Concerns │ ├── GetStats.php │ ├── GetsStatsFromGroups.php │ └── ReadsDynamicAttributes.php ├── Endpoint.php ├── Example.php ├── ExampleException.php ├── ExampleGroup.php ├── ExampleQuery.php ├── ExampleRequest.php ├── ExampleSnippet.php ├── Module.php ├── ModuleCollection.php ├── ReplacesValues.php ├── Run.php ├── Statable.php ├── Status.php ├── Statusable.php └── Wrappable.php ├── Providers ├── EnlightenServiceProvider.php ├── RegistersConsoleConfiguration.php ├── RegistersDatabaseConnection.php └── RegistersViewComponents.php ├── Section.php ├── Settings.php ├── Tests ├── EnlightenSetup.php ├── ExceptionRecorder.php └── bootstrap.php ├── Utils ├── Annotations.php ├── FileLink.php ├── Git.php └── JsonFormatter.php ├── View └── Components │ ├── AppLayoutComponent.php │ ├── BreadcrumbsComponent.php │ ├── CodeExampleComponent.php │ ├── DynamicTabsComponent.php │ ├── EditButtonComponent.php │ ├── ExampleBreadcrumbs.php │ ├── ExampleTabsComponent.php │ ├── ExceptionInfoComponent.php │ ├── GroupBreadcrumbs.php │ ├── HtmlResponseComponent.php │ ├── KeyValueComponent.php │ ├── RepresentsStatusAsColor.php │ ├── RequestInfoComponent.php │ ├── RequestInputTableComponent.php │ ├── ResponseInfoComponent.php │ ├── RouteParametersTableComponent.php │ ├── SearchBoxComponent.php │ ├── SearchBoxStaticComponent.php │ ├── StatsBadgeComponent.php │ └── StatusBadgeComponent.php └── helpers.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/config') 8 | ->in(__DIR__.'/database') 9 | ->in(__DIR__.'/src') 10 | ->in(__DIR__.'/tests') 11 | ; 12 | 13 | return (new Config) 14 | ->setRiskyAllowed(true) 15 | ->setRules([ 16 | '@PSR2' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 19 | 'single_quote' => true, 20 | 'visibility_required' => false, 21 | ]) 22 | ->setFinder($finder); 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Styde.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styde/enlighten", 3 | "description": "Enlighten your APIs with auto-generated documentation", 4 | "type": "library", 5 | "require": { 6 | "php": "^8.2", 7 | "laravel/framework": "^11.0", 8 | "ext-json": "*", 9 | "guzzlehttp/guzzle": "^7.2" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^11.0.1", 13 | "orchestra/testbench": "^9.0", 14 | "friendsofphp/php-cs-fixer": "^3.55", 15 | "rector/rector": "^1.0" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Duilio Palacios", 21 | "email": "duilio@styde.net" 22 | }, 23 | { 24 | "name": "Jeffer Ochoa", 25 | "email": "jeffer.8a@gmail.com" 26 | } 27 | ], 28 | "autoload": { 29 | "files": ["src/helpers.php"], 30 | "psr-4": { 31 | "Styde\\Enlighten\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Styde\\Enlighten\\Providers\\EnlightenServiceProvider" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config/enlighten.php: -------------------------------------------------------------------------------- 1 | true, 6 | 7 | 'driver' => 'database', 8 | 9 | // Add values to this array to hide certain sections 10 | // from your views. For all valid sections check 11 | // the constants in \Styde\Enlighten\Section. 12 | 'hide' => [ 13 | // 'queries', 14 | // 'html', 15 | // 'blade', 16 | // 'route_parameters', 17 | // 'request_input', 18 | // 'request_headers', 19 | // 'response_headers', 20 | // 'session', 21 | // 'exception', 22 | ], 23 | 24 | // Default directory to export the documentation. 25 | 'docs_base_dir' => 'public/docs', 26 | // Default base URL for exported the documentation. 27 | 'docs_base_url' => '/docs', 28 | 29 | // Display / hide quick access links to open your IDE from the UI 30 | 'developer_mode' => true, 31 | 'editor' => 'phpstorm', // phpstorm, vscode or sublime 32 | 33 | 'tests' => [ 34 | // Add regular expressions to skip certain test classes and test methods. 35 | // i.e. Tests\Unit\* will ignore all the tests in the Tests\Unit\ suite, 36 | // validates_* will ignore all the tests that start with "validates_". 37 | 'ignore' => [], 38 | ], 39 | 40 | // Use the arrays below to hide or obfuscate parameters 41 | // from the HTTP requests including headers, input, 42 | // query parameters, session data, and others. 43 | 'request' => [ 44 | 'headers' => [ 45 | 'hide' => [], 46 | 'overwrite' => [], 47 | ], 48 | 'query' => [ 49 | 'hide' => [], 50 | 'overwrite' => [], 51 | ], 52 | 'input' => [ 53 | 'hide' => [], 54 | 'overwrite' => [], 55 | ], 56 | ], 57 | 58 | 'response' => [ 59 | 'headers' => [ 60 | 'hide' => [], 61 | 'overwrite' => [], 62 | ], 63 | 'body' => [ 64 | 'hide' => [], 65 | 'overwrite' => [], 66 | ] 67 | ], 68 | 69 | 'session' => [ 70 | 'hide' => [], 71 | 'overwrite' => [], 72 | ], 73 | 74 | // Configure a default view for the panel. Options: features, modules and endpoints. 75 | 'area_view' => 'features', 76 | 77 | // Customise the name and view template of each area that will be shown in the panel. 78 | // By default, each area slug will represent a "test suite" in the tests directory. 79 | // Each area can have a different view style, ex: features, modules or endpoints. 80 | 'areas' => [ 81 | [ 82 | 'slug' => 'api', 83 | 'name' => 'API', 84 | 'view' => 'endpoints', 85 | ], 86 | [ 87 | 'slug' => 'feature', 88 | 'name' => 'Features', 89 | 'view' => 'modules', 90 | ], 91 | [ 92 | 'slug' => 'unit', 93 | 'name' => 'Unit', 94 | 'view' => 'features', 95 | ], 96 | ], 97 | 98 | // If you want to use "modules" or "endpoints" as the area view, 99 | // you will need to configure the modules adding their names 100 | // and patterns to match the test classes and/or routes. 101 | 'modules' => [ 102 | [ 103 | 'name' => 'Users', 104 | 'classes' => ['*User*'], 105 | 'routes' => ['users/*'], 106 | ], 107 | [ 108 | 'name' => 'Other Modules', 109 | 'classes' => ['*'], 110 | ], 111 | ] 112 | ]; 113 | -------------------------------------------------------------------------------- /database/migrations/2020_09_20_000000_create_enlighten_runs_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_runs')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_runs', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->string('branch'); 24 | $table->string('head'); 25 | $table->boolean('modified'); 26 | 27 | $table->unique(['head', 'modified']); 28 | 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::connection('enlighten')->dropIfExists('enlighten_runs'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2020_09_21_000000_create_enlighten_example_groups_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_example_groups')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_example_groups', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->foreignId('run_id') 24 | ->references('id') 25 | ->on('enlighten_runs') 26 | ->cascadeOnDelete(); 27 | 28 | $table->string('class_name'); 29 | 30 | $table->unique(['run_id', 'class_name']); 31 | 32 | $table->string('title'); 33 | $table->string('slug'); 34 | 35 | $table->text('description')->nullable(); 36 | $table->string('area'); 37 | 38 | $table->unsignedInteger('order_num')->nullable(); 39 | 40 | $table->timestamps(); 41 | }); 42 | } 43 | 44 | /** 45 | * Reverse the migrations. 46 | * 47 | * @return void 48 | */ 49 | public function down() 50 | { 51 | Schema::connection('enlighten')->dropIfExists('enlighten_example_groups'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/migrations/2020_09_22_000000_create_enlighten_examples_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_examples')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_examples', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->foreignId('group_id') 24 | ->references('id') 25 | ->on('enlighten_example_groups') 26 | ->cascadeOnDelete(); 27 | 28 | $table->string('method_name'); 29 | $table->string('data_name')->nullable(); 30 | $table->json('provided_data')->nullable(); 31 | 32 | $table->string('slug'); 33 | 34 | $table->unique(['group_id', 'method_name', 'data_name']); 35 | $table->unique(['group_id', 'slug', 'data_name']); 36 | 37 | $table->integer('line')->nullable(); 38 | $table->string('title'); 39 | $table->text('description')->nullable(); 40 | 41 | $table->string('test_status')->nullable(); 42 | $table->string('status')->nullable(); 43 | 44 | $table->unsignedInteger('order_num')->nullable(); 45 | 46 | $table->timestamps(); 47 | }); 48 | } 49 | 50 | /** 51 | * Reverse the migrations. 52 | * 53 | * @return void 54 | */ 55 | public function down() 56 | { 57 | Schema::connection('enlighten')->dropIfExists('enlighten_examples'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /database/migrations/2020_09_23_000000_create_enlighten_example_requests_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_example_requests')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_example_requests', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->foreignId('example_id') 24 | ->references('id') 25 | ->on('enlighten_examples') 26 | ->cascadeOnDelete(); 27 | 28 | $table->json('request_headers'); 29 | $table->string('request_method'); 30 | $table->string('request_path'); 31 | $table->json('request_query_parameters'); 32 | $table->json('request_input'); 33 | $table->json('request_files'); 34 | $table->string('route')->nullable(); 35 | $table->json('route_parameters')->nullable(); 36 | $table->char('response_status', 3)->nullable(); 37 | $table->boolean('follows_redirect')->default(false); 38 | $table->json('response_headers')->nullable(); 39 | $table->longText('response_body')->nullable(); 40 | $table->text('response_template')->nullable(); 41 | 42 | $table->text('session_data')->nullable(); 43 | 44 | $table->timestamps(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | * 51 | * @return void 52 | */ 53 | public function down() 54 | { 55 | Schema::connection('enlighten')->dropIfExists('enlighten_example_requests'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /database/migrations/2020_09_24_000000_create_enlighten_exceptions_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_exceptions')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_exceptions', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->foreignId('example_id') 24 | ->unique() 25 | ->references('id') 26 | ->on('enlighten_examples') 27 | ->cascadeOnDelete(); 28 | 29 | $table->string('code'); 30 | $table->string('class_name'); 31 | $table->longText('message'); 32 | $table->string('file'); 33 | $table->unsignedSmallInteger('line'); 34 | $table->longText('trace'); 35 | $table->longText('extra'); 36 | 37 | $table->timestamps(); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | * 44 | * @return void 45 | */ 46 | public function down() 47 | { 48 | Schema::connection('enlighten')->dropIfExists('enlighten_exceptions'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /database/migrations/2020_10_18_000000_create_enlighten_example_snippets_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_example_snippets')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_example_snippets', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->string('key')->unique()->nullable(); 24 | 25 | $table->foreignId('example_id') 26 | ->references('id') 27 | ->on('enlighten_examples') 28 | ->cascadeOnDelete(); 29 | 30 | $table->longText('code'); 31 | 32 | $table->longText('result')->nullable(); 33 | 34 | $table->timestamps(); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::connection('enlighten')->dropIfExists('enlighten_example_snippets'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2020_10_23_000000_create_enlighten_example_queries_table.php: -------------------------------------------------------------------------------- 1 | hasTable('enlighten_example_queries')) { 17 | return; 18 | } 19 | 20 | Schema::connection('enlighten')->create('enlighten_example_queries', function (Blueprint $table) { 21 | $table->id(); 22 | 23 | $table->foreignId('example_id') 24 | ->references('id') 25 | ->on('enlighten_examples') 26 | ->cascadeOnDelete(); 27 | 28 | $table->text('sql'); 29 | 30 | $table->longText('bindings'); 31 | 32 | $table->string('time'); 33 | 34 | $table->foreignId('request_id') 35 | ->nullable() 36 | ->references('id') 37 | ->on('enlighten_example_requests'); 38 | 39 | $table->foreignId('snippet_id') 40 | ->nullable() 41 | ->references('id') 42 | ->on('enlighten_example_snippets'); 43 | 44 | $table->timestamps(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | * 51 | * @return void 52 | */ 53 | public function down() 54 | { 55 | Schema::connection('enlighten')->dropIfExists('enlighten_example_queries'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/areas.md: -------------------------------------------------------------------------------- 1 | # Areas 2 | 3 | Each area in Enlighten represents a **test suite**. 4 | 5 | We use the generic, less technical, term of "Areas" because one of the objectives of Enlighten 6 | is generating the documentation for your end users, clients or even the QA department. 7 | 8 | By default, each area is the second segment of your test class FQN in slug format, i.e.: 9 | 10 | `Tests\Feature\CreateUserTest` -> `feature` 11 | 12 | `Tests\Unit\UserTest` -> `unit` 13 | 14 | ## Customise the displayed areas (optional): 15 | 16 | If you wish to customise the areas that are displayed in the Enlighten panel, add the `areas` key in your Enlighten config, as an associative or simple array: 17 | 18 | ```php 19 | // config/enlighten.php 20 | return [ 21 | //... 22 | 23 | 'areas' => ['feature', 'unit'], 24 | 25 | // or: 26 | 27 | 'areas' => ['api' => 'API', 'feature' => 'Feature'], 28 | 29 | //... 30 | ]; 31 | ``` 32 | 33 | Otherwise, leave that config option commented and Enlighten will show all the available areas. 34 | 35 | This is a display option, therefore it will not ignore tests. If you wish to ignore tests please refer to our readme file. 36 | 37 | ## Advanced configuration (optional) 38 | 39 | You can also create your own custom area resolver if for any reason your areas / test suites are not the second segment of your test classes. 40 | 41 | This is an advanced option, that won't be necessary in most cases. 42 | 43 | For example if your area is represented by the forth segment of your test classes instead of the second one, you can add the following logic to a boot method of a Service Provider in your app: 44 | 45 | ```php 46 | if (config('enlighten.enabled')) { 47 | \Styde\Enlighten\Facades\Settings::setCustomAreaResolver(function ($className) { 48 | return explode('\\', $className)[3]; 49 | }); 50 | } 51 | ``` 52 | 53 | Now `Enlighten::getAreaSlug('Modules\Field\Tests\Feature\FieldGroupTest')` will return 'feature' instead of 'field'. 54 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StydeNet/enlighten/815e0ead1dc1bf79ca79c04525bcc7577c030262/preview.png -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]) 14 | // uncomment to reach your current PHP version 15 | ->withPhpSets(php82: true) 16 | ->withRules([ 17 | AddVoidReturnTypeWhereNoReturnRector::class, 18 | ]) 19 | ->withSkip([ 20 | ClosureToarrowFunctionRector::class => [ 21 | __DIR__ . '/tests', 22 | ], 23 | ]); 24 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @import "tailwindcss/base"; 3 | @import "tailwindcss/components"; 4 | @import "./code-snippets.css"; 5 | /* purgecss end ignore */ 6 | 7 | @import "tailwindcss/utilities"; 8 | 9 | /* purgecss start ignore */ 10 | @import "./prism.css"; 11 | 12 | .code-toolbar { 13 | @apply h-full; 14 | } 15 | 16 | @media (min-width: 640px) { 17 | .md\:columns-2 { columns: 2 } 18 | } 19 | 20 | [x-cloak] { 21 | display: none; 22 | } 23 | 24 | pre.sf-dump { 25 | background-color: transparent !important; 26 | } 27 | .sf-dump { 28 | @apply h-full; 29 | } 30 | 31 | .intro-page-content { 32 | @apply px-4 pb-8 w-full mx-auto block; 33 | } 34 | 35 | .intro-page-content p:nth-child(2) { 36 | @apply flex; 37 | } 38 | .intro-page-content p:nth-child(2)>* { 39 | margin-right: 0.5rem; 40 | } 41 | /* purgecss end ignore */ 42 | -------------------------------------------------------------------------------- /resources/css/code-snippets.css: -------------------------------------------------------------------------------- 1 | .enlighten-symbol, 2 | .enlighten-int, 3 | .enlighten-float, 4 | .enlighten-string, 5 | .enlighten-class, 6 | .enlighten-property, 7 | .enlighten-bool, 8 | .enlighten-null { 9 | font-size: 0.9rem; 10 | } 11 | 12 | .enlighten-symbol { 13 | @apply text-white; 14 | } 15 | .enlighten-int { 16 | @apply text-white; 17 | } 18 | .enlighten-float { 19 | @apply text-white; 20 | } 21 | .enlighten-string { 22 | color: #a6e22e; 23 | } 24 | .enlighten-class { 25 | @apply text-white; 26 | } 27 | .enlighten-property { 28 | color: #a6e22e; 29 | } 30 | .enlighten-bool { 31 | @apply text-green-500; 32 | } 33 | .enlighten-null { 34 | color: #48beb4; 35 | } 36 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'All Areas', 5 | 'all_endpoints' => 'All Endpoints', 6 | 'branch_commit' => 'Branch / Commit', 7 | 'dashboard' => 'Dashboard', 8 | 'date' => 'Date', 9 | 'features' => 'Features', 10 | 'input' => 'Input', 11 | 'output' => 'Output', 12 | 'pattern' => 'Pattern', 13 | 'requirement' => 'Requirement', 14 | 'response' => 'Response', 15 | 'route_parameter' => 'Route Parameter', 16 | 'session_data' => 'Session Data', 17 | 'snippet' => 'Snippet', 18 | 'stats' => 'Stats', 19 | 'test_queries' => 'Test Queries', 20 | 'request_queries' => 'Request Queries', 21 | 'snippet_queries' => 'Snippet Queries', 22 | 'there_are_no_examples_to_show' => 'There are no examples to show.', 23 | 'time' => 'Time', 24 | 'value' => 'Value', 25 | 'view' => 'View' 26 | ]; 27 | -------------------------------------------------------------------------------- /resources/lang/es/messages.php: -------------------------------------------------------------------------------- 1 | 'Todas las áreas', 5 | 'all_endpoints' => 'Todos las URLs', 6 | 'branch_commit' => 'Rama / Confirmación', 7 | 'dashboard' => 'Tablero', 8 | 'date' => 'Fecha', 9 | 'features' => 'Funcionalidades', 10 | 'input' => 'Entrada', 11 | 'output' => 'Salida', 12 | 'pattern' => 'Patrón', 13 | 'requirement' => 'Requerimiento', 14 | 'response' => 'Respuesta', 15 | 'route_parameter' => 'Parámetro de ruta', 16 | 'session_data' => 'Datos de sesión', 17 | 'snippet' => 'Snippet', 18 | 'stats' => 'Estadísticas', 19 | 'request_queries' => 'Consultas en peticiones', 20 | 'test_queries' => 'Consultas en la prueba', 21 | 'snippet_queries' => 'Consultas en Snippets', 22 | 'there_are_no_examples_to_show' => 'No hay ejemplos para mostrar.', 23 | 'time' => 'Hora', 24 | 'value' => 'Valor', 25 | 'view' => 'Vista' 26 | ]; 27 | -------------------------------------------------------------------------------- /resources/views/area/features.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{ $area->name }} 3 | 4 | @foreach($groups as $group) 5 | @if($group->description) 6 |

{{ $group->description }}

7 | @endif 8 | 9 |
10 | 59 |
60 | @endforeach 61 |
62 | -------------------------------------------------------------------------------- /resources/views/area/modules.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{ $area->name }} 3 | 4 |
5 |
6 | @forelse($modules as $module) 7 | 8 | @empty 9 |

10 | {{ __('enlighten::messages.there_are_no_examples_to_show') }} 11 |

12 | @endforelse 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /resources/views/components/area-module-panel.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $module->name }} 4 | 5 |
6 | 16 |
17 | -------------------------------------------------------------------------------- /resources/views/components/breadcrumbs.blade.php: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /resources/views/components/content-table.blade.php: -------------------------------------------------------------------------------- 1 | @props(['examples']) 2 | 3 |
4 | {{ __('enlighten::messages.features') }} 5 | 19 |
20 | -------------------------------------------------------------------------------- /resources/views/components/dynamic-tabs-menu.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($tabs_collection->count() > 1) 3 |
4 | @foreach($tabs_collection as $name => $title) 5 | @if(isset($$name) && $$name instanceof $htmlable && !$$name->isEmpty()) 6 | 14 | @endif 15 | @endforeach 16 |
17 | @endif 18 | 19 |
20 | @foreach($tabs_collection as $name => $title) 21 | @if(isset($$name) && $$name instanceof $htmlable && !$$name->isEmpty()) 22 |
23 | {!! $$name !!} 24 |
25 | @endif 26 | @endforeach 27 |
28 |
29 | -------------------------------------------------------------------------------- /resources/views/components/dynamic-tabs.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($tabs_collection->count() > 1) 3 |
4 |
5 | @foreach($tabs_collection as $name => $title) 6 | @if(isset($$name) && $$name instanceof $htmlable && !$$name->isEmpty()) 7 | 12 | @endif 13 | @endforeach 14 |
15 |
16 | @endif 17 | 18 |
19 | @foreach($tabs_collection as $name => $title) 20 | @if(isset($$name) && $$name instanceof $htmlable && !$$name->isEmpty()) 21 |
22 | {!! $$name !!} 23 |
24 | @endif 25 | @endforeach 26 |
27 |
28 | -------------------------------------------------------------------------------- /resources/views/components/edit-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/example-breadcrumbs.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/example-snippets.blade.php: -------------------------------------------------------------------------------- 1 | @foreach($snippets as $snippet) 2 |
3 | 4 | {{ __('enlighten::messages.snippet') }} 5 | 6 | 7 | 8 | {{ __('enlighten::messages.output') }} 9 |
11 | {!! $snippet->result_code !!} 12 |
13 |
14 |
15 | @endforeach 16 | -------------------------------------------------------------------------------- /resources/views/components/example-tabs.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if($showException) 3 | 4 | 5 | 6 | @endif 7 | @if($showQueries) 8 | 9 | 10 | 11 | @endif 12 | @if($showRequests) 13 | 14 | 15 | @foreach($requestTabs as $tab) 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | @if($tab->showSession) 26 | 27 | {{ __('enlighten::messages.session_data') }} 28 | 29 | 30 | @endif 31 |
32 |
33 | @if($tab->showPreviewOnly) 34 | 35 | @else 36 | 37 | @endif 38 |
39 |
40 |
41 | @endforeach 42 |
43 |
44 | @endif 45 |
46 | -------------------------------------------------------------------------------- /resources/views/components/exception-info.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ $exception->class_name }}: {{ $exception->message }} 5 | 6 | 7 | 8 | 9 | @if($exception->extra) 10 | 11 | 12 | @endif 13 | 14 | @foreach($trace as $data) 15 |
16 | @if(!empty($title)) 17 | {{ $title }} 19 | @endif 20 | 21 | 22 | @if($data['file'] && $data['line']) 23 | 24 | 25 | 26 | @endif 27 | 28 | 37 | 38 | @if(!empty($data['args'])) 39 | 40 | 47 | 48 | @endif 49 |
{{ $data['file'] }} : {{ $data['line'] }}
29 | {{ $data['function'] }} 30 | @if(!empty($data['args'])) 31 | 32 | 33 | 34 | 35 | @endif 36 |
41 |
42 |
43 | 44 |
45 |
46 |
50 |
51 | 52 | @unless($loop->last) 53 | 54 | @endunless 55 | @endforeach 56 |
57 | -------------------------------------------------------------------------------- /resources/views/components/expansible-section.blade.php: -------------------------------------------------------------------------------- 1 | @props(['collapsed' => true]) 2 | 3 |
13 | 19 |
20 | {{ $slot }} 21 |
22 |
23 | -------------------------------------------------------------------------------- /resources/views/components/group-breadcrumbs.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/html-response.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @if ($showHtml) 7 | 8 | 9 | 10 | @endif 11 | @if ($showTemplate) 12 | 13 | 14 | 15 | @endif 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/views/components/iframe.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/components/info-panel.blade.php: -------------------------------------------------------------------------------- 1 |
merge(['class' => 'bg-gray-800 rounded-md overflow-hidden']) }}> 2 | 3 |

{{ $title }}

4 |
5 | {!! $slot !!} 6 |
7 | -------------------------------------------------------------------------------- /resources/views/components/json-response.blade.php: -------------------------------------------------------------------------------- 1 |
{{ enlighten_json_prettify($json) }}
4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/key-value.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(!empty($title)) 3 | {{ $title }} 4 | @endif 5 | 6 | 7 | @foreach($items as $key => $value) 8 | 9 | 10 | 12 | 13 | @endforeach 14 |
{{ $key }}: {!! $value !!}
15 |
16 | -------------------------------------------------------------------------------- /resources/views/components/panel-title.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 3 | -------------------------------------------------------------------------------- /resources/views/components/pre.blade.php: -------------------------------------------------------------------------------- 1 | @props(['language', 'code']) 2 | 3 |
4 |
{{ $code }}
7 |
-------------------------------------------------------------------------------- /resources/views/components/queries-info.blade.php: -------------------------------------------------------------------------------- 1 | @props(['example']) 2 | 3 | @php 4 | $queryGroups = $example->queries->groupBy('request_id'); 5 | @endphp 6 | 7 | @foreach($queryGroups as $group) 8 |
11 | 24 | @if($group->first()->context === 'request') 25 |
26 |
27 | @foreach($group as $query) 28 | 29 | {{ __('enlighten::messages.time') }}: {{ $query->time }} 30 | 31 | @if($query->bindings) 32 | 33 | @endif 34 | 35 | @endforeach 36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 | @else 44 |
45 |
46 | @foreach($group as $query) 47 | 48 | {{ __('enlighten::messages.time') }}: {{ $query->time }} 49 | 50 | @if($query->bindings) 51 | 52 | @endif 53 | 54 | @endforeach 55 |
56 |
57 | @endif 58 |
59 | @endforeach 60 | -------------------------------------------------------------------------------- /resources/views/components/request-info.blade.php: -------------------------------------------------------------------------------- 1 | 2 | Request 3 | 4 | 5 | 6 | @if($showRouteParameters) 7 | 8 | @endif 9 | 10 | @if($showInput) 11 | 12 | @endif 13 | 14 | @if($showHeaders) 15 | 16 | @endif 17 | 18 | -------------------------------------------------------------------------------- /resources/views/components/request-input-table.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | @foreach($input as $name => $value) 10 | 11 | 12 | 19 | 20 | @endforeach 21 | 22 |
{{ __('enlighten::messages.input') }}{{ __('enlighten::messages.value') }}
{{ $name }} 13 | @if(is_bool($value)) 14 | {{ $value ? 'true' : 'false' }} 15 | @else 16 | {{ $value }} 17 | @endif 18 |
23 | -------------------------------------------------------------------------------- /resources/views/components/response-info.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{ __('enlighten::messages.response') }} 3 |
4 | {{ $status }} 5 | {{ $request->response_type }} 6 |
7 | 8 | @if($showHeaders) 9 | 10 | @endif 11 |
12 | -------------------------------------------------------------------------------- /resources/views/components/response-preview.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($request->response_type === 'JSON') 3 | 4 | @elseif($request->response_type === 'HTML') 5 | 6 | @endif 7 |
8 | -------------------------------------------------------------------------------- /resources/views/components/route-parameters-table.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | @foreach($parameters as $parameter) 11 | 12 | 13 | 14 | 15 | 16 | @endforeach 17 | 18 |
{{ __('enlighten::messages.route_parameter') }}{{ __('enlighten::messages.pattern') }}{{ __('enlighten::messages.requirement') }}
{{ $parameter['name'] }}{{ $parameter['pattern'] }}{{ $parameter['requirement'] }}
19 | -------------------------------------------------------------------------------- /resources/views/components/runs-table.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | @foreach ($runs as $run) 11 | 12 | 17 | 18 | 21 | 24 | 25 | @endforeach 26 | 27 |
{{ __('enlighten::messages.branch_commit') }}{{ __('enlighten::messages.date') }}{{ __('enlighten::messages.stats') }}
13 | {{ $run->branch }} 14 | @if ($run->modified)*@endif 15 | {{ $run->head }} 16 | {{ $run->created_at->toDateTimeString() }} 19 | 20 | 22 | {{ __('enlighten::messages.view') }} 23 |
28 | -------------------------------------------------------------------------------- /resources/views/components/scroll-to-top.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/search-box-static.blade.php: -------------------------------------------------------------------------------- 1 |
12 | 21 |
22 |
    23 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/views/components/search-box.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /resources/views/components/stats-badge.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if ($color == 'success') 3 | {{ $total }} 4 | @else 5 | {{ $positive }} / {{ $total }} 6 | @endif 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/status-badge.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if ($color === 'success') 3 | 4 | @elseif ($color === 'failure') 5 | 6 | @else 7 | 8 | @endif 9 | 10 | -------------------------------------------------------------------------------- /resources/views/components/svg-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/example/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | {{ $example->title }} 11 | 12 |
13 |
14 | 15 | @if($example->description) 16 |

{{ $example->description }}

17 | @endif 18 | 19 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /resources/views/group/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title }} 8 | 9 | @if($group->description) 10 |

{{ $group->description }}

11 | @endif 12 | 13 | 61 |
62 | -------------------------------------------------------------------------------- /resources/views/intro.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {!! $content !!} 5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /resources/views/layout/main.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | Laravel Enlighten 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | {{ $top ?? '' }} 18 | {{ $title ?? 'Dashboard' }} 19 | {{ $slot }} 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/views/run/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /resources/views/search/results.blade.php: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/CodeExamples/BaseCodeResultFormat.php: -------------------------------------------------------------------------------- 1 | ', $code, '']); 10 | } 11 | 12 | public function indentation($level): string 13 | { 14 | return str_repeat(' ', $level * 4); 15 | } 16 | 17 | public function space(): string 18 | { 19 | return ' '; 20 | } 21 | 22 | public function line(): string 23 | { 24 | return PHP_EOL; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CodeExamples/CodeExampleCreator.php: -------------------------------------------------------------------------------- 1 | exampleCreator->getCurrentExample(); 19 | 20 | if (is_null($testExample)) { 21 | return $callback(); 22 | } 23 | 24 | $testExample->addSnippet($key, $this->codeInspector->getCodeFrom($callback)); 25 | 26 | try { 27 | $result = call_user_func($callback); 28 | 29 | $testExample->setSnippetResult(CodeResultTransformer::export($result)); 30 | 31 | return $result; 32 | } catch (Throwable $throwable) { 33 | $this->exampleCreator->captureException($throwable); 34 | 35 | throw $throwable; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CodeExamples/CodeInspector.php: -------------------------------------------------------------------------------- 1 | getFileName()); 15 | 16 | return collect(explode(PHP_EOL, $code)) 17 | ->slice( 18 | $reflection->getStartLine(), 19 | $reflection->getEndLine() - $reflection->getStartLine() - 1 20 | ) 21 | ->pipe(fn ($collection) => $this->removeExternalIndentation($collection)) 22 | ->pipe(fn ($collection) => $this->removeReturnKeyword($collection)) 23 | ->implode("\n"); 24 | } 25 | 26 | /** 27 | * Remove the indentation outside the scope of the current code block. 28 | */ 29 | private function removeExternalIndentation(Collection $lines) 30 | { 31 | $leadingSpacesInFirstLine = $this->numberOfLeadingSpaces($lines->first()); 32 | 33 | return $lines->transform(fn ($line) => preg_replace("/^( {{$leadingSpacesInFirstLine}})/", '', (string) $line)); 34 | } 35 | 36 | private function numberOfLeadingSpaces(string $str) 37 | { 38 | preg_match('/^( +)/', $str, $matches); 39 | 40 | return strlen($matches[1]); 41 | } 42 | 43 | /** 44 | * Remove the return keyword in the first or in the last line of the code block. 45 | */ 46 | private function removeReturnKeyword(Collection $lines) 47 | { 48 | if (str_starts_with((string) $lines->first(), 'return ')) { 49 | return $lines->prepend(substr((string) $lines->shift(), 7)); 50 | } 51 | 52 | if (str_starts_with((string) $lines->last(), 'return ')) { 53 | return $lines->add(substr((string) $lines->pop(), 7)); 54 | } 55 | 56 | return $lines; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/CodeExamples/CodeResultExporter.php: -------------------------------------------------------------------------------- 1 | currentLevel = 1; 18 | 19 | return $this->format->block( 20 | $this->exportIndentation() 21 | . $this->exportValue($snippet) 22 | ); 23 | } 24 | 25 | private function exportValue($value): string 26 | { 27 | if (isset($value[ExampleSnippet::CLASS_NAME])) { 28 | return $this->exportObject($value); 29 | } 30 | return match (gettype($value)) { 31 | 'array' => $this->exportArray($value), 32 | 'integer' => $this->format->integer($value), 33 | 'double', 'float' => $this->format->float($value), 34 | 'string' => $this->format->string($value), 35 | 'boolean' => $this->format->bool($value ? 'true' : 'false'), 36 | 'NULL', 'null' => $this->format->null(), 37 | default => '', 38 | }; 39 | } 40 | 41 | private function exportArray($items): string 42 | { 43 | $result = $this->format->symbol('[').$this->format->line(); 44 | 45 | if ($this->isAssoc($items)) { 46 | $result .= $this->exportAssocArrayItems($items); 47 | } else { 48 | $result .= $this->exportArrayItems($items); 49 | } 50 | 51 | $result .= $this->exportIndentation() 52 | . $this->format->symbol(']'); 53 | 54 | return $result; 55 | } 56 | 57 | public function isAssoc(array $array): bool 58 | { 59 | return array_keys($array) !== range(0, count($array) - 1); 60 | } 61 | 62 | private function exportAssocArrayItems($items): string 63 | { 64 | $result = ''; 65 | 66 | $this->currentLevel += 1; 67 | 68 | foreach ($items as $key => $value) { 69 | $result .= $this->exportIndentation() 70 | . $this->exportValue($key) 71 | . $this->format->space() 72 | . $this->format->symbol('=>') 73 | . $this->format->space() 74 | . $this->exportValue($value) 75 | . $this->format->symbol(',') 76 | . $this->format->line(); 77 | } 78 | 79 | $this->currentLevel -= 1; 80 | 81 | return $result; 82 | } 83 | 84 | private function exportArrayItems($items): string 85 | { 86 | $result = ''; 87 | 88 | $this->currentLevel += 1; 89 | 90 | foreach ($items as $item) { 91 | $result .= $this->exportIndentation() 92 | . $this->exportValue($item) 93 | . $this->format->symbol(',') 94 | . $this->format->line(); 95 | } 96 | 97 | $this->currentLevel -= 1; 98 | 99 | return $result; 100 | } 101 | 102 | private function exportObject($snippet): string 103 | { 104 | $className = $snippet[ExampleSnippet::CLASS_NAME]; 105 | $attributes = $snippet[ExampleSnippet::ATTRIBUTES] ?? []; 106 | 107 | $result = $this->format->className($className) 108 | . $this->format->space() 109 | . $this->format->symbol('{') 110 | . $this->format->line(); 111 | 112 | $this->currentLevel += 1; 113 | 114 | foreach ($attributes as $property => $value) { 115 | $result .= $this->format->indentation($this->currentLevel) 116 | . $this->format->propertyName($property) 117 | . $this->format->symbol(':') 118 | . $this->format->space() 119 | . $this->exportValue($value) 120 | . $this->format->symbol(',') 121 | . $this->format->line(); 122 | } 123 | 124 | $this->currentLevel -= 1; 125 | 126 | $result .= $this->format->indentation($this->currentLevel) 127 | .$this->format->symbol('}'); 128 | 129 | return $result; 130 | } 131 | 132 | private function exportIndentation(): string 133 | { 134 | return $this->format->indentation($this->currentLevel); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/CodeExamples/CodeResultFormat.php: -------------------------------------------------------------------------------- 1 | transformInArray($result); 19 | } 20 | 21 | public static function exportProvidedData(array $data): array 22 | { 23 | return static::export($data); 24 | } 25 | 26 | private function transformInArray($result, int $currentLevel = 0) 27 | { 28 | if ($result instanceof Closure) { 29 | return $this->exportFunction($result); 30 | } 31 | 32 | if (is_object($result)) { 33 | return $this->exportObject($result, $currentLevel); 34 | } 35 | 36 | if (! is_array($result)) { 37 | return $result; 38 | } 39 | 40 | return array_map(fn ($item) => $this->transformInArray($item, $currentLevel), $result); 41 | } 42 | 43 | private function exportFunction($result): array 44 | { 45 | $functionReflection = new ReflectionFunction($result); 46 | 47 | return [ 48 | ExampleSnippet::FUNCTION => ExampleSnippet::ANONYMOUS_FUNCTION, 49 | ExampleSnippet::PARAMETERS => $this->exportParameters($functionReflection->getParameters()), 50 | ExampleSnippet::RETURN_TYPE => $functionReflection->hasReturnType() ? $functionReflection->getReturnType()->getName(): null, 51 | ]; 52 | } 53 | 54 | private function exportParameters(array $parameters): array 55 | { 56 | return collect($parameters) 57 | ->map(fn (ReflectionParameter $parameter) => [ 58 | ExampleSnippet::TYPE => $parameter->hasType() ? $parameter->getType()->getName() : null, 59 | ExampleSnippet::PARAMETER => $parameter->getName(), 60 | ExampleSnippet::OPTIONAL => $parameter->isOptional(), 61 | ExampleSnippet::DEFAULT => $parameter->isOptional() ? $parameter->getDefaultValue() : null, 62 | ]) 63 | ->all(); 64 | } 65 | 66 | private function exportObject(object $result, int $currentLevel): array 67 | { 68 | return [ 69 | ExampleSnippet::CLASS_NAME => $result::class, 70 | ExampleSnippet::ATTRIBUTES => $this->exportAttributes($result, $currentLevel), 71 | ]; 72 | } 73 | 74 | private function exportAttributes(object $result, int $currentLevel) 75 | { 76 | if ($currentLevel >= static::$maxNestedLevel) { 77 | return null; 78 | } 79 | 80 | return $this->transformInArray($this->getObjectAttributes($result), $currentLevel + 1); 81 | } 82 | 83 | private function getObjectAttributes(object $object) 84 | { 85 | if ($object instanceof Enumerable) { 86 | return ['items' => $object->all()]; 87 | } 88 | 89 | if ($object instanceof Arrayable) { 90 | return $object->toArray(); 91 | } 92 | 93 | return get_object_vars($object); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CodeExamples/HtmlResultFormat.php: -------------------------------------------------------------------------------- 1 | {$symbol}"; 10 | } 11 | 12 | public function integer(int $value): string 13 | { 14 | return "{$value}"; 15 | } 16 | 17 | public function float($value): string 18 | { 19 | return "{$value}"; 20 | } 21 | 22 | public function string($value): string 23 | { 24 | return sprintf('"%s"', $value); 25 | } 26 | 27 | public function className($className): string 28 | { 29 | return "{$className}"; 30 | } 31 | 32 | public function propertyName(string $property) 33 | { 34 | return "{$property}"; 35 | } 36 | 37 | public function bool($value): string 38 | { 39 | return "{$value}"; 40 | } 41 | 42 | public function null(): string 43 | { 44 | return 'null'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CodeExamples/PlainCodeResultFormat.php: -------------------------------------------------------------------------------- 1 | '.$code.''); 12 | } 13 | 14 | public function symbol(string $symbol): string 15 | { 16 | return $symbol; 17 | } 18 | 19 | public function integer(int $value): string 20 | { 21 | return $value; 22 | } 23 | 24 | public function float(float $value): string 25 | { 26 | return $value; 27 | } 28 | 29 | public function string(string $value): string 30 | { 31 | return sprintf('"%s"', $value); 32 | } 33 | 34 | public function bool(string $value): string 35 | { 36 | return strtoupper($value); 37 | } 38 | 39 | public function null(): string 40 | { 41 | return 'NULL'; 42 | } 43 | 44 | public function className(string $name): string 45 | { 46 | return $name; 47 | } 48 | 49 | public function propertyName(string $name): string 50 | { 51 | return $name; 52 | } 53 | 54 | public function indentation($level): string 55 | { 56 | return ''; 57 | } 58 | 59 | public function space(): string 60 | { 61 | return ' '; 62 | } 63 | 64 | public function line(): string 65 | { 66 | return ' '; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Console/Commands/ExportDocumentationCommand.php: -------------------------------------------------------------------------------- 1 | getLatestRuns(); 26 | 27 | if ($runs->isEmpty()) { 28 | $this->line(''); 29 | $this->warn('There are no runs available. Please setup `Enlighten` and run the tests first.'); 30 | return; 31 | } 32 | 33 | $selectedRun = $runs->firstWhere('signature', $this->choice( 34 | "Please select the run you'd like to export", 35 | $runs->pluck('signature')->all(), 36 | $runs->first()->signature 37 | )); 38 | 39 | $baseDir = $this->ask('In which directory would you like to export the documentation?', config('enlighten.docs_base_dir')); 40 | 41 | $baseUrl = $this->normalizeBaseUrl($this->ask("What's the base URL for this documentation going to be?", config('enlighten.docs_base_url'))); 42 | 43 | $this->warn("Exporting the documentation for `{$selectedRun->signature}`...\n"); 44 | 45 | $this->exporter->export($selectedRun, $baseDir, $baseUrl); 46 | 47 | $this->info("`{$selectedRun->signature}` run exported!"); 48 | } 49 | 50 | protected function getLatestRuns(): Collection 51 | { 52 | return Run::query() 53 | ->latest() 54 | ->take(5) 55 | ->get(); 56 | } 57 | 58 | private function normalizeBaseUrl($url): string 59 | { 60 | if (Str::startsWith($url, ['http://', 'https://'])) { 61 | return $url; 62 | } 63 | 64 | return '/'.ltrim((string) $url, '/'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Console/Commands/FreshCommand.php: -------------------------------------------------------------------------------- 1 | call('db:wipe', [ 17 | '--database' => 'enlighten', 18 | '--force' => $this->option('force'), 19 | ]); 20 | 21 | $this->call('enlighten:migrate', [ 22 | '--force' => $this->option('force'), 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Console/Commands/GenerateDocumentationCommand.php: -------------------------------------------------------------------------------- 1 | ignoreValidationErrors(); 23 | } 24 | 25 | public function handle(): void 26 | { 27 | $runBuilder = app(RunBuilder::class); 28 | 29 | $runBuilder->reset(); 30 | $runBuilder->save(); 31 | 32 | $this->addCustomBootstrapToGlobalArguments(); 33 | 34 | $this->runTests(); 35 | 36 | $run = $runBuilder->getRun(); 37 | 38 | if ($run->isEmpty()) { 39 | $this->printMissingSetupWarning(); 40 | } else { 41 | $this->printFailedExamples($run); 42 | $this->printDocumentationLink($run); 43 | } 44 | } 45 | 46 | private function addCustomBootstrapToGlobalArguments(): void 47 | { 48 | $_SERVER['argv'] = array_merge( 49 | array_slice($_SERVER['argv'], 0, 2), 50 | ['--bootstrap=vendor/styde/enlighten/src/Tests/bootstrap.php'], 51 | array_slice($_SERVER['argv'], 2) 52 | ); 53 | } 54 | 55 | private function runTests(): void 56 | { 57 | $this->call('test', [ 58 | '--parallel' => $this->option('parallel'), 59 | '--recreate-databases' => $this->option('recreate-databases') 60 | ]); 61 | } 62 | 63 | private function printMissingSetupWarning(): void 64 | { 65 | $this->output->newLine(); 66 | $this->alert('The documentation was not generated'); 67 | $this->output->newLine(); 68 | $this->error('Did you forget to call `$this->setUpEnlighten();` in your tests?'); 69 | $this->warn('Learn more: https://github.com/StydeNet/enlighten#installation'); 70 | } 71 | 72 | private function printFailedExamples(RunContract $run): void 73 | { 74 | $failedExamples = $run->getFailedExamples(); 75 | 76 | if ($failedExamples->isNotEmpty()) { 77 | $this->printFailedExamplesHeader($failedExamples); 78 | $this->printFailedExampleItems($failedExamples); 79 | } 80 | } 81 | 82 | private function printFailedExamplesHeader($examples): void 83 | { 84 | $this->output->newLine(); 85 | $this->error(sprintf( 86 | '⚠️ %s %s failed:', 87 | $examples->count(), 88 | Str::plural('test', $examples->count()) 89 | )); 90 | } 91 | 92 | private function printFailedExampleItems($examples): void 93 | { 94 | $examples->each(function ($example) { 95 | $this->output->newLine(); 96 | $this->line("❌ {$example->getTitle()}:"); 97 | $this->warn($example->getUrl()); 98 | }); 99 | } 100 | 101 | private function printDocumentationLink(RunContract $run): void 102 | { 103 | $this->output->newLine(); 104 | $this->line('⚡ Check your documentation at:'); 105 | $this->info($run->url()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Console/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | publishBuildAndConfigFiles(); 17 | 18 | $this->output->newLine(); 19 | 20 | if ($this->setupEnlightenInTestCase()) { 21 | $this->info('Installation complete!'); 22 | } else { 23 | $this->error('The installer has detected changes in your TestCase class.'); 24 | $this->error('Please setup Enlighten manually with the link below:'); 25 | $this->error('https://github.com/StydeNet/enlighten#manual-setup'); 26 | } 27 | 28 | $this->output->newLine(); 29 | $this->warn('Please remember to create and setup the database for Enlighten and to change the APP_URL env variable if necessary.'); 30 | $this->output->newLine(); 31 | $this->info("After running `php artisan enlighten`, you'll find your documentation by visiting: ".url('/enlighten')); 32 | } 33 | 34 | private function publishBuildAndConfigFiles(): void 35 | { 36 | $this->call('vendor:publish', ['--tag' => 'enlighten']); 37 | } 38 | 39 | private function setupEnlightenInTestCase(): bool 40 | { 41 | $appTestCase = File::get(base_path('tests/TestCase.php')); 42 | $baseTestCase = File::get(__DIR__.'/stubs/BaseTestCase.php.stub'); 43 | 44 | if ($appTestCase != $baseTestCase) { 45 | return false; 46 | } 47 | 48 | $enlightenTestCase = File::get(__DIR__ . '/stubs/EnlightenTestCase.php.stub'); 49 | File::put(base_path('tests/TestCase.php'), $enlightenTestCase); 50 | 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Console/Commands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | call('migrate', [ 18 | '--database' => 'enlighten', 19 | '--realpath' => true, 20 | '--path' => __DIR__ . '/../../../database/migrations', 21 | '--force' => $this->option('force'), 22 | '--pretend' => $this->option('pretend'), 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Console/Commands/stubs/BaseTestCase.php.stub: -------------------------------------------------------------------------------- 1 | setUpEnlighten(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Console/ContentRequest.php: -------------------------------------------------------------------------------- 1 | httpKernel->handle(Request::createFromBase($symfonyRequest)); 21 | 22 | return $response->getContent(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Console/DocumentationExporter.php: -------------------------------------------------------------------------------- 1 | baseDir = rtrim($baseDir, '/'); 28 | $this->staticBaseUrl = rtrim($staticBaseUrl, '/'); 29 | $this->originalBaseUrl = $run->base_url; 30 | 31 | $this->createDirectory('/'); 32 | 33 | $this->exportAssets(); 34 | 35 | $this->exportRunWithAreas($run); 36 | 37 | $run->groups->each(function ($group) { 38 | $this->exportGroupWithExamples($group); 39 | }); 40 | 41 | $this->exportSearchJson($run); 42 | } 43 | 44 | private function exportAssets(): void 45 | { 46 | $this->filesystem->deleteDirectory("{$this->baseDir}/assets"); 47 | 48 | $this->filesystem->copyDirectory(__DIR__.'/../../dist', "{$this->baseDir}/assets"); 49 | } 50 | 51 | private function exportRunWithAreas(Run $run): void 52 | { 53 | $this->createFile('index.html', $this->withContentFrom($run->url)); 54 | 55 | $this->createDirectory('/areas'); 56 | 57 | $run->areas->each(function ($area) use ($run) { 58 | $this->exportArea($run, $area); 59 | }); 60 | } 61 | 62 | private function exportArea(Run $run, Area $area): void 63 | { 64 | $this->createFile("areas/{$area->slug}.html", $this->withContentFrom($run->areaUrl($area->slug))); 65 | } 66 | 67 | private function exportGroupWithExamples(ExampleGroup $group): void 68 | { 69 | $this->createFile("{$group->slug}.html", $this->withContentFrom($group->url)); 70 | 71 | $this->createDirectory($group->slug); 72 | 73 | $group->examples->each(function (Example $example) use ($group) { 74 | $this->exportExample($example->setRelation('group', $group)); 75 | }); 76 | } 77 | 78 | private function exportExample(Example $example): void 79 | { 80 | $this->createFile( 81 | "{$example->group->slug}/{$example->slug}.html", 82 | $this->withContentFrom($example->url) 83 | ); 84 | } 85 | 86 | private function exportSearchJson(Run $run): void 87 | { 88 | $this->createFile( 89 | 'search.json', 90 | json_encode(['items' => $this->getSearchItems($run)], JSON_THROW_ON_ERROR) 91 | ); 92 | } 93 | 94 | private function getSearchItems(Run $run) 95 | { 96 | return $run->groups 97 | ->load('examples') 98 | ->flatMap(fn ($group) => $group->examples->map(fn ($example) => [ 99 | 'section' => "{$group->area_title} / {$group->title}", 100 | 'title' => $example->title, 101 | 'url' => $this->getStaticUrl($example->url), 102 | ])) 103 | ->sortBy('title') 104 | ->values(); 105 | } 106 | 107 | private function createDirectory($path): void 108 | { 109 | if ($this->filesystem->isDirectory("{$this->baseDir}/$path")) { 110 | return; 111 | } 112 | 113 | $this->filesystem->makeDirectory("{$this->baseDir}/$path", 0755); 114 | } 115 | 116 | private function createFile(string $filename, string $contents): void 117 | { 118 | $this->filesystem->put("{$this->baseDir}/{$filename}", $contents); 119 | } 120 | 121 | private function withContentFrom(string $url): string 122 | { 123 | return $this->replaceUrls($this->request->getContent($url)); 124 | } 125 | 126 | private function replaceUrls(string $contents) 127 | { 128 | // Search json path 129 | $contents = preg_replace('@fetch\((.*?)search.json\'\)@', "fetch('{$this->staticBaseUrl}/search.json')", $contents); 130 | 131 | // Assets paths 132 | $contents = str_replace('/vendor/enlighten/', "{$this->staticBaseUrl}/assets/", (string) $contents); 133 | 134 | // Internal links 135 | return preg_replace_callback( 136 | '@'.$this->originalBaseUrl.'([^"]+)?@', 137 | fn ($matches) => $this->getStaticUrl($matches[0]), 138 | $contents 139 | ); 140 | } 141 | 142 | private function getStaticUrl(string $originalUrl): string 143 | { 144 | $result = str_replace($this->originalBaseUrl, $this->staticBaseUrl, $originalUrl); 145 | 146 | if ($result === $this->staticBaseUrl) { 147 | return $result; 148 | } 149 | 150 | return "{$result}.html"; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Contracts/Example.php: -------------------------------------------------------------------------------- 1 | title = $title; 62 | 63 | return $this; 64 | } 65 | 66 | public function setMethodName(string $methodName): ExampleBuilder 67 | { 68 | $this->methodName = $methodName; 69 | 70 | return $this; 71 | } 72 | 73 | public function setProvidedData(array $data = null): ExampleBuilder 74 | { 75 | $this->providedData = $data; 76 | 77 | return $this; 78 | } 79 | 80 | public function setDataName($name = null): ExampleBuilder 81 | { 82 | $this->dataName = $name; 83 | 84 | return $this; 85 | } 86 | 87 | public function setOrderNum(int $order_num): ExampleBuilder 88 | { 89 | $this->order_num = $order_num; 90 | 91 | return $this; 92 | } 93 | 94 | public function setDescription(?string $description): ExampleBuilder 95 | { 96 | $this->description = $description; 97 | 98 | return $this; 99 | } 100 | 101 | public function setSlug(string $slug): ExampleBuilder 102 | { 103 | $this->slug = $slug; 104 | 105 | return $this; 106 | } 107 | 108 | public function setStatus(string $testStatus, string $status): void 109 | { 110 | $this->testStatus = $testStatus; 111 | $this->status = $status; 112 | } 113 | 114 | public function setLine(int $line): ExampleBuilder 115 | { 116 | $this->line = $line; 117 | 118 | return $this; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Drivers/BaseExampleGroupBuilder.php: -------------------------------------------------------------------------------- 1 | area = $area; 42 | 43 | return $this; 44 | } 45 | 46 | public function setClassName(string $className): ExampleGroupBuilder 47 | { 48 | $this->className = $className; 49 | 50 | return $this; 51 | } 52 | 53 | public function is(string $name): bool 54 | { 55 | return $this->className === $name; 56 | } 57 | 58 | public function setOrderNum(int $orderNum): ExampleGroupBuilder 59 | { 60 | $this->orderNum = $orderNum; 61 | 62 | return $this; 63 | } 64 | 65 | public function setDescription(?string $description): ExampleGroupBuilder 66 | { 67 | $this->description = $description; 68 | 69 | return $this; 70 | } 71 | 72 | public function setSlug(string $slug): ExampleGroupBuilder 73 | { 74 | $this->slug = $slug; 75 | 76 | return $this; 77 | } 78 | 79 | public function setTitle(string $title): ExampleGroupBuilder 80 | { 81 | $this->title = $title; 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Drivers/DatabaseExampleBuilder.php: -------------------------------------------------------------------------------- 1 | currentRequests = new Collection; 35 | } 36 | 37 | public function addRequest(RequestInfo $request): void 38 | { 39 | $this->save(); 40 | 41 | $this->currentRequests->push($this->example->requests()->create([ 42 | 'example_id' => $this->example->id, 43 | 'request_headers' => $request->getHeaders(), 44 | 'request_method' => $request->getMethod(), 45 | 'request_path' => $request->getPath(), 46 | 'request_query_parameters' => $request->getQueryParameters(), 47 | 'request_input' => $request->getInput(), 48 | 'request_files' => $request->getFiles(), 49 | ])); 50 | } 51 | 52 | public function setResponse(ResponseInfo $response, bool $followsRedirect, RouteInfo $routeInfo, array $session): void 53 | { 54 | $this->save(); 55 | 56 | $this->currentRequests->pop()->update([ 57 | // Route 58 | 'route' => $routeInfo->getUri(), 59 | 'route_parameters' => $routeInfo->getParameters(), 60 | // Response 61 | 'response_status' => $response->getStatusCode(), 62 | 'follows_redirect' => $followsRedirect, 63 | 'response_headers' => $response->getHeaders(), 64 | 'response_body' => $response->getContent(), 65 | 'response_template' => $response->getTemplate(), 66 | // Session 67 | 'session_data' => $session, 68 | ]); 69 | } 70 | 71 | public function setException(ExceptionInfo $exception): void 72 | { 73 | $this->example->exception->fill([ 74 | 'class_name' => $exception->getClassName(), 75 | 'code' => $exception->getCode(), 76 | 'message' => $exception->getMessage(), 77 | 'file' => $exception->getFile(), 78 | 'line' => $exception->getLine(), 79 | 'trace' => $exception->getTrace(), 80 | 'extra' => $exception->getData(), 81 | ])->save(); 82 | } 83 | 84 | public function addQuery(QueryExecuted $queryExecuted): void 85 | { 86 | $this->save(); 87 | 88 | $this->example->queries()->create([ 89 | 'sql' => $queryExecuted->sql, 90 | 'bindings' => $queryExecuted->bindings, 91 | 'time' => $queryExecuted->time, 92 | 'request_id' => optional($this->currentRequests->last())->id, 93 | 'snippet_id' => optional($this->currentSnippet)->id, 94 | ]); 95 | } 96 | 97 | public function addSnippet($key, string $code): void 98 | { 99 | $this->save(); 100 | 101 | $this->currentSnippet = $this->example->snippets()->create([ 102 | 'key' => $key, 103 | 'code' => $code, 104 | ]); 105 | } 106 | 107 | public function setSnippetResult($result): void 108 | { 109 | $this->currentSnippet->update(['result' => $result]); 110 | 111 | $this->currentSnippet = null; 112 | } 113 | 114 | public function build(): ExampleContract 115 | { 116 | $this->save(); 117 | 118 | $this->example->update([ 119 | 'test_status' => $this->testStatus, 120 | 'status' => $this->status, 121 | ]); 122 | 123 | return $this->example; 124 | } 125 | 126 | private function save(): void 127 | { 128 | if ($this->example != null) { 129 | return; 130 | } 131 | 132 | $group = $this->exampleGroupBuilder->save(); 133 | 134 | $this->example = Example::create([ 135 | 'group_id' => $group->id, 136 | 'method_name' => $this->methodName, 137 | 'slug' => $this->slug, 138 | 'title' => $this->title, 139 | 'data_name' => $this->dataName, 140 | 'provided_data' => $this->providedData, 141 | 'description' => $this->description, 142 | 'order_num' => $this->order_num, 143 | 'line' => $this->line, 144 | 'test_status' => Status::UNKNOWN, 145 | 'status' => Status::UNKNOWN, 146 | ]); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Drivers/DatabaseExampleGroupBuilder.php: -------------------------------------------------------------------------------- 1 | exampleGroup !== null) { 22 | return $this->exampleGroup; 23 | } 24 | 25 | $run = $this->runBuilder->save(); 26 | 27 | $this->exampleGroup = ExampleGroup::updateOrCreate([ 28 | 'run_id' => $run->id, 29 | 'class_name' => $this->className, 30 | 'title' => $this->title, 31 | 'description' => $this->description, 32 | 'area' => $this->area, 33 | 'slug' => $this->slug, 34 | 'order_num' => $this->orderNum, 35 | ]); 36 | 37 | return $this->exampleGroup; 38 | } 39 | 40 | public function newExample(): ExampleBuilder 41 | { 42 | return new DatabaseExampleBuilder($this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Drivers/DatabaseRunBuilder.php: -------------------------------------------------------------------------------- 1 | initRun(); 31 | 32 | $this->run->groups()->delete(); 33 | } 34 | 35 | public function save(): RunContract 36 | { 37 | $this->initRun(); 38 | 39 | $this->run->save(); 40 | 41 | return $this->run; 42 | } 43 | 44 | public function getRun(): RunContract 45 | { 46 | $this->initRun(); 47 | 48 | return $this->run->fresh(); 49 | } 50 | 51 | protected function initRun() 52 | { 53 | if ($this->run !== null) { 54 | return; 55 | } 56 | 57 | $this->run = Run::firstOrNew([ 58 | 'branch' => VersionControl::currentBranch(), 59 | 'head' => VersionControl::head(), 60 | 'modified' => VersionControl::modified(), 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Enlighten.php: -------------------------------------------------------------------------------- 1 | createSnippet($callback, $key); 44 | } catch (BindingResolutionException) { 45 | throw new LaravelNotPresent; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ExampleProfile.php: -------------------------------------------------------------------------------- 1 | ignore = $config['ignore']; 18 | } 19 | 20 | public function shouldIgnore(string $className, string $methodName, ?array $options): bool 21 | { 22 | // If the test has been explicitly ignored via the 23 | // annotation options we need to ignore the test. 24 | if (Arr::get($options, 'ignore', false)) { 25 | return true; 26 | } 27 | 28 | // If the test has been explicitly included via the 29 | // annotation options we need to include the test. 30 | if (Arr::get($options, 'include', false)) { 31 | return false; 32 | } 33 | 34 | // Otherwise check the patterns we've got from the 35 | // config to check if the test should be ignored. 36 | if (Str::is($this->ignore, $className)) { 37 | return true; 38 | } 39 | 40 | return Str::is($this->ignore, $methodName); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ExceptionInfo.php: -------------------------------------------------------------------------------- 1 | exception::class; 22 | } 23 | 24 | public function getCode(): int 25 | { 26 | return $this->exception->getCode(); 27 | } 28 | 29 | public function getMessage(): string 30 | { 31 | return $this->exception->getMessage(); 32 | } 33 | 34 | public function getFile(): string 35 | { 36 | return $this->exception->getFile(); 37 | } 38 | 39 | public function getLine(): int 40 | { 41 | return $this->exception->getLine(); 42 | } 43 | 44 | public function getTrace(): array 45 | { 46 | return $this->exception->getTrace(); 47 | } 48 | 49 | public function getData(): array 50 | { 51 | if ($this->exception instanceof ValidationException) { 52 | return [ 53 | 'errors' => $this->exception->errors(), 54 | ]; 55 | } 56 | 57 | return []; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidDriverException.php: -------------------------------------------------------------------------------- 1 | with('stats')->latest()->get(); 12 | 13 | if ($runs->isEmpty()) { 14 | return redirect(route('enlighten.intro')); 15 | } 16 | 17 | return view('enlighten::run.index', ['runs' => $runs]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/SearchController.php: -------------------------------------------------------------------------------- 1 | getExamples($run, $request->query('search')); 14 | 15 | return view('enlighten::search.results', [ 16 | 'examples' => $examples, 17 | 'run' => $run 18 | ]); 19 | } 20 | 21 | private function getExamples($run, $search) 22 | { 23 | return Example::query() 24 | ->with('group') 25 | ->whereHas('group.run', function ($q) use ($run) { 26 | $q->where('id', $run->id); 27 | }) 28 | ->where('title', 'like', "%$search%") 29 | ->limit(5) 30 | ->get(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Controllers/ShowAreaController.php: -------------------------------------------------------------------------------- 1 | getArea($run, $areaSlug); 20 | 21 | $action = $area->view; 22 | 23 | if (! in_array($action, ['features', 'modules', 'endpoints'])) { 24 | $action = 'features'; 25 | } 26 | 27 | return $this->$action($run, $area); 28 | } 29 | 30 | private function modules(Run $run, Area $area) 31 | { 32 | return view('enlighten::area.modules', [ 33 | 'area' => $area, 34 | 'modules' => $this->wrapByModule($this->getGroups($run, $area)->load('stats')), 35 | ]); 36 | } 37 | 38 | private function features(Run $run, Area $area) 39 | { 40 | $groups = $this->getGroups($run, $area) 41 | ->load([ 42 | 'examples' => function ($q) { 43 | $q->withCount('queries'); 44 | }, 45 | 'examples.group', 46 | 'examples.requests', 47 | 'examples.exception' 48 | ]); 49 | 50 | return view('enlighten::area.features', [ 51 | 'area' => $area, 52 | 'showQueries' => Settings::show(Section::QUERIES), 53 | 'groups' => $groups, 54 | ]); 55 | } 56 | 57 | private function endpoints(Run $run, Area $area) 58 | { 59 | $requests = ExampleRequest::query() 60 | ->select('id', 'example_id', 'request_method', 'request_path') 61 | ->addSelect('route', 'response_status', 'response_headers') 62 | ->with([ 63 | 'example:id,group_id,title,slug,status,order_num', 64 | 'example.group:id,slug,run_id', 65 | ]) 66 | ->when($area->isNotDefault(), function ($q) use ($area) { 67 | $q->whereHas('example.group', function ($q) use ($area) { 68 | $q->where('area', $area->slug); 69 | }); 70 | }) 71 | ->whereHas('example.group.run', function ($q) use ($run) { 72 | $q->where('id', $run->id); 73 | }) 74 | ->where('follows_redirect', false) 75 | ->get(); 76 | 77 | $endpoints = $requests 78 | ->groupBy('signature') 79 | ->map(fn ($requests) => new Endpoint( 80 | $requests->first()->request_method, 81 | $requests->first()->route_or_path, 82 | $requests->unique(fn ($response) => $response->signature.$response->example->slug)->sortBy('example.order') 83 | )) 84 | ->sortBy('method_index'); 85 | 86 | return view('enlighten::area.endpoints', [ 87 | 'area' => $area, 88 | 'modules' => $this->wrapByModule($endpoints), 89 | ]); 90 | } 91 | 92 | private function getArea(Run $run, string $areaSlug = null): Area 93 | { 94 | if (empty($areaSlug)) { 95 | return $this->defaultArea(); 96 | } 97 | 98 | return $run->areas->firstWhere('slug', $areaSlug) ?: $this->defaultArea(); 99 | } 100 | 101 | private function defaultArea(): Area 102 | { 103 | return new Area('', trans('enlighten::messages.all_areas'), config('enlighten.area_view', 'features')); 104 | } 105 | 106 | private function getGroups(Run $run, Area $area): Collection 107 | { 108 | // We always want to get the collection with all the groups 109 | // because we use them to build the menu. So by filtering 110 | // at a collection level we're actually saving a query. 111 | return $run->groups 112 | ->when($area->isNotDefault(), fn ($collection) => $collection->where('area', $area->slug)) 113 | ->sortBy('order'); 114 | } 115 | 116 | private function wrapByModule(Collection $groups): ModuleCollection 117 | { 118 | return Module::all()->wrapGroups($groups)->whereHasGroups(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Http/Controllers/ShowExampleController.php: -------------------------------------------------------------------------------- 1 | findGroup($groupSlug); 14 | 15 | return view('enlighten::example.show', [ 16 | 'example' => $this->getExampleWithRelations($group, $exampleSlug) 17 | ]); 18 | } 19 | 20 | private function getExampleWithRelations(Model $group, string $exampleSlug) 21 | { 22 | return Example::query() 23 | ->with('requests', 'requests.queries', 'snippets', 'exception', 'queries') 24 | ->where([ 25 | 'group_id' => $group->id, 26 | 'slug' => $exampleSlug, 27 | ]) 28 | ->firstOrFail() 29 | ->setRelation('group', $group); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Controllers/ShowExampleGroupController.php: -------------------------------------------------------------------------------- 1 | findGroup($groupSlug); 14 | 15 | $examples = $group->examples() 16 | ->with(['group', 'requests', 'exception']) 17 | ->withCount('queries') 18 | ->get(); 19 | 20 | return view('enlighten::group.show', [ 21 | 'group' => $group, 22 | 'title' => $group->title, 23 | 'examples' => $examples, 24 | 'showQueries' => Settings::show(Section::QUERIES), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Controllers/WelcomeController.php: -------------------------------------------------------------------------------- 1 | $this->getIntroContent() 14 | ]); 15 | } 16 | 17 | private function getIntroContent() 18 | { 19 | if (file_exists(base_path('ENLIGHTEN.md'))) { 20 | return $this->parseMarkdownFile(base_path('ENLIGHTEN.md')); 21 | } 22 | 23 | return $this->fixImagesPath($this->parseMarkdownFile(__DIR__ . '/../../../README.md')); 24 | } 25 | 26 | private function parseMarkdownFile(string $filePath): HtmlString 27 | { 28 | return Markdown::parse(file_get_contents($filePath)); 29 | } 30 | 31 | private function fixImagesPath(string $content) 32 | { 33 | $baseImagePath = asset('vendor/enlighten/img') . '/'; 34 | 35 | return str_replace('middleware('web')->group(function () { 13 | Route::get('intro', WelcomeController::class) 14 | ->name('enlighten.intro'); 15 | 16 | Route::get('/', ListRunsController::class) 17 | ->name('enlighten.run.index'); 18 | 19 | Route::get('run/{run}/areas/{area?}', ShowAreaController::class) 20 | ->name('enlighten.area.show'); 21 | 22 | Route::get('run/{run}/{group:slug}', ShowExampleGroupController::class) 23 | ->name('enlighten.group.show'); 24 | 25 | Route::get('run/{run}/{group:slug}/{example:slug}', ShowExampleController::class) 26 | ->name('enlighten.method.show'); 27 | }); 28 | 29 | Route::prefix('enlighten/api') 30 | ->middleware(SubstituteBindings::class) 31 | ->group(function () { 32 | Route::get('run/{run}/search', SearchController::class)->name('enlighten.api.search'); 33 | }); 34 | -------------------------------------------------------------------------------- /src/HttpExamples/HttpExampleCreator.php: -------------------------------------------------------------------------------- 1 | exampleCreator->getCurrentExample(); 36 | 37 | if (is_null($testExample)) { 38 | return; 39 | } 40 | 41 | $testExample->addRequest( 42 | $this->requestInspector->getDataFrom($request) 43 | ); 44 | } 45 | 46 | public function saveHttpResponseData(Request $request, Response $response): void 47 | { 48 | $testExample = $this->exampleCreator->getCurrentExample(); 49 | 50 | if (is_null($testExample)) { 51 | return; 52 | } 53 | 54 | $testExample->setResponse( 55 | $this->responseInspector->getDataFrom($this->normalizeResponse($response)), 56 | static::$followsRedirect, 57 | $this->routeInspector->getInfoFrom($request->route()), 58 | $this->sessionInspector->getData() 59 | ); 60 | } 61 | 62 | private function normalizeResponse(Response $response) 63 | { 64 | if ($response instanceof TestResponse) { 65 | $response = $response->baseResponse; 66 | } 67 | 68 | return $response; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/HttpExamples/HttpExampleCreatorMiddleware.php: -------------------------------------------------------------------------------- 1 | httpExampleCreator->createHttpExample($request); 25 | 26 | $response = $next($request); 27 | 28 | $this->httpExampleCreator->saveHttpResponseData($request, $response); 29 | 30 | return $response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/HttpExamples/RequestInfo.php: -------------------------------------------------------------------------------- 1 | method; 14 | } 15 | 16 | public function getPath(): string 17 | { 18 | return $this->path; 19 | } 20 | 21 | public function getHeaders(): array 22 | { 23 | return $this->headers; 24 | } 25 | 26 | public function getQueryParameters(): array 27 | { 28 | return $this->queryParameters; 29 | } 30 | 31 | public function getInput(): array 32 | { 33 | return $this->input; 34 | } 35 | 36 | public function getFiles(): array 37 | { 38 | return $this->files; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/HttpExamples/RequestInspector.php: -------------------------------------------------------------------------------- 1 | method(), 14 | $request->path(), 15 | $request->headers->all(), 16 | $request->query(), 17 | $request->post(), 18 | $this->getFilesInfo($request->allFiles()), 19 | ); 20 | } 21 | 22 | public function getFilesInfo(array $files): array 23 | { 24 | return collect($files) 25 | ->map(fn (UploadedFile $file) => [ 26 | 'name' => $file->getClientOriginalName(), 27 | 'type' => $file->getMimeType(), 28 | 'size' => intdiv($file->getSize(), 1024), 29 | ]) 30 | ->all(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/HttpExamples/ResponseInfo.php: -------------------------------------------------------------------------------- 1 | statusCode; 14 | } 15 | 16 | public function getHeaders(): array 17 | { 18 | return $this->headers; 19 | } 20 | 21 | public function getContent(): string 22 | { 23 | return $this->content; 24 | } 25 | 26 | public function getTemplate(): ?string 27 | { 28 | return $this->template; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HttpExamples/ResponseInspector.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), 15 | $response->headers->all(), 16 | $response->getContent(), 17 | $this->getTemplate($response) 18 | ); 19 | } 20 | 21 | protected function getTemplate(Response $response): ?string 22 | { 23 | if (isset($response->original) && $response->original instanceof View) { 24 | return File::get($response->original->getPath()); 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/HttpExamples/RouteInfo.php: -------------------------------------------------------------------------------- 1 | uri = null; 21 | $this->parameters = null; 22 | } else { 23 | $this->uri = $uri; 24 | $this->parameters = $parameters; 25 | } 26 | } 27 | 28 | public function getUri(): ?string 29 | { 30 | return $this->uri; 31 | } 32 | 33 | public function getParameters(): ?array 34 | { 35 | return $this->parameters; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/HttpExamples/RouteInspector.php: -------------------------------------------------------------------------------- 1 | uri(), $this->getParameters($route)); 16 | } 17 | 18 | /** 19 | * Get all the route parameters as keys and the parameter-where conditions as values. 20 | * 21 | * @return array 22 | */ 23 | protected function getParameters(Route $route): array 24 | { 25 | return collect($route->parameterNames()) 26 | ->mapWithKeys(fn ($parameter) => [$parameter => '*']) 27 | ->merge( 28 | array_intersect_key($route->wheres, $route->originalParameters()) 29 | ) 30 | ->map(fn ($pattern, $name) => [ 31 | 'name' => $name, 32 | 'pattern' => $pattern, 33 | 'optional' => $this->isParameterOptional($route, $name), 34 | ]) 35 | ->values() 36 | ->all(); 37 | } 38 | 39 | protected function isParameterOptional(Route $route, $parameter): bool 40 | { 41 | return (bool) preg_match("/{{$parameter}\?}/", $route->uri()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/HttpExamples/SessionInspector.php: -------------------------------------------------------------------------------- 1 | session->all(); 16 | 17 | // Wrap the errors array in a collection so it can be 18 | // exported by calling the toArray method since the 19 | // error bags implement the Arrayable interface. 20 | if (! empty($session['errors'])) { 21 | $session['errors'] = collect($session['errors']->getBags()); 22 | } 23 | 24 | return collect($session)->toArray(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/Area.php: -------------------------------------------------------------------------------- 1 | map(fn ($data) => new static( 21 | $data['slug'], 22 | $data['name'] ?? null, 23 | $data['view'] ?? $defaultView, 24 | )); 25 | } 26 | 27 | public static function get($areas): Collection 28 | { 29 | return collect($areas) 30 | ->map(function ($slug) { 31 | $config = static::getConfigFor($slug); 32 | 33 | return new static( 34 | $slug, 35 | $config['name'] ?? null, 36 | $config['view'] ?? config('enlighten.area_view') 37 | ); 38 | }) 39 | ->sortBy('name') 40 | ->values(); 41 | } 42 | 43 | public static function getConfigFor(string $areaSlug): array 44 | { 45 | return collect(config('enlighten.areas'))->firstWhere('slug', $areaSlug) ?: []; 46 | } 47 | 48 | public function __construct(string $slug, string $name = null, $view = 'features') 49 | { 50 | $this->setAttributes([ 51 | 'name' => $name ?: ucfirst(str_replace('-', ' ', $slug)), 52 | 'slug' => $slug, 53 | 'view' => $view, 54 | ]); 55 | } 56 | 57 | public function isNotDefault(): bool 58 | { 59 | return ! empty($this->slug); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Models/Concerns/GetStats.php: -------------------------------------------------------------------------------- 1 | stats 14 | ->where('status', Status::SUCCESS) 15 | ->sum('count'); 16 | } 17 | 18 | public function getTestsCount(): int 19 | { 20 | return $this->stats->sum('count'); 21 | } 22 | 23 | // Statusable 24 | public function getStatus(): string 25 | { 26 | if ($this->getPassingTestsCount() === $this->getTestsCount()) { 27 | return Status::SUCCESS; 28 | } 29 | 30 | if ($this->stats->firstWhere('status', Status::FAILURE)) { 31 | return Status::FAILURE; 32 | } 33 | 34 | return Status::WARNING; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Concerns/GetsStatsFromGroups.php: -------------------------------------------------------------------------------- 1 | groups->pluck('passing_tests_count')->sum(); 12 | } 13 | 14 | public function getTestsCount(): int 15 | { 16 | return $this->groups->pluck('tests_count')->sum(); 17 | } 18 | 19 | public function getStatus(): string 20 | { 21 | if ($this->getPassingTestsCount() === $this->getTestsCount()) { 22 | return Status::SUCCESS; 23 | } 24 | 25 | if ($this->groups->firstWhere('status', Status::FAILURE)) { 26 | return Status::FAILURE; 27 | } 28 | 29 | return Status::WARNING; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Concerns/ReadsDynamicAttributes.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 17 | } 18 | 19 | public function __isset($name) 20 | { 21 | if (method_exists($this, 'get'.Str::studly($name))) { 22 | return true; 23 | } 24 | 25 | return array_key_exists($name, $this->attributes); 26 | } 27 | 28 | public function __get($name) 29 | { 30 | if (method_exists($this, $method = 'get'.Str::studly($name))) { 31 | return $this->$method(); 32 | } 33 | 34 | return $this->attributes[$name] ?? null; 35 | } 36 | 37 | public function toArray(): array 38 | { 39 | return $this->attributes; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Models/Endpoint.php: -------------------------------------------------------------------------------- 1 | setAttributes([ 17 | 'method' => $method, 18 | 'route' => $route, 19 | 'requests' => $requests ?: collect(), 20 | ]); 21 | } 22 | 23 | public function matches(Module $module): bool 24 | { 25 | return Str::is($module->routes, $this->route); 26 | } 27 | 28 | public function getSignature() 29 | { 30 | return "{$this->method} {$this->route}"; 31 | } 32 | 33 | public function getTitle() 34 | { 35 | return $this->getMainRequest()->example->group->title; 36 | } 37 | 38 | public function getMainRequest() 39 | { 40 | return $this->requests->first(); 41 | } 42 | 43 | public function getAdditionalRequests() 44 | { 45 | return $this->requests->slice(1); 46 | } 47 | 48 | public function getMethodIndex() 49 | { 50 | return array_search($this->method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); 51 | } 52 | 53 | public function getStats() 54 | { 55 | return $this->requests 56 | ->groupBy('example.status') 57 | ->map(fn ($endpoints, $status) => [ 58 | 'status' => $status, 59 | 'count' => count($endpoints), 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Models/Example.php: -------------------------------------------------------------------------------- 1 | 'array', 19 | 'count' => 'int', 20 | 'order_num' => 'int', 21 | ]; 22 | 23 | // Relationships 24 | 25 | public function group() 26 | { 27 | return $this->belongsTo(ExampleGroup::class); 28 | } 29 | 30 | public function requests() 31 | { 32 | return $this->hasMany(ExampleRequest::class); 33 | } 34 | 35 | public function exception() 36 | { 37 | return $this->hasOne(ExampleException::class)->withDefault(); 38 | } 39 | 40 | public function queries() 41 | { 42 | return $this->hasMany(ExampleQuery::class); 43 | } 44 | 45 | public function snippets() 46 | { 47 | return $this->hasMany(ExampleSnippet::class); 48 | } 49 | 50 | // Accessors 51 | 52 | public function getTitleAttribute($title) 53 | { 54 | if (is_null($this->data_name)) { 55 | return $title; 56 | } 57 | 58 | if (is_numeric($this->data_name)) { 59 | return sprintf('%s (dataset #%s)', $title, $this->data_name); 60 | } 61 | 62 | return sprintf('%s (%s)', $title, $this->data_name); 63 | } 64 | 65 | public function getSignatureAttribute() 66 | { 67 | return $this->group->class_name.'::'.$this->method_name; 68 | } 69 | 70 | public function getHasExceptionAttribute() 71 | { 72 | return $this->exception->exists; 73 | } 74 | 75 | public function getFileLinkAttribute() 76 | { 77 | return FileLink::get(str_replace('\\', '/', (string) $this->group->class_name).'.php', $this->line); 78 | } 79 | 80 | public function getIsHttpAttribute() 81 | { 82 | return $this->requests->isNotEmpty(); 83 | } 84 | 85 | public function getUrlAttribute() 86 | { 87 | return route('enlighten.method.show', [ 88 | $this->group->run_id, 89 | $this->group->slug, 90 | $this->slug, 91 | ]); 92 | } 93 | 94 | public function getOrphanQueriesAttribute() 95 | { 96 | return $this->queries->where('request_id', null); 97 | } 98 | 99 | public function getOrderAttribute() 100 | { 101 | return [$this->order_num, $this->id]; 102 | } 103 | 104 | // Contract 105 | 106 | public function getSignature(): string 107 | { 108 | return $this->signature; 109 | } 110 | 111 | public function getTitle(): string 112 | { 113 | return "{$this->group->title} - {$this->title}"; 114 | } 115 | 116 | public function getStatus(): string 117 | { 118 | return $this->attributes['status'] ?? Status::UNKNOWN; 119 | } 120 | 121 | public function getUrl(): string 122 | { 123 | return $this->url; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Models/ExampleException.php: -------------------------------------------------------------------------------- 1 | 'int', 18 | 'trace' => 'array', 19 | 'extra' => 'array', 20 | ]; 21 | 22 | public function getFileLinkAttribute() 23 | { 24 | if (empty($this->file)) { 25 | return ''; 26 | } 27 | 28 | return FileLink::get($this->file); 29 | } 30 | 31 | public function getValidationErrorsAttribute() 32 | { 33 | return $this->extra['errors'] ?? []; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/ExampleGroup.php: -------------------------------------------------------------------------------- 1 | 'int', 22 | ]; 23 | 24 | // Relationships 25 | public function run() 26 | { 27 | return $this->belongsTo(Run::class); 28 | } 29 | 30 | public function examples() 31 | { 32 | return $this->hasMany(Example::class, 'group_id') 33 | ->orderBy('order_num') 34 | ->orderBy('id'); 35 | } 36 | 37 | public function stats() 38 | { 39 | return $this->hasMany(Example::class, 'group_id', 'id') 40 | ->selectRaw('DISTINCT(status), COUNT(id) as count, group_id') 41 | ->groupBy('status', 'group_id'); 42 | } 43 | 44 | // Helpers 45 | public function matches(Module $module): bool 46 | { 47 | return Str::is($module->classes, $this->class_name); 48 | } 49 | 50 | // Scopes 51 | public function scopeFilterByArea($query, Area $area) : Builder 52 | { 53 | return $query->where('area', $area->slug); 54 | } 55 | 56 | // Accessors 57 | 58 | public function getAreaTitleAttribute() 59 | { 60 | return config('enlighten.areas.'.$this->area) ?: ucwords((string) $this->area); 61 | } 62 | 63 | public function getPassingTestsCountAttribute() 64 | { 65 | return $this->getPassingTestsCount(); 66 | } 67 | 68 | public function getTestsCountAttribute() 69 | { 70 | return $this->getTestsCount(); 71 | } 72 | 73 | public function getStatusAttribute(): string 74 | { 75 | return $this->getStatus(); 76 | } 77 | 78 | public function getUrlAttribute() 79 | { 80 | return route('enlighten.group.show', [ 81 | 'run' => $this->run_id, 82 | 'group' => $this->slug, 83 | ]); 84 | } 85 | 86 | public function getOrderAttribute() 87 | { 88 | return [$this->order_num, $this->title]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Models/ExampleQuery.php: -------------------------------------------------------------------------------- 1 | 'array', 17 | 'request_id' => 'int', 18 | 'snippet_id' => 'int', 19 | ]; 20 | 21 | public function request() 22 | { 23 | return $this->belongsTo(ExampleRequest::class); 24 | } 25 | 26 | public function snippet() 27 | { 28 | return $this->belongsTo(ExampleSnippet::class); 29 | } 30 | 31 | // Accessors 32 | 33 | public function getContextAttribute() 34 | { 35 | if ($this->request_id !== null) { 36 | return 'request'; 37 | } 38 | 39 | if ($this->snippet_id !== null) { 40 | return 'snippet'; 41 | } 42 | 43 | return 'test'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/ExampleSnippet.php: -------------------------------------------------------------------------------- 1 | 'array' 30 | ]; 31 | 32 | public function getResultCodeAttribute() 33 | { 34 | return app(CodeResultExporter::class)->export($this->result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Module.php: -------------------------------------------------------------------------------- 1 | map(fn ($item) => new static($item['name'], $item['classes'] ?? [], $item['routes'] ?? [])); 17 | } 18 | 19 | public function __construct(string $name, array $classes = [], array $routes = []) 20 | { 21 | $this->setAttributes([ 22 | 'name' => $name, 23 | 'classes' => $classes, 24 | 'routes' => $routes, 25 | ]); 26 | } 27 | 28 | public function addGroups(Collection $groups): self 29 | { 30 | $this->attributes['groups'] = $groups; 31 | 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/ModuleCollection.php: -------------------------------------------------------------------------------- 1 | firstWhere('name', $name); 12 | } 13 | 14 | public function wrapGroups(Collection $groups) : self 15 | { 16 | return $this 17 | ->each(function ($module) use (&$groups) { 18 | [$matches, $groups] = $groups->partition(fn (Wrappable $group) => $group->matches($module)); 19 | 20 | $module->addGroups($matches); 21 | }) 22 | ->wrapRemainingGroups($groups); 23 | } 24 | 25 | private function wrapRemainingGroups(Collection $groups): self 26 | { 27 | if ($groups->isEmpty()) { 28 | return $this; 29 | } 30 | 31 | $module = new Module(config('enlighten.default_module', 'Other Modules')); 32 | 33 | return $this->add($module->addGroups($groups)); 34 | } 35 | 36 | public function whereHasGroups(): self 37 | { 38 | return $this->filter(fn ($module) => $module->groups->isNotEmpty()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/ReplacesValues.php: -------------------------------------------------------------------------------- 1 | decodeValues($originalValues); 17 | 18 | if (! is_array($decodedValues)) { 19 | return $originalValues; 20 | } 21 | 22 | return collect($decodedValues) 23 | ->merge(array_intersect_key($config['overwrite'] ?? [], $decodedValues)) 24 | ->diffKeys(array_flip($config['hide'] ?? [])) 25 | ->all(); 26 | } 27 | 28 | private function decodeValues($originalValues) 29 | { 30 | if (! is_string($originalValues)) { 31 | return $originalValues; 32 | } 33 | 34 | return json_decode($originalValues, JSON_OBJECT_AS_ARRAY); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Run.php: -------------------------------------------------------------------------------- 1 | hasMany(ExampleGroup::class); 25 | } 26 | 27 | public function examples() 28 | { 29 | return $this->hasManyThrough(Example::class, ExampleGroup::class, 'run_id', 'group_id'); 30 | } 31 | 32 | public function findGroup(string $slug) 33 | { 34 | return $this->groups() 35 | ->where('slug', $slug) 36 | ->firstOrFail(); 37 | } 38 | 39 | public function stats() 40 | { 41 | return $this->hasManyThrough(Example::class, ExampleGroup::class, 'run_id', 'group_id') 42 | ->selectRaw(' 43 | DISTINCT(status), 44 | COUNT(enlighten_examples.id) as count 45 | ') 46 | ->groupBy('status', 'run_id'); 47 | } 48 | 49 | // Run Contract 50 | 51 | public function isEmpty(): bool 52 | { 53 | return $this->groups()->count() == 0; 54 | } 55 | 56 | public function getFailedExamples(): SupportCollection 57 | { 58 | return $this->examples()->where('status', '!=', Status::SUCCESS)->get(); 59 | } 60 | 61 | public function url(): string 62 | { 63 | return $this->getUrlAttribute(); 64 | } 65 | 66 | // Accessors 67 | 68 | public function getAreasAttribute() 69 | { 70 | $areas = $this->groups->pluck('area')->unique(); 71 | 72 | return Area::get($areas); 73 | } 74 | 75 | public function getSignatureAttribute($value) 76 | { 77 | if ($this->modified) { 78 | return "{$this->branch} * {$this->head}"; 79 | } 80 | 81 | return "{$this->branch} {$this->head}"; 82 | } 83 | 84 | public function getUrlAttribute() 85 | { 86 | return route('enlighten.area.show', $this); 87 | } 88 | 89 | public function getBaseUrlAttribute() 90 | { 91 | return url("enlighten/run/{$this->id}"); 92 | } 93 | 94 | public function areaUrl(string $area) 95 | { 96 | return route('enlighten.area.show', [$this, $area]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Models/Statable.php: -------------------------------------------------------------------------------- 1 | app->environment('production') && ! $this->app->runningInConsole()) { 33 | return; 34 | } 35 | 36 | $this->mergeConfigFrom($this->packageRoot('config/enlighten.php'), 'enlighten'); 37 | 38 | $this->registerDatabaseConnection($this->app['config']); 39 | 40 | $this->loadRoutesFrom($this->packageRoot('src/Http/routes/api.php')); 41 | 42 | if ($this->app[Settings::class]->dashboardEnabled() || $this->app->runningInConsole()) { 43 | $this->loadRoutesFrom($this->packageRoot('src/Http/routes/web.php')); 44 | $this->loadViewsFrom($this->packageRoot('resources/views'), 'enlighten'); 45 | $this->loadTranslationsFrom($this->packageRoot('resources/lang'), 'enlighten'); 46 | $this->registerViewComponents(); 47 | } 48 | 49 | if ($this->app->runningInConsole()) { 50 | $this->registerMiddleware(); 51 | 52 | $this->registerPublishing(); 53 | 54 | $this->registerCommands(); 55 | } 56 | } 57 | 58 | public function register(): void 59 | { 60 | $this->registerSettings(); 61 | $this->registerRunBuilder(); 62 | $this->registerExampleCreator(); 63 | $this->registerVersionControlSystem(); 64 | $this->registerHttpExampleCreator(); 65 | $this->registerCodeResultFormat(); 66 | } 67 | 68 | private function registerMiddleware(): void 69 | { 70 | $this->app[HttpKernel::class]->pushMiddleware(HttpExampleCreatorMiddleware::class); 71 | } 72 | 73 | private function registerSettings(): void 74 | { 75 | $this->app->singleton(Settings::class, fn () => new Settings); 76 | } 77 | 78 | private function registerRunBuilder(): void 79 | { 80 | $this->app->singleton(RunBuilder::class, fn ($app) => $this->getDriver($app)); 81 | } 82 | 83 | private function getDriver($app) 84 | { 85 | return match ($app['config']->get('enlighten.driver', 'database')) { 86 | 'database' => new DatabaseRunBuilder, 87 | 'api' => new ApiRunBuilder, 88 | default => throw new InvalidDriverException, 89 | }; 90 | } 91 | 92 | private function registerExampleCreator(): void 93 | { 94 | $this->app->singleton(ExampleCreator::class, function ($app) { 95 | $annotations = new Annotations; 96 | 97 | $annotations->addCast('enlighten', function ($value) { 98 | $options = json_decode($value, JSON_OBJECT_AS_ARRAY); 99 | return array_merge(['include' => true], $options ?: []); 100 | }); 101 | 102 | return new ExampleCreator( 103 | $app[RunBuilder::class], 104 | $annotations, 105 | $app[Settings::class], 106 | new ExampleProfile($app['config']->get('enlighten.tests')), 107 | ); 108 | }); 109 | } 110 | 111 | private function registerVersionControlSystem(): void 112 | { 113 | $this->app->singleton(VersionControl::class, Git::class); 114 | } 115 | 116 | private function registerHttpExampleCreator(): void 117 | { 118 | $this->app->singleton(HttpExampleCreator::class, fn ($app) => new HttpExampleCreator( 119 | $app[ExampleCreator::class], 120 | new RequestInspector, 121 | new RouteInspector, 122 | new ResponseInspector, 123 | new SessionInspector($app['session.store']), 124 | )); 125 | } 126 | 127 | private function registerCodeResultFormat(): void 128 | { 129 | $this->app->singleton(CodeResultFormat::class, HtmlResultFormat::class); 130 | } 131 | 132 | private function packageRoot(string $path): string 133 | { 134 | return __DIR__.'/../../'.$path; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Providers/RegistersConsoleConfiguration.php: -------------------------------------------------------------------------------- 1 | app->singleton(MigrateCommand::class, fn ($app) => new MigrateCommand($app['migrator'], $app['events'])); 20 | 21 | $this->app->singleton(ExportDocumentationCommand::class, fn ($app) => new ExportDocumentationCommand( 22 | new DocumentationExporter( 23 | $app[Filesystem::class], 24 | new ContentRequest($app[HttpKernel::class]), 25 | ) 26 | )); 27 | 28 | $this->commands([ 29 | InstallCommand::class, 30 | FreshCommand::class, 31 | MigrateCommand::class, 32 | GenerateDocumentationCommand::class, 33 | ExportDocumentationCommand::class 34 | ]); 35 | } 36 | 37 | private function registerPublishing(): void 38 | { 39 | $this->publishes([ 40 | $this->packageRoot('config') => base_path('config'), 41 | ], ['enlighten', 'enlighten-config']); 42 | 43 | $this->publishes([ 44 | $this->packageRoot('dist') => public_path('vendor/enlighten'), 45 | $this->packageRoot('/preview.png') => public_path('vendor/enlighten/img/preview.png'), 46 | ], ['enlighten', 'enlighten-build']); 47 | 48 | $this->publishes([ 49 | $this->packageRoot('resources/views') => resource_path('views/vendor/enlighten'), 50 | ], 'enlighten-views'); 51 | 52 | $this->publishes([ 53 | $this->packageRoot('resources/lang') => resource_path('lang/vendor/enlighten'), 54 | ], 'enlighten-translations'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Providers/RegistersDatabaseConnection.php: -------------------------------------------------------------------------------- 1 | has('database.connections.enlighten')) { 13 | return; 14 | } 15 | 16 | $connection = $config->get('database.connections.'.$config->get('database.default')); 17 | 18 | $config->set('database.connections.enlighten', array_merge($connection, [ 19 | 'database' => $this->guessDatabaseName($connection), 20 | ])); 21 | } 22 | 23 | protected function guessDatabaseName(array $connection) 24 | { 25 | if ($connection['driver'] === 'sqlite') { 26 | return $connection['database']; 27 | } 28 | 29 | $result = $connection['database']; 30 | 31 | if (Str::endsWith($result, '_tests')) { 32 | $result = Str::substr($result, 0, -6); 33 | } elseif (Str::endsWith($result, '_test')) { 34 | $result = Str::substr($result, 0, -5); 35 | } 36 | 37 | return "{$result}_enlighten"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Providers/RegistersViewComponents.php: -------------------------------------------------------------------------------- 1 | loadViewComponentsAs('enlighten', [ 30 | 'status-badge' => StatusBadgeComponent::class, 31 | 'response-info' => ResponseInfoComponent::class, 32 | 'request-info' => RequestInfoComponent::class, 33 | 'stats-badge' => StatsBadgeComponent::class, 34 | 'html-response' => HtmlResponseComponent::class, 35 | 'key-value' => KeyValueComponent::class, 36 | 'app-layout' => AppLayoutComponent::class, 37 | 'route-parameters-table' => RouteParametersTableComponent::class, 38 | 'request-input-table' => RequestInputTableComponent::class, 39 | 'dynamic-tabs' => DynamicTabsComponent::class, 40 | 'exception-info' => ExceptionInfoComponent::class, 41 | 'edit-button' => EditButtonComponent::class, 42 | 'breadcrumbs' => BreadcrumbsComponent::class, 43 | 'search-box-static' => SearchBoxStaticComponent::class, 44 | 'search-box' => SearchBoxComponent::class, 45 | // Group 46 | 'code-example' => CodeExampleComponent::class, 47 | 'content-table' => 'enlighten::components.content-table', 48 | 'response-preview' => 'enlighten::components.response-preview', 49 | // Layout components 50 | 'info-panel' => 'enlighten::components.info-panel', 51 | 'scroll-to-top' => 'enlighten::components.scroll-to-top', 52 | 'pre' => 'enlighten::components.pre', 53 | 'main-layout' => 'enlighten::layout.main', 54 | 'area-module-panel' => 'enlighten::components.area-module-panel', 55 | 'queries-info' => 'enlighten::components.queries-info', 56 | 'iframe' => 'enlighten::components.iframe', 57 | 'widget' => 'enlighten::components.widget', 58 | 'expansible-section' => 'enlighten::components.expansible-section', 59 | 'svg-logo' => 'enlighten::components.svg-logo', 60 | 'runs-table' => 'enlighten::components.runs-table', 61 | 'panel-title' => 'enlighten::components.panel-title', 62 | 'example-snippets' => 'enlighten::components.example-snippets', 63 | 'example-tabs' => ExampleTabsComponent::class, 64 | 'example-breadcrumbs' => ExampleBreadcrumbs::class, 65 | 'group-breadcrumbs' => GroupBreadcrumbs::class 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Section.php: -------------------------------------------------------------------------------- 1 | hide($sectionName); 39 | } 40 | 41 | public function setCustomAreaResolver(Closure $callback): self 42 | { 43 | $this->customAreaResolver = $callback; 44 | 45 | return $this; 46 | } 47 | 48 | public function getAreaSlug(string $className): string 49 | { 50 | if ($this->customAreaResolver != null) { 51 | return Str::slug(call_user_func($this->customAreaResolver, $className)); 52 | } 53 | 54 | return Str::slug(collect(explode('\\', $className))[1]); 55 | } 56 | 57 | public function setCustomTitleGenerator(Closure $callback): self 58 | { 59 | $this->customTitleGenerator = $callback; 60 | 61 | return $this; 62 | } 63 | 64 | public function generateTitle(string $type, string $classOrMethodName): string 65 | { 66 | if ($this->customTitleGenerator) { 67 | return call_user_func($this->customTitleGenerator, $type, $classOrMethodName); 68 | } elseif ($type == 'class') { 69 | return $this->generateDefaultTitleFromClassName($classOrMethodName); 70 | } else { 71 | return $this->generateDefaultTitleFromMethodName($classOrMethodName); 72 | } 73 | } 74 | 75 | protected function generateDefaultTitleFromMethodName($methodName): string 76 | { 77 | $result = Str::of($methodName); 78 | 79 | if ($result->startsWith('test')) { 80 | $result = $result->substr(4); 81 | } 82 | 83 | return $result 84 | ->replaceMatches('@([A-Z])|_@', ' $1') 85 | ->lower() 86 | ->trim() 87 | ->ucfirst() 88 | ->__toString(); 89 | } 90 | 91 | protected function generateDefaultTitleFromClassName($className): string 92 | { 93 | $result = Str::of(class_basename($className)); 94 | 95 | if ($result->endsWith('Test')) { 96 | $result = $result->substr(0, -4); 97 | } 98 | 99 | return $result 100 | ->replaceMatches('@([A-Z])@', ' $1') 101 | ->trim() 102 | ->__toString(); 103 | } 104 | 105 | public function setCustomSlugGenerator(Closure $callback): self 106 | { 107 | $this->customSlugGenerator = $callback; 108 | 109 | return $this; 110 | } 111 | 112 | public function generateSlugFromClassName($className): string 113 | { 114 | if ($this->customSlugGenerator) { 115 | return call_user_func($this->customSlugGenerator, $className, 'class'); 116 | } 117 | 118 | $result = Str::of($className); 119 | 120 | if ($result->startsWith('Tests\\')) { 121 | $result = $result->substr(6); 122 | } 123 | 124 | if ($result->endsWith('Test')) { 125 | $result = $result->substr(0, -4); 126 | } 127 | 128 | return $result 129 | ->replaceMatches('@([A-Z])@', '-$1') 130 | ->ltrim('-') 131 | ->slug() 132 | ->__toString(); 133 | } 134 | 135 | public function generateSlugFromMethodName($methodName): string 136 | { 137 | if ($this->customSlugGenerator) { 138 | return call_user_func($this->customSlugGenerator, $methodName, 'method'); 139 | } 140 | 141 | $result = Str::of($methodName); 142 | 143 | if ($result->startsWith('test') || $result->startsWith('Test')) { 144 | $result = $result->substr(4); 145 | } 146 | 147 | return $result 148 | ->replaceMatches('@([A-Z])@', '-$1') 149 | ->ltrim('-') 150 | ->slug() 151 | ->__toString(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Tests/EnlightenSetup.php: -------------------------------------------------------------------------------- 1 | app)) { 22 | throw new LaravelNotPresent; 23 | } 24 | 25 | if (Enlighten::isDocumenting()) { 26 | $this->afterApplicationCreated(function () { 27 | $this->makeExample(); 28 | 29 | $this->captureExceptions(); 30 | 31 | $this->captureQueries(); 32 | }); 33 | 34 | $this->beforeApplicationDestroyed(function () { 35 | $this->stopCapturingQueries(); 36 | 37 | $this->saveExampleStatus(); 38 | 39 | $this->restoreBackupOriginalExceptionHandler(); 40 | }); 41 | } 42 | } 43 | 44 | private function makeExample(): void 45 | { 46 | $this->app->make(ExampleCreator::class)->makeExample( 47 | $this::class, 48 | $this->name(), 49 | $this->providedData(), 50 | $this->dataName() 51 | ); 52 | } 53 | 54 | private function captureQueries(): void 55 | { 56 | DB::listen(function ($query) { 57 | if (! $this->captureQueries) { 58 | return; 59 | } 60 | 61 | if ($query->connectionName === 'enlighten') { 62 | return; 63 | } 64 | 65 | $this->app->make(ExampleCreator::class)->addQuery($query); 66 | }); 67 | } 68 | 69 | private function stopCapturingQueries(): void 70 | { 71 | $this->captureQueries = false; 72 | } 73 | 74 | private function captureExceptions(): void 75 | { 76 | if ($this->exceptionRecorder === null) { 77 | $this->backupOriginalExceptionHandler = $this->app->make(ExceptionHandler::class); 78 | $this->exceptionRecorder = new ExceptionRecorder($this->backupOriginalExceptionHandler); 79 | } 80 | 81 | $this->app->instance(ExceptionHandler::class, $this->exceptionRecorder); 82 | } 83 | 84 | private function restoreBackupOriginalExceptionHandler(): void 85 | { 86 | $this->app->instance(ExceptionHandler::class, $this->backupOriginalExceptionHandler); 87 | } 88 | 89 | /** 90 | * Only handle the given exceptions via the exception handler. 91 | * 92 | * @return $this 93 | */ 94 | protected function withoutExceptionHandling(array $except = []): self 95 | { 96 | if (Enlighten::isDocumenting()) { 97 | $this->captureExceptions(); 98 | 99 | $this->exceptionRecorder->forceThrow($except); 100 | 101 | return $this; 102 | } else { 103 | return parent::withoutExceptionHandling($except); 104 | } 105 | } 106 | 107 | /** 108 | * Restore exception handling. 109 | * 110 | * @return $this 111 | */ 112 | protected function withExceptionHandling(): self 113 | { 114 | if (Enlighten::isDocumenting()) { 115 | $this->captureExceptions(); 116 | 117 | $this->exceptionRecorder->forwardToOriginal(); 118 | 119 | return $this; 120 | } else { 121 | return parent::withExceptionHandling(); 122 | } 123 | } 124 | 125 | protected function saveExampleStatus(): void 126 | { 127 | $exampleCreator = $this->app->make(ExampleCreator::class); 128 | 129 | $exampleCreator->setStatus($this->getStatusAsText()); 130 | $exampleCreator->build(); 131 | } 132 | 133 | private function getStatusAsText(): string 134 | { 135 | return $this->status()->asString(); 136 | } 137 | 138 | /** 139 | * Follow a redirect chain until a non-redirect is received. 140 | * 141 | * @param \Illuminate\Http\Response $response 142 | * @return \Illuminate\Http\Response|\Illuminate\Testing\TestResponse 143 | */ 144 | protected function followRedirects($response) 145 | { 146 | return HttpExampleCreator::followingRedirect(fn () => parent::followRedirects($response)); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Tests/ExceptionRecorder.php: -------------------------------------------------------------------------------- 1 | forwardToOriginalHandler = true; 27 | $this->except = []; 28 | } 29 | 30 | public function forceThrow(array $except = []): void 31 | { 32 | $this->forwardToOriginalHandler = false; 33 | $this->except = $except; 34 | } 35 | 36 | public function report(Throwable $e): void 37 | { 38 | app(ExampleCreator::class)->captureException($e); 39 | 40 | if ($this->forwardToOriginalHandler) { 41 | $this->originalHandler->report($e); 42 | } 43 | } 44 | 45 | public function shouldReport(Throwable $e) 46 | { 47 | if ($this->forwardToOriginalHandler) { 48 | return $this->originalHandler->shouldReport($e); 49 | } 50 | 51 | return false; 52 | } 53 | 54 | public function render($request, Throwable $e) 55 | { 56 | if ($this->forwardToOriginalHandler) { 57 | return $this->originalHandler->render($request, $e); 58 | } 59 | 60 | foreach ($this->except as $class) { 61 | if ($e instanceof $class) { 62 | return $this->originalHandler->render($request, $e); 63 | } 64 | } 65 | 66 | if ($e instanceof NotFoundHttpException) { 67 | throw new NotFoundHttpException( 68 | "{$request->method()} {$request->url()}", 69 | null, 70 | $e->getCode() 71 | ); 72 | } 73 | 74 | throw $e; 75 | } 76 | 77 | public function renderForConsole($output, Throwable $e): void 78 | { 79 | if ($this->forwardToOriginalHandler) { 80 | $this->originalHandler->renderForConsole($output, $e); 81 | return; 82 | } 83 | 84 | (new ConsoleApplication)->renderThrowable($e, $output); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | casts[$key] = $callback; 17 | } 18 | 19 | public function getFromClass($class): Collection 20 | { 21 | $reflectionClass = new ReflectionClass($class); 22 | 23 | return $this->fromDocComment($reflectionClass->getDocComment()); 24 | } 25 | 26 | public function getFromMethod($class, $method): Collection 27 | { 28 | $reflectionMethod = new ReflectionMethod($class, $method); 29 | 30 | return $this->fromDocComment($reflectionMethod->getDocComment()); 31 | } 32 | 33 | protected function fromDocComment($docComment) 34 | { 35 | return Collection::make(explode(PHP_EOL, trim((string) $docComment, '/*'))) 36 | ->map(fn ($line) => ltrim(rtrim((string) $line, ' .'), '* ')) 37 | ->pipe(fn ($collection) => Collection::make(static::chunkByAnnotation($collection))) 38 | ->map(fn ($value, $name) => static::applyCast($name, trim((string) $value))); 39 | } 40 | 41 | protected function chunkByAnnotation(Collection $lines) 42 | { 43 | $result = []; 44 | 45 | foreach ($lines as $line) { 46 | if (preg_match("#^@(\w+)(.*?)?$#", (string) $line, $matches)) { 47 | $currentAnnotation = $matches[1]; 48 | $result[$currentAnnotation] = $matches[2] ?? ''; 49 | continue; 50 | } 51 | 52 | if (isset($currentAnnotation)) { 53 | $result[$currentAnnotation] .= PHP_EOL.$line; 54 | } 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | protected function applyCast($name, $value) 61 | { 62 | if (empty($this->casts[$name])) { 63 | return $value; 64 | } 65 | 66 | return call_user_func($this->casts[$name], $value); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Utils/FileLink.php: -------------------------------------------------------------------------------- 1 | 'phpstorm://open?file={path}&line={line}', 11 | 'sublime' => 'subl://open?url=file://{path}&line={line}', 12 | 'vscode' => 'vscode://file/{path}:{line}', 13 | ]; 14 | 15 | public static $template; 16 | 17 | public static function get(string $path, ?int $line = 1) 18 | { 19 | if (static::$template == null) { 20 | static::$template = Arr::get( 21 | static::$editors, 22 | config('enlighten.editor', 'phpstorm'), 23 | Arr::first(static::$editors) 24 | ); 25 | } 26 | 27 | return str_replace(['{path}', '{line}'], [urlencode(base_path($path)), $line], (string) static::$template); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Utils/Git.php: -------------------------------------------------------------------------------- 1 | activeRun = $request->route('run') ?: Run::latest()->firstOrNew(); 18 | } 19 | 20 | public function render() 21 | { 22 | return view('enlighten::components.app-layout', [ 23 | 'showDashboardLink' => ! app()->runningInConsole(), 24 | 'useStaticSearch' => app()->runningInConsole(), 25 | ]); 26 | } 27 | 28 | public function tabs() 29 | { 30 | return $this->activeRun->areas->map(fn ($area) => [ 31 | 'slug' => $area->slug, 32 | 'title' => $area->name, 33 | 'active' => $area->slug === request()->route('area'), 34 | 'panels' => $this->panels($area) 35 | ]); 36 | } 37 | 38 | public function panels(Area $area) 39 | { 40 | return Module::all() 41 | ->wrapGroups( 42 | $this->activeRun->groups->where('area', $area->slug) 43 | )->filter(fn ($panel) => $panel->groups->isNotEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/View/Components/BreadcrumbsComponent.php: -------------------------------------------------------------------------------- 1 | segments = $segments; 14 | } 15 | 16 | public function render() 17 | { 18 | return view('enlighten::components.breadcrumbs'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/View/Components/CodeExampleComponent.php: -------------------------------------------------------------------------------- 1 | example = $example; 20 | } 21 | 22 | public function render() 23 | { 24 | return view('enlighten::group._code-example', [ 25 | 'developer_mode' => config('enlighten.developer-mode'), 26 | 'failed' => $this->example->getStatus() !== Status::SUCCESS, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/View/Components/DynamicTabsComponent.php: -------------------------------------------------------------------------------- 1 | tabs = $this->normalizeTabs($tabs); 19 | $this->type = $type; 20 | $this->htmlable = class_exists(ComponentSlot::class) ? ComponentSlot::class : HtmlString::class; 21 | } 22 | 23 | public function render() 24 | { 25 | if ($this->type === 'menu') { 26 | $view = 'enlighten::components.dynamic-tabs-menu'; 27 | } else { 28 | $view = 'enlighten::components.dynamic-tabs'; 29 | } 30 | 31 | return view($view, [ 32 | 'tabs_collection' => $this->tabs 33 | ]); 34 | } 35 | 36 | private function normalizeTabs(array $tabs): Collection 37 | { 38 | return collect($tabs)->mapWithKeys(function ($value, $key) { 39 | if (is_numeric($key)) { 40 | return [strtolower($value) => $value]; 41 | } 42 | 43 | return [$key => $value]; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/View/Components/EditButtonComponent.php: -------------------------------------------------------------------------------- 1 | file) 17 | && ! app()->runningInConsole(); 18 | } 19 | 20 | public function render() 21 | { 22 | return view('enlighten::components.edit-button', [ 23 | 'file' => $this->file 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/View/Components/ExampleBreadcrumbs.php: -------------------------------------------------------------------------------- 1 | example = $example; 16 | } 17 | 18 | public function render() 19 | { 20 | return view('enlighten::components.example-breadcrumbs', [ 21 | 'segments' => $this->getBreadcrumbSegments() 22 | ]); 23 | } 24 | 25 | private function getBreadcrumbSegments(): array 26 | { 27 | return [ 28 | route('enlighten.area.show', ['run' => $this->example->group->run_id, 'area' => $this->example->group->area]) => ucwords((string) $this->example->group->area), 29 | $this->example->group->url => $this->example->group->title 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/View/Components/ExampleTabsComponent.php: -------------------------------------------------------------------------------- 1 | example = $example; 19 | } 20 | 21 | public function render() 22 | { 23 | return view('enlighten::components.example-tabs', [ 24 | 'showRequests' => $this->example->requests->isNotEmpty(), 25 | 'requestTabs' => $this->getRequestTabs(), 26 | 'showQueries' => $this->shouldShowQueries(), 27 | 'showException' => $this->showException(), 28 | ]); 29 | } 30 | private function showException() 31 | { 32 | if (Settings::hide(Section::EXCEPTION)) { 33 | return false; 34 | } 35 | 36 | return $this->example->has_exception; 37 | } 38 | 39 | private function shouldShowQueries(): bool 40 | { 41 | if (Settings::hide(Section::QUERIES)) { 42 | return false; 43 | } 44 | 45 | return $this->example->queries->isNotEmpty(); 46 | } 47 | 48 | private function getRequestTabs() 49 | { 50 | return $this->example->requests->map(fn ($request, $key) => $this->newRequestTab($request, $key + 1)); 51 | } 52 | 53 | private function newRequestTab(ExampleRequest $request, int $requestNumber): object 54 | { 55 | return (object) [ 56 | 'request' => $request, 57 | 'key' => $request->hash, 58 | 'title' => "Request #{$requestNumber}", 59 | 'showSession' => $this->shouldShowSessionTab($request), 60 | 'showPreviewOnly' => $this->shouldOnlyShowPreview($request), 61 | ]; 62 | } 63 | 64 | private function shouldShowSessionTab(ExampleRequest $request): bool 65 | { 66 | return Settings::show(Section::SESSION) && !empty($request->session_data); 67 | } 68 | 69 | private function shouldOnlyShowPreview(ExampleRequest $request): bool 70 | { 71 | return $this->example->has_exception && $request->response_type === 'JSON'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/View/Components/ExceptionInfoComponent.php: -------------------------------------------------------------------------------- 1 | exception->trace)) { 17 | return collect(); 18 | } 19 | 20 | return collect($this->exception->trace) 21 | ->map(fn ($data) => [ 22 | 'file' => $data['file'] ?? '', 23 | 'line' => $data['line'] ?? '', 24 | 'function' => $this->getFunctionSignature($data), 25 | 'args' => $data['args'] ?? [], 26 | ]); 27 | } 28 | 29 | private function getFunctionSignature(array $data): string 30 | { 31 | if (empty($data['class'])) { 32 | return $data['function']; 33 | } 34 | 35 | return $data['class'].$data['type'].$data['function']; 36 | } 37 | 38 | public function render() 39 | { 40 | if ($this->trace()->isEmpty()) { 41 | return; 42 | } 43 | 44 | return view('enlighten::components.exception-info', [ 45 | 'trace' => $this->trace(), 46 | 'exception' => $this->exception 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/View/Components/GroupBreadcrumbs.php: -------------------------------------------------------------------------------- 1 | exampleGroup = $group; 15 | } 16 | 17 | public function render() 18 | { 19 | return view('enlighten::components.group-breadcrumbs', [ 20 | 'segments' => $this->getBreadcrumbsSegments() 21 | ]); 22 | } 23 | 24 | private function getBreadcrumbsSegments(): array 25 | { 26 | return [ 27 | route('enlighten.area.show', [ 28 | 'run' => $this->exampleGroup->run_id, 29 | 'area' => $this->exampleGroup->area 30 | ]) => ucwords((string) $this->exampleGroup->area), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/View/Components/HtmlResponseComponent.php: -------------------------------------------------------------------------------- 1 | request = $request; 20 | } 21 | 22 | public function render() 23 | { 24 | return view('enlighten::components.html-response', [ 25 | 'showHtml' => Settings::show(Section::HTML), 26 | 'showTemplate' => $this->showTemplate(), 27 | ]); 28 | } 29 | 30 | private function showTemplate(): bool 31 | { 32 | if (Settings::hide(Section::BLADE)) { 33 | return false; 34 | } 35 | 36 | return ! empty($this->request->response_template); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/View/Components/KeyValueComponent.php: -------------------------------------------------------------------------------- 1 | items = $this->normalizeItems($items); 23 | 24 | $this->title = $title; 25 | } 26 | 27 | private function normalizeItems(array $items): array 28 | { 29 | return array_map(fn ($value) => is_array($value) ? implode('
', $value) : $value, $items); 30 | } 31 | 32 | public function render() 33 | { 34 | return view('enlighten::components.key-value'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/View/Components/RepresentsStatusAsColor.php: -------------------------------------------------------------------------------- 1 | 'green', 14 | 'warning' => 'yellow', 15 | 'failure' => 'red' 16 | ])->get($model->getStatus(), 'yellow'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/View/Components/RequestInfoComponent.php: -------------------------------------------------------------------------------- 1 | $this->routeInfo($this->request), 20 | 'request' => $this->request, 21 | 'request_input' => $this->normalizeRequestInput(), 22 | 'showRouteParameters' => $this->showRouteParameters(), 23 | 'showInput' => $this->showInput(), 24 | 'showHeaders' => $this->showHeaders(), 25 | ]); 26 | } 27 | 28 | private function showRouteParameters() 29 | { 30 | if (Settings::hide(Section::ROUTE_PARAMETERS)) { 31 | return false; 32 | } 33 | 34 | return ! empty($this->request->route_parameters); 35 | } 36 | 37 | private function showInput() 38 | { 39 | if (Settings::hide(Section::REQUEST_INPUT)) { 40 | return false; 41 | } 42 | 43 | return ! empty($this->request->request_input); 44 | } 45 | 46 | private function showHeaders() 47 | { 48 | if (Settings::hide(Section::REQUEST_HEADERS)) { 49 | return false; 50 | } 51 | 52 | return ! empty($this->request->request_headers); 53 | } 54 | 55 | private function routeInfo(ExampleRequest $request): array 56 | { 57 | return [ 58 | 'Method' => $this->request->request_method, 59 | 'Route' => $this->request->route, 60 | 'Example' => $this->request->request_path . ($this->request->request_query_parameters ? '?' . http_build_query($this->request->request_query_parameters) : ''), 61 | ]; 62 | } 63 | 64 | private function normalizeRequestInput(): array 65 | { 66 | return collect($this->request['request_input']) 67 | ->map(fn ($value) => is_array($value) ? enlighten_json_prettify($value) : $value)->toArray(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/View/Components/RequestInputTableComponent.php: -------------------------------------------------------------------------------- 1 | input = $this->normalizeInput($input); 18 | } 19 | 20 | private function normalizeInput(array $input): array 21 | { 22 | return array_map(fn ($value) => is_array($value) ? implode(': ', $value) : $value, $input); 23 | } 24 | 25 | public function render() 26 | { 27 | return view('enlighten::components.request-input-table'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/View/Components/ResponseInfoComponent.php: -------------------------------------------------------------------------------- 1 | request = $request; 20 | } 21 | 22 | public function render() 23 | { 24 | return view('enlighten::components.response-info', [ 25 | 'request' => $this->request, 26 | 'color' => $this->request->getStatus(), 27 | 'status' => $this->request->response_status ?? 'UNKNOWN', 28 | 'showHeaders' => $this->showHeaders(), 29 | ]); 30 | } 31 | 32 | private function showHeaders() 33 | { 34 | if (Settings::hide(Section::RESPONSE_HEADERS)) { 35 | return false; 36 | } 37 | 38 | return ! empty($this->request->response_headers); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/View/Components/RouteParametersTableComponent.php: -------------------------------------------------------------------------------- 1 | parameters = $this->normalizeParameters($parameters); 18 | } 19 | 20 | public function normalizeParameters(array $parameters): array 21 | { 22 | return array_map(function ($parameter) { 23 | $parameter['requirement'] = $parameter['optional'] ? 'Optional' : 'Required'; 24 | return $parameter; 25 | }, $parameters); 26 | } 27 | 28 | public function render() 29 | { 30 | return view('enlighten::components.route-parameters-table'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/View/Components/SearchBoxComponent.php: -------------------------------------------------------------------------------- 1 | activeRun = $run; 15 | } 16 | 17 | public function render() 18 | { 19 | return view('enlighten::components.search-box', [ 20 | 'searchUrl' => route('enlighten.api.search', ['run' => $this->activeRun]) 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/View/Components/SearchBoxStaticComponent.php: -------------------------------------------------------------------------------- 1 | $this->model->getPassingTestsCount(), 18 | 'total' => $this->model->getTestsCount(), 19 | 'color' => $this->model->getStatus(), 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/View/Components/StatusBadgeComponent.php: -------------------------------------------------------------------------------- 1 | model = $model; 23 | $this->size = $size; 24 | } 25 | 26 | public function render() 27 | { 28 | return view('enlighten::components.status-badge', [ 29 | 'color' => $this->model->getStatus(), 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |