knuckleswtf / scribe

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

Eloquent model factory failed to instantiate #169

Closed maguilar92 closed 3 years ago

maguilar92 commented 3 years ago

What happened? The doc generation failed to instantiate User model factory.

  1. Set the api doc with:
    /**
     * Invite user
     *
     * @bodyParam collective_key string The collective where invite the user, null will invite to the platform. No-example
     * @bodyParam inviter_message string The message that will be send to the user. Example: I invite you to copernic, you will found amazing apps.
     * @bodyParam users object required
     * @bodyParam users[].user_type_key string required Key of the user type. Example: No-Example
     * @bodyParam users[].email string required Email of the user to be invited. Example: User
     * @bodyParam users[].name string required Name of the user to be invited. Example: User
     * @bodyParam users[].surname string required Surname of the user to be invited. Example: Invited
     * @apiResourceCollection App\Http\Resources\Basic\UserResource
     * @apiResourceModel Copernic\Core\User\Models\User
     */
  2. Then I ran php artisan scribe:generate and this warning appears: warning Eloquent model factory failed to instantiate Copernic\Core\User\Models\User; trying to fetch from database.
  3. But running $user = \Copernic\Core\User\Models\User::factory()->make(); on tinker works perfectly!

Screenshots and stack traces:

Screenshot 2020-12-21 at 14 48 40 Screenshot 2020-12-21 at 14 53 16

My environment:

My Scribe config (minus the comments):

<?php

return [
    'type' => 'static',
    'static' => [
        'output_path' => 'api-doc/html',
    ],
    'laravel' => [
        'add_routes' => true,
        'docs_url' => '/docs',
        'middleware' => [],
    ],
    'auth' => [
        'enabled' => true,
        'in' => 'bearer',
        'name' => 'token',
        'use_value' => env('SCRIBE_AUTH_KEY'),
        'placeholder' => '{YOUR_AUTH_KEY}',
        'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking <b>Generate API token</b>.',
    ],
    'intro_text' => <<<INTRO
Welcome to our API documentation!

<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), and you can switch the programming language of the examples with the tabs in the top right (or from the nav menu at the top left on mobile).</aside>
INTRO
    ,
    'example_languages' => ['bash', 'javascript', 'php',],
    'base_url' => null,
    'title' => null,
    'description' => '',
    'postman' => [
        'enabled' => true,
        'overrides' => [],
    ],
    'openapi' => [
        'enabled' => false,
        'overrides' => [],
    ],
    'default_group' => 'Endpoints',
    'logo' => false,
    'router' => 'laravel',
    'routes' => [
        [
            'match' => [
                'domains' => ['*'],
                'prefixes' => ['*'],
                'versions' => ['v1'],
            ],
            'include' => [],
            'exclude' => [],
            'apply' => [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'response_calls' => [
                    'methods' => ['GET'],
                    'config' => [
                        'app.env' => 'documentation',
                        'app.debug' => false,
                    ],
                    'cookies' => [],
                    'queryParams' => [],
                    'bodyParams' => [],
                    'fileParams' => [],
                ],
            ],
        ],
    ],
    'fractal' => [
        'serializer' => null,
    ],
    'faker_seed' => null,
    'strategies' => [
        'metadata' => [
            \Knuckles\Scribe\Extracting\Strategies\Metadata\GetFromDocBlocks::class,
        ],
        'urlParameters' => [
            \Knuckles\Scribe\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class,
        ],
        'queryParameters' => [
            \Knuckles\Scribe\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class,
        ],
        'headers' => [
            \Knuckles\Scribe\Extracting\Strategies\Headers\GetFromRouteRules::class,
            \Knuckles\Scribe\Extracting\Strategies\Headers\GetFromHeaderTag::class,
        ],
        'bodyParameters' => [
            \Knuckles\Scribe\Extracting\Strategies\BodyParameters\GetFromFormRequest::class,
            \Knuckles\Scribe\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class,
        ],
        'responses' => [
            \Knuckles\Scribe\Extracting\Strategies\Responses\UseTransformerTags::class,
            \Knuckles\Scribe\Extracting\Strategies\Responses\UseResponseTag::class,
            \Knuckles\Scribe\Extracting\Strategies\Responses\UseResponseFileTag::class,
            \Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags::class,
            \Knuckles\Scribe\Extracting\Strategies\Responses\ResponseCalls::class,
        ],
        'responseFields' => [
            \Knuckles\Scribe\Extracting\Strategies\ResponseFields\GetFromResponseFieldTag::class,
        ],
    ],
    'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,
    'continue_without_database_transactions' => [],
];

Additional info: I have two projects, one with the api endpoints (using laravel actions) and another with the business logic. For this reason I have defined in Users model:

    protected static function newFactory()
    {
        return \Copernic\Core\User\Database\Factories\UserFactory::new();
    }

Also it will be nice if could be improved how to generate the documentation using laravel actions, actually I must document the __invoke method because does not work in handle method.

shalvah commented 3 years ago

That message means it encountered an exception when making the factory. Try running in verbose mode to see the exception. (If it doesn't log the exception, edit the code and add a dump statement.)

maguilar92 commented 3 years ago

Hi @shalvah. Thanks for your replay. Here you have the error display:

🚸 warning Eloquent model factory failed to instantiate Copernic\Core\User\Models\User; trying to fetch from database.

   InvalidArgumentException

  Unable to locate factory for [Copernic\Core\User\Models\User].

  at vendor/laravel/legacy-factories/src/FactoryBuilder.php:273
    269β–•      */
    270β–•     protected function getRawAttributes(array $attributes = [])
    271β–•     {
    272β–•         if (! isset($this->definitions[$this->class])) {
  ➜ 273β–•             throw new InvalidArgumentException("Unable to locate factory for [{$this->class}].");
    274β–•         }
    275β–•
    276β–•         $definition = call_user_func(
    277β–•             $this->definitions[$this->class],

  1   vendor/laravel/legacy-factories/src/FactoryBuilder.php:296
      Illuminate\Database\Eloquent\FactoryBuilder::getRawAttributes([])

  2   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:157
      Illuminate\Database\Eloquent\FactoryBuilder::Illuminate\Database\Eloquent\{closure}()

  3   vendor/laravel/legacy-factories/src/FactoryBuilder.php:304
      Illuminate\Database\Eloquent\Model::unguarded(Object(Closure))

  4   vendor/laravel/legacy-factories/src/FactoryBuilder.php:223
      Illuminate\Database\Eloquent\FactoryBuilder::makeInstance([])

  5   vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseApiResourceTags.php:212
      Illuminate\Database\Eloquent\FactoryBuilder::make()

  6   vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseApiResourceTags.php:85
      Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags::instantiateApiResourceModel("Copernic\Core\User\Models\User", [], [])

  7   vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseApiResourceTags.php:54
      Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags::getApiResourceResponse(Object(Illuminate\Routing\Route))

  8   vendor/knuckleswtf/scribe/src/Extracting/Generator.php:236
      Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags::__invoke(Object(Illuminate\Routing\Route), Object(ReflectionClass), Object(ReflectionMethod), [])

  9   vendor/knuckleswtf/scribe/src/Extracting/Generator.php:172
      Knuckles\Scribe\Extracting\Generator::iterateThroughStrategies("responses")

  10  vendor/knuckleswtf/scribe/src/Extracting/Generator.php:127
      Knuckles\Scribe\Extracting\Generator::fetchResponses(Object(ReflectionClass), Object(ReflectionMethod), Object(Illuminate\Routing\Route), [])

  11  vendor/knuckleswtf/scribe/src/Commands/GenerateDocumentation.php:119
      Knuckles\Scribe\Extracting\Generator::processRoute(Object(Illuminate\Routing\Route), [])

  12  vendor/knuckleswtf/scribe/src/Commands/GenerateDocumentation.php:73
      Knuckles\Scribe\Commands\GenerateDocumentation::processRoutes()

  13  app/Console/Commands/GenerateDocumentation.php:31
      Knuckles\Scribe\Commands\GenerateDocumentation::handle(Object(Knuckles\Scribe\Matching\RouteMatcher))

  14  vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:36
      App\Console\Commands\GenerateDocumentation::handle(Object(Knuckles\Scribe\Matching\RouteMatcher))

  15  vendor/laravel/framework/src/Illuminate/Container/Util.php:40
      Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()

  16  vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:93
      Illuminate\Container\Util::unwrapIfClosure(Object(Closure))

  17  vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:37
      Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Object(Closure))

  18  vendor/laravel/framework/src/Illuminate/Container/Container.php:610
      Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), [])

  19  vendor/laravel/framework/src/Illuminate/Console/Command.php:136
      Illuminate\Container\Container::call()

  20  vendor/symfony/console/Command/Command.php:255
      Illuminate\Console\Command::execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))

  21  vendor/laravel/framework/src/Illuminate/Console/Command.php:121
      Symfony\Component\Console\Command\Command::run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))

  22  vendor/symfony/console/Application.php:971
      Illuminate\Console\Command::run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

  23  vendor/symfony/console/Application.php:290
      Symfony\Component\Console\Application::doRunCommand(Object(App\Console\Commands\GenerateDocumentation), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

  24  vendor/symfony/console/Application.php:166
      Symfony\Component\Console\Application::doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

  25  vendor/laravel/framework/src/Illuminate/Console/Application.php:93
      Symfony\Component\Console\Application::run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

  26  vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:129
      Illuminate\Console\Application::run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

  27  artisan:37
      Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
maguilar92 commented 3 years ago

As I can see is attempting to do using legacy factories instead laravel 8 factories

shalvah commented 3 years ago

Seems so. But that shouldn't be. These lines only use legacy factories if you have a factory() function.

https://github.com/knuckleswtf/scribe/blob/af1a0b21c9be0ab6b8532db68ce993d96fd6a561/src/Tools/Utils.php#L117-L128

What kind of factories are you using, and how do you define them? Can you add a debug within this method^^^ to see which factory approach it chooses?

shalvah commented 3 years ago

Never mind about the first part, I see how you already mentioned how you defined your factory.

But, yes, please add that logging statement over there. And log the result of this too.

https://github.com/knuckleswtf/scribe/blob/af1a0b21c9be0ab6b8532db68ce993d96fd6a561/src/Tools/Utils.php#L118

maguilar92 commented 3 years ago

Hi @shalvah. I think that is caused because I'm using a package that actually uses legacy-factories:

https://github.com/larapackages/repository

shalvah commented 3 years ago

Right. So...what to do?

maguilar92 commented 3 years ago

We could change the conditional to check if model has factory method instead if function factory exists. What do you think about @shalvah?

        if (method_exists($modelName, 'factory')) {
            $factory = call_user_func_array([$modelName, 'factory'], []);
            if (count($states)) {
                foreach ($states as $state) {
                    $factory = $factory->$state();
                }
            }
        } else {
            $factory = factory($modelName);
            if (count($states)) {
                $factory = $factory->states($states);
            }
        }

        return $factory;
shalvah commented 3 years ago

Alright. I'd welcome a PR.

maguilar92 commented 3 years ago

I created the PR with the change. Thank @shalvah πŸ˜„

shalvah commented 3 years ago

Released in 2.5.2