When do you need to create interfaces in PHP/Laravel? Suppose you have a method with different implementations depending on some condition. In that case, it may be a candidate for an interface, with different classes implementing that interface method differently, depending on that condition. Let me show an example.
Let's look at the question from the Laracasts forum:
"Where I should put a simple code that needs to be used in a few places. This code just fetched certain $data about the user based on the environment"
$env = config('app.env');if ($env === 'test') { $data = // get from one place;} else if ($env === 'prod') { $data = // get from another place}
Or, let's try to transform it to a more realistic example: you want to load the list of cities into your application, but the production environment should work with real city names. In contrast, other environments would work with a "fake" list.
if (app()->isProduction()) { $cities = City::all()->toArray();} else { $cities = config('app.fake_cities');}
You could solve it in various ways, but it sounds like one method to get the data from different sources. And the main thing is that the source is decided not by some method parameter but by the global environment.
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 { // This means .env file APP_ENV=production 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.
I've also seen different implementation 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.
You can find more examples of interfaces and different patterns of their implementations in my 2-hour course SOLID Code in Laravel
After all, we got back to the same ugly if .. else but placed somewhere else, moreover Service Provider is now coupled with environment on which we run app - which is in contradiction to SOLID principle.
Intoducing CitySelectionStrategyService that would use simple match(app()->isProduction()) in it's contstructor, to pick one of the strategies (RealCityService or FakeCityService) is the way to go for above example.
All 3: CitySelectionStrategyService, RealCityService and FakeCityService should implement same interface, Service provider should resolve abstract to CitySelectionStrategyService.
Doing it this way code follows SOLID principle.
https://refactoring.guru/design-patterns/strategy/php/example
Thanks for the valuable comment. Yes I agree that Strategy pattern is another good solution, but in my personal experience, it is more complicated to understand than using the more-or-less standard Laravel way that I described and I saw many developers using.
What you're talking about is more PHP and framework agnostic way.
Developers have a choice which to use, both are ok, in my opinion.
@jakub Can you elaborate on why using env() or config() at Service Provider is not following SOLID? Thanks.
@quyle92
ServiceContainer is a pattern that has just one responsibility - (when is it asked) it resolves an abstract implmentation to a concrete implementation. It's behavior should not vary on any conditions and only possible mapping should A to B, but not A to B but sometimes C and on 2nd Tuesday of a month D.
Pattern that returns instances of different objects depending on some codition is Strategy, and above is mix of container and strategy.