knuckleswtf / scribe

Generate API documentation for humans from your Laravel codebase.✍
https://scribe.knuckles.wtf/laravel/
MIT License
1.75k stars 314 forks source link

ReflectionException while running `php artisan scribe:generate` #315

Closed pratyushpundir closed 3 years ago

pratyushpundir commented 3 years ago

Hi there.

I'm attempting to get Scribe working on a Laravel project (with Jetstream + Inertia + Sail). The package installation goes without a problem but when I attempt to generate the actual docs using php artisan scribe:generate OR sail artisan scribe:generate, I get this:

ERROR:

  ReflectionException

  Class "C:32:"Opis\Closure\SerializableClosure":271:{" does not exist

  at vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromApp.php:243
    239▕
    240▕     private function doesControllerMethodExist(array $routeControllerAndMethod): bool
    241▕     {
    242▕         [$class, $method] = $routeControllerAndMethod;
  ➜ 243▕         $reflection = new ReflectionClass($class);
    244▕
    245▕         if ($reflection->hasMethod($method)) {
    246▕             return true;
    247▕         }

      +18 vendor frames
  19  artisan:37
      Illuminate\Foundation\Console\Kernel::handle()

config/scribe.php - I have tried both laravel and static as values type:

<?php

use Knuckles\Scribe\Extracting\Strategies;

return [

    'theme' => 'default',
    'title' => 'API Documentation | ' . env('APP_NAME'),
    'description' => '',
    'base_url' => null,
    'routes' => [
        [
            'match' => [
                'prefixes' => ['api/*'],
                'domains' => ['*'],
                // 'versions' => ['v1'],
            ],
            'include' => [
                // 'users.index', 'healthcheck*'
            ],
            'exclude' => [
                // '/health', 'admin.*'
            ],
            'apply' => [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'response_calls' => [
                    'methods' => ['*'],
                    'config' => [
                        'app.env' => 'documentation',
                        'app.debug' => false,
                    ],
                    'queryParams' => [
                        // 'key' => 'value',
                    ],
                    'bodyParams' => [
                        // 'key' => 'value',
                    ],
                    'fileParams' => [
                        // 'key' => 'storage/app/image.png',
                    ],
                    'cookies' => [
                        // 'name' => 'value'
                    ],
                ],
            ],
        ],
    ],

    'type' => 'static',
    'static' => [
        'output_path' => 'public/docs',
    ],
    'laravel' => [
        'add_routes' => true,
        'docs_url' => '/docs',
        'middleware' => ['auth:sanctum'],
    ],

    'try_it_out' => [
        'enabled' => true,
        'base_url' => null,
    ],
    'auth' => [
        'enabled' => true,
        'default' => false,
        'in' => 'bearer',
        'name' => 'Authorization',
        'use_value' => env('SCRIBE_AUTH_KEY'),
        'placeholder' => '{YOUR_AUTH_KEY}',
        'extra_info' => 'You can retrieve your token by visiting the <a href="/user/api-tokens">API Tokens page</a>.',
    ],
    'intro_text' => <<<INTRO
This documentation aims to provide all the information you need to work with our API.

<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
INTRO
    ,

    'example_languages' => [
        'bash',
        'javascript',
        'php',
        'python'
    ],

    'postman' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],
    'openapi' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],
    'default_group' => 'Endpoints',
    'logo' => false,
    'faker_seed' => null,
    'strategies' => [
        'metadata' => [
            Strategies\Metadata\GetFromDocBlocks::class,
        ],
        'urlParameters' => [
            Strategies\UrlParameters\GetFromLaravelAPI::class,
            Strategies\UrlParameters\GetFromLumenAPI::class,
            Strategies\UrlParameters\GetFromUrlParamTag::class,
        ],
        'queryParameters' => [
            Strategies\QueryParameters\GetFromFormRequest::class,
            Strategies\QueryParameters\GetFromInlineValidator::class,
            Strategies\QueryParameters\GetFromQueryParamTag::class,
        ],
        'headers' => [
            Strategies\Headers\GetFromRouteRules::class,
            Strategies\Headers\GetFromHeaderTag::class,
        ],
        'bodyParameters' => [
            Strategies\BodyParameters\GetFromFormRequest::class,
            Strategies\BodyParameters\GetFromInlineValidator::class,
            Strategies\BodyParameters\GetFromBodyParamTag::class,
        ],
        'responses' => [
            Strategies\Responses\UseTransformerTags::class,
            Strategies\Responses\UseResponseTag::class,
            Strategies\Responses\UseResponseFileTag::class,
            Strategies\Responses\UseApiResourceTags::class,
            Strategies\Responses\ResponseCalls::class,
        ],
        'responseFields' => [
            Strategies\ResponseFields\GetFromResponseFieldTag::class,
        ],
    ],

    'fractal' => [
        'serializer' => null,
    ],

    'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,

    'database_connections_to_transact' => [config('database.default')]
];

