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\Providers;
use App\Services\BookRatingsService;
use App\Services\Interfaces\BookRatingsApiInterface;
use Exception;
use Illuminate\Support\ServiceProvider;
class BookRatingsServiceProvider extends ServiceProvider
{
// The same issue happens with both method `register()` and `singleton()` used instead, regardless.
public function boot(): void
{
$this->app->singletonIf(BookRatingsService::class, function ($app) {
$ratings = $this->getCurrencyServices($app);
return new BookRatingsService($ratings);
});
}
private function getRatings($app): array
{
$currencyServices = config('currencies.genres');
$services = collect($currencyServices)->map(function (array $genres, string $serviceClass) {
dump([
'serviceClass' => $serviceClass,
'genres' => $genres,
]);
$bookApi = app()->make($serviceClass);
if (!$bookApi instanceof BookRatingsApiInterface) {
throw new Exception(
sprintf(
'Book ratings API \'%s\' does not implement interface: \'%s\'.',
$serviceClass,
BookRatingsApiInterface::class
)
);
}
return $bookApi->getGenreRatings($genres);
});
return $services->toArray();
}
}
<?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