Artwork's avatar

Service building fails with dependency of type array.

Dear Developers,

Thank you for the incredible marvel, art you do!

Issue

For currently unknown reason, when we are trying to call a method from the service singleton to resolved and build with the service container, we get the following error:

$ ./artisan tinker --execute 'app(BookRatingsService::class);';
[!] Aliasing 'BookRatingsService' to 'App\Services\BookRatingsService' for this Tinker session.

   Illuminate\Contracts\Container\BindingResolutionException  Unresolvable dependency resolving [Parameter #0 [ <required> array $ratings ]] in class App\Services\BookRatingsService.

Example to Reproduce

The current versions are:
- Laravel v10.38.1;
- PHP v8.1.32.

Let's consider the following abstract case prepared.

<?php

namespace App\Services;

use Exception;

class BookRatingsService
{
    protected readonly array $ratings;

    public function __construct(array $ratings)
    {
        foreach ($ratings as $genre => $rating) {
            if (!is_float($rating) || !$rating <= 0) {
                throw new Exception(sprintf('Encountered unexpected rating \'%s\' for genre \'%s\'.', $rating, $key));
            }
        }

        $this->ratings = $ratings;
    }
}
<?php

namespace App\Services\Interfaces;

interface BookRatingsApiInterface
{
    /**
     * Get average rating of book genres.
     *
     * @param $genres array<int, string>
     *
     * @return array<string, float> Average rating per genre.
     */
    public function getGenreRatings(array $genres): array;
}
// config/app.php

return [
    // ...
    'providers' => [
        // ...
        App\Providers\BookRatingsServiceProvider::class,
        // ...
    ],
    // ...
];

Question

It seems the execution does not pass to the service provider's register nor boot methods if existed, and the service container fails in method \Illuminate\Container\Container::resolveDependencies, where the dependency for service BookRatingsService is likely not properly "type-hinted" - a simple array.

Therefore, why would the service provider is not called to "provide" the proper previously injected dependency - simple array of ratings?

In case the type-hinting is required in this case, how would you "type-hint" that simple array of genres and ratings?


Just in case, the "books", "genres", and "ratings" are the first idea I came with to represent the issue in a reproducible example. In the actual code, the array should contain instantiated services for books to be called in the service organized by genre (no actual ratings would be requested yet). Hence, a simple array with string and integers to be "injected" instead.


Best and kind regards

0 likes
4 replies
Artwork's avatar

It seems like the issue source is found! Right after the question posted, I noticed the difference between the abstract in Tinker being aliased and the expected in the service container:

- Tinker alias: class - App\Services\BookRatingsService;
- Container: string - App\Services\BookRatingsService::class.

Though, just to clarify, is it correct that the Tinker alias is the culprit? If so, why does Tinker alias a string to class at this point?

Artwork's avatar
Artwork
OP
Best Answer
Level 2

I see that Artisan registers itself for the standard PHP auto-loading with spl_autoload_register on:
- https://github.com/laravel/tinker/blob/102bfc19b79817022e9fb1d3dd235d43d42f1954/src/ClassAliasAutoloader.php#L57C13-L57C34

It takes the class names from Composer autoload file autoload_classmap.php:
- https://github.com/laravel/tinker/blob/102bfc19b79817022e9fb1d3dd235d43d42f1954/src/ClassAliasAutoloader.php#L104
- https://github.com/laravel/tinker/blob/102bfc19b79817022e9fb1d3dd235d43d42f1954/src/Console/TinkerCommand.php#L65

Then, compares by the class basename on the standard auto-load event:
- https://github.com/laravel/tinker/blob/102bfc19b79817022e9fb1d3dd235d43d42f1954/src/ClassAliasAutoloader.php#L84

And, if it matches, calls class_alias on:
- https://github.com/laravel/tinker/blob/102bfc19b79817022e9fb1d3dd235d43d42f1954/src/ClassAliasAutoloader.php#L109


Yet, it still interesting why exactly the alias and string do not match in service provider steps at this point, since it fails during a Tinker shell only, and the same exact app(BookRatingsService::class); succeeds in a normal PHP file.

martinbean's avatar

@artwork You should be registering container bindings in the register method. The clue is in the name.

As for the code, I’m having a hard time following it. Your interface seems completely redundant given that you use the class name (BookRatingsService) as the key, and also try and resolve it via the class name and not the interface name.

I personally register services in a service provider all the time and have no problem doing so. I don’t know why you’re using singletonIf and what difference it has over just singleton, but I register services like this:

public function register(): void
{
    $this->app->singleton(FooService::class, function () {
        return new FooService($foo, $bar, $baz);
    });
}

But I do notice that in your binding, you call a getCurrencyServices method, but then in show the definition for a getRatings method instead. So where is this getCurrencyServices method, and what does it look like?

Artwork's avatar

@martinbean , thank you! To clarify, when you try calling that app(FooService::class) with Tinker, do you have the same issue?

The reason I used boot and not register is that there are services that must be registered before this one like APIs with resolved and configured HTTP clients, and those have service providers which utilize the method register.

Please or to participate in this conversation.