api.php:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/ping', function () {
    return response()->json("Pong!", 200);
});

Route::middleware(['auth:sanctum'])->group(function () {

    Route::get('/admin-user', function (Request $request) {
        if ($request->user()->tokenCan('service:administer')) {
            return response()->json($request->user(), 200);
        } else {
            return response()->json("Forbidden! You do not have the correct permissions.", 403);
        }
    });

    Route::get('/user', function (Request $request) {
        if ($request->user()->tokenCan('service:access')) {
            return response()->json($request->user(), 200);
        } else {
            return response()->json("Forbidden! You do not have the correct permissions.", 403);
        }
    });

});

REPRO:

  1. Install a fresh copy of Laravel 8.x.
  2. Configure Laravel Sail and get everything running on Docker
  3. Install Laravel Jetstream with Inertia and Teams support - at this point, all PHPUnit tests, including those for Jetstream and Authentication, should be green.
  4. Write your first API route. Example:
Route::get("/ping", function () {
  return response()->json("Pong!", 200);
})
  1. Install Scribe, configure as needed and attempt to run either php artisan scribe:generate or sail artisan scribe:generate - This is where I get the error.

Other Info:

PHP version - 8.0.10 (Local and Sail/Docker)
Composer version - 2.1.6 (Local and Sail/Docker)
Laravel version - 8.58.0 (Local and Sail/Docker)
Scribe version - ^3.9 (Local and Sail/Docker)

Any help or guidance would be much appreciated!

pratyushpundir commented 3 years ago

Closing because the issue was resolved by installing league/fractal.

Just run composer require league/fractal OR sail composer require league/fractal before trying to generate the docs

pratyushpundir commented 3 years ago

Sorry had to reopen. The issue still keeps raising its head again and again. Any pointers would be helpful!!

pratyushpundir commented 3 years ago

Hi there.

I'm attempting to get Scribe working on a Laravel project (with Jetstream + Inertia + Sail). The package installation goes without a problem but when I attempt to generate the actual docs using php artisan scribe:generate OR sail artisan scribe:generate, I get this:

ERROR:

  ReflectionException

  Class "C:32:"Opis\Closure\SerializableClosure":271:{" does not exist

  at vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromApp.php:243
    239▕
    240▕     private function doesControllerMethodExist(array $routeControllerAndMethod): bool
    241▕     {
    242▕         [$class, $method] = $routeControllerAndMethod;
  ➜ 243▕         $reflection = new ReflectionClass($class);
    244▕
    245▕         if ($reflection->hasMethod($method)) {
    246▕             return true;
    247▕         }

      +18 vendor frames
  19  artisan:37
      Illuminate\Foundation\Console\Kernel::handle()

config/scribe.php - I have tried both laravel and static as values type:

<?php

use Knuckles\Scribe\Extracting\Strategies;

return [

    'theme' => 'default',
    'title' => 'API Documentation | ' . env('APP_NAME'),
    'description' => '',
    'base_url' => null,
    'routes' => [
        [
            'match' => [
                'prefixes' => ['api/*'],
                'domains' => ['*'],
                // 'versions' => ['v1'],
            ],
            'include' => [
                // 'users.index', 'healthcheck*'
            ],
            'exclude' => [
                // '/health', 'admin.*'
            ],
            'apply' => [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'response_calls' => [
                    'methods' => ['*'],
                    'config' => [
                        'app.env' => 'documentation',
                        'app.debug' => false,
                    ],
                    'queryParams' => [
                        // 'key' => 'value',
                    ],
                    'bodyParams' => [
                        // 'key' => 'value',
                    ],
                    'fileParams' => [
                        // 'key' => 'storage/app/image.png',
                    ],
                    'cookies' => [
                        // 'name' => 'value'
                    ],
                ],
            ],
        ],
    ],

    'type' => 'static',
    'static' => [
        'output_path' => 'public/docs',
    ],
    'laravel' => [
        'add_routes' => true,
        'docs_url' => '/docs',
        'middleware' => ['auth:sanctum'],
    ],

    'try_it_out' => [
        'enabled' => true,
        'base_url' => null,
    ],
    'auth' => [
        'enabled' => true,
        'default' => false,
        'in' => 'bearer',
        'name' => 'Authorization',
        'use_value' => env('SCRIBE_AUTH_KEY'),
        'placeholder' => '{YOUR_AUTH_KEY}',
        'extra_info' => 'You can retrieve your token by visiting the <a href="/user/api-tokens">API Tokens page</a>.',
    ],
    'intro_text' => <<<INTRO
This documentation aims to provide all the information you need to work with our API.

<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
INTRO
    ,

    'example_languages' => [
        'bash',
        'javascript',
        'php',
        'python'
    ],

    'postman' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],
    'openapi' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],
    'default_group' => 'Endpoints',
    'logo' => false,
    'faker_seed' => null,
    'strategies' => [
        'metadata' => [
            Strategies\Metadata\GetFromDocBlocks::class,
        ],
        'urlParameters' => [
            Strategies\UrlParameters\GetFromLaravelAPI::class,
            Strategies\UrlParameters\GetFromLumenAPI::class,
            Strategies\UrlParameters\GetFromUrlParamTag::class,
        ],
        'queryParameters' => [
            Strategies\QueryParameters\GetFromFormRequest::class,
            Strategies\QueryParameters\GetFromInlineValidator::class,
            Strategies\QueryParameters\GetFromQueryParamTag::class,
        ],
        'headers' => [
            Strategies\Headers\GetFromRouteRules::class,
            Strategies\Headers\GetFromHeaderTag::class,
        ],
        'bodyParameters' => [
            Strategies\BodyParameters\GetFromFormRequest::class,
            Strategies\BodyParameters\GetFromInlineValidator::class,
            Strategies\BodyParameters\GetFromBodyParamTag::class,
        ],
        'responses' => [
            Strategies\Responses\UseTransformerTags::class,
            Strategies\Responses\UseResponseTag::class,
            Strategies\Responses\UseResponseFileTag::class,
            Strategies\Responses\UseApiResourceTags::class,
            Strategies\Responses\ResponseCalls::class,
        ],
        'responseFields' => [
            Strategies\ResponseFields\GetFromResponseFieldTag::class,
        ],
    ],

    'fractal' => [
        'serializer' => null,
    ],

    'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,

    'database_connections_to_transact' => [config('database.default')]
];

api.php:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/ping', function () {
    return response()->json("Pong!", 200);
});

Route::middleware(['auth:sanctum'])->group(function () {

    Route::get('/admin-user', function (Request $request) {
        if ($request->user()->tokenCan('service:administer')) {
            return response()->json($request->user(), 200);
        } else {
            return response()->json("Forbidden! You do not have the correct permissions.", 403);
        }
    });

    Route::get('/user', function (Request $request) {
        if ($request->user()->tokenCan('service:access')) {
            return response()->json($request->user(), 200);
        } else {
            return response()->json("Forbidden! You do not have the correct permissions.", 403);
        }
    });

});

REPRO:

  1. Install a fresh copy of Laravel 8.x.
  2. Configure Laravel Sail and get everything running on Docker
  3. Install Laravel Jetstream with Inertia and Teams support - at this point, all PHPUnit tests, including those for Jetstream and Authentication, should be green.
  4. Write your first API route. Example:
Route::get("/ping", function () {
  return response()->json("Pong!", 200);
})
  1. Install Scribe, configure as needed and attempt to run either php artisan scribe:generate or sail artisan scribe:generate - This is where I get the error.

Other Info:

PHP version - 8.0.10 (Local and Sail/Docker)
Composer version - 2.1.6 (Local and Sail/Docker)
Laravel version - 8.58.0 (Local and Sail/Docker)
Scribe version - ^3.9 (Local and Sail/Docker)

Any help or guidance would be much appreciated!

Still persists after updating to Scribe 3.10.2. Have added league/fractal as a "required" instead of a "required-dev" dependency as well (since I'm not installing dev dependencies on staging and prod.)

shalvah commented 3 years ago

I doubt fractal itself fixes this, but possibly running require fractal the first time must have triggered something in the Composer autoloader to correct itself. Perhaps this is an issue with Composer 2 and opis/closure?🤔

(Also, check that you have opis/closure installed, because Laravel forked it some days ago.)

pratyushpundir commented 3 years ago

I finally found the repro and how to avoid it (although it's far from ideal).

Essentially, the issue shows itself if I run php artisan optimize BEFORE php artisan scribe:generate. Seems like this is an issue when files are cached! It works fine if you do php artisan optimize:clear right before generating the docs.

shalvah commented 3 years ago

Not sure why you're running php artisan optimize. Wasn't that command deprecated/removed some time ago?

shalvah commented 3 years ago

Oh, I see it was removed in 5.6, but later [added back] (https://github.com/laravel/framework/pull/23468).

But yes, I guess it makes sense. You should do all those caching after you've run generate.

pratyushpundir commented 3 years ago

Yes it does. Thanks for the awesome package!