@Ligonsker ok I decided to spent some more time and create a full example to write on my blog. So here's the code, I hope it will give you full understanding.
One of the solutions is this:
- Create an interface with a method
getList() that should return the array
- Create two Service classes with different logic for that method: one for "real data" and one for "fake data".
- When calling that Service from Controller, you call the interface everywhere instead.
- In the AppServiceProvider, you resolve the Interface with one of the Service classes, depending on the environment.
Let's see it in action, in the code.
Step 1. Create Interface
app/Interfaces/CityListInterface.php:
namespace App\Interfaces;
interface CityListInterface {
public function getList(): array;
}
Step 2. Create Two Service Classes
Both classes should implement that interface which requires them to have a method getList() with identical parameters and return types.
app/Services/RealCityService.php:
namespace App\Services;
use App\Interfaces\CityListInterface;
use App\Models\City;
class RealCityService implements CityListInterface {
public function getList(): array
{
return City::all()->toArray();
}
}
app/Services/FakeCityService.php:
namespace App\Services;
use App\Interfaces\CityListInterface;
class FakeCityService implements CityListInterface {
public function getList(): array
{
return config('app.fake_cities');
}
}
Step 3. In Controller, Type-Hint the Interface
Whenever you need to get the list of cities in Controller or elsewhere, type-hint the Interface, not a specific service.
If you need that in a particular method, do this:
use App\Interfaces\CityListInterface;
class RestaurantController extends Controller
{
public function create(CityListInterface $cityList)
{
$cities = $cityList->getList();
If you need to use the service in multiple methods of the class, use it in Constructor:
use App\Interfaces\CityListInterface;
class RestaurantController extends Controller
{
public function __construct(public CityListInterface $cityList) { }
public function create()
{
$cities = $this->cityList->getList();
// ...
}
public function edit(Restaurant $restaurant)
{
$cities = $this->cityList->getList();
// ...
}
Step 4. Resolve Interface with Service
In the AppServiceProvider or any ServiceProvider (Laravel default one or your custom one), you need to add this into the register() method.
app/Providers/AppServiceProvider.php:
namespace App\Providers;
use App\Interfaces\CityListInterface;
use App\Services\FakeCityService;
use App\Services\RealCityService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
if (app()->isProduction()) {
$this->app->singleton(CityListInterface::class, RealCityService::class);
} else {
$this->app->singleton(CityListInterface::class, FakeCityService::class);
}
}
Here you have the logic that creates a specific object of your specific Service class for all your application based on a global environment. So, you define it once here and "forget" it.
Back to the @martinbean point about singleton/bind, I dug deeper and indeed, singleton is a better approach. I've also seen different syntax using boot() method of the ServiceProvider, and bind() instead of singleton():
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
if (app()->isProduction()) {
$this->app->bind(CityListInterface::class, RealCityService::class);
} else {
$this->app->bind(CityListInterface::class, FakeCityService::class);
}
}
While practically you wouldn't notice much difference, this bind() method would create a new Service object whenever called. The singleton() way creates the object once and reuses it, saving memory.