I like and use the Yii framework, particularly its "components", which are lazily-instantiated and you can swap them in or out in your configuration file. Kind of like a dependency injection-lite.
I try to keep the business logic of my code completely independent of the Framework, in case I ever want to repurpose that code, or even change frameworks.
Let's say I have a class in my service layer called AccountService, which implements IAccountService and has a one-argument constructor.
interface IAccountService
{
function getUserById($id);
}
class AccountService implements IAccountService
{
private $_userRepository;
public function __construct(IUserRepository $userRepository) {
$this->_userRepository = $userRepository;
}
public function getUserById($id) {
return $this->_userRepository->getById($id);
}
}
Great. So far, it's totally framework-free. Now I'd like to expose this as a Yii component, so it can be lazily-instantiated and easily used by Yii controllers and other Yii components.
But Yii components (which implement IApplicationComponent) must have exactly zero constructor arguments, while my class requires one!
Any ideas?
Here's what I've got. I'm not really happy with any of them; they both look over-engineered and I'm detecting a distinct smell from them.
Option 1 - compose: I create a class called "AccountServiceComponent" which implements Yii's IApplicationComponent. It cannot extend my AccountService class, because of the constructor, but it could instantiate one as a private member and wrap all of its methods, like so:
class AccountServiceComponent implements IApplicationComponent, IAccountservice
{
private $_accountService;
public __construct() {
$this->_accountService = new AccountService(new UserRepository());
}
public getUserById($id) {
return $this->_accountService->getUserById($id);
}
}
Cons: I'll have to wrap every method like that, which is tedious and could lead to "baklava code." Especially considering that there'll be multiple service classes, each with multiple methods.
Option 2 - mixin: (Or behavior or trait or whatever it's called these days.)
Yii (having been written prior to PHP 5.4) offers "behaviors" in the form of a class which implements IBehavior. I could create a behavior class which extends my service, and attach it to a component:
class AccountServicesBehavior extends AccountService implements IBehavior
{
// Implement the few required methods here
}
class AccountServiceComponent implements IApplicationComponent
{
public function __construct() {
$accountService = new AccountService(new UserRepository());
$this->attachBehavior($accountService);
}
Cons: My component no longer officially implements IAccountService. Also seems to be getting excessive with the layering.
Option 3 - optional constructor parameters:
I could just make the constructor parameter to my service class optional, and then extend it into a component:
class AccountService implements IAccountService
{
public $userRepository;
public function __construct(IUserRepository $userRepository = null) {
$this->userRepository = $userRepository;
}
public function getUserById($id) {
return $this->_userRepository->getById($id);
}
}
class AccountServiceComponent extends AccountService implements IApplicationComponent
{
}
Cons: The optional constructor parameter means this class coudld now be instantiated without supplying it with everything it needs.
...so, any other options I'm missing? Or am I just going to have to choose the one that disturbs me the least?
Option 3 but with an object as the optional argument sounds best imo:
public function __construct(IUserRepository $userRepository = new UserRepository()) {
$this->userRepository = $userRepository;
}
Related
interface UserRepositoryInterface {
public function getUser($userType, $login): Builder;
}
class UserRepository implements UserRepositoryInterface {
public function getUser('clientA', 'user-login', EmailValidatorInterface $emailValidator, PhoneValidatorInterface $phoneValidator): Builder {}
}
Method UserRepository#getUser throws an error because he is not abiding by the contract UserRepositoryInterface#getUser. How can I remove an error, while using method injection?
There are several ways how to solve this kind of error. I 'm not a big fan of overriding the signature of an interface provided method. I prefer DI over the constructor of a class.
Dependency Injection with the constructor
This one makes more sence in my eyes, because it is cleaner. Before overriding interface method signatures you 'll always have to check if the additional parameters are used in more than that one method. If this is the case, just inject the dependency over the constructor.
<?php
declare(strict_types=1);
namespace Marcel;
class UserRepository implements UserRepositoryInterface
{
protected EmailValidatorInterface $emailValidator;
protected PhoneValidatorInterface $phoneValidator;
public function __construct(
EmailValidatorInterface $emailValidator,
PhoneValidatorInterface $phoneValidator
) {
$this->emailValidator = $emailValidator;
$this->phoneValidator = $phoneValidator;
}
public function getUser(string $userType, string $login): Builder
{
// $this->emailValidator and $this->phoneValidator are available
}
}
As you can see this one is exact what the interface implements. No additional parameters. Just clean and simple dependency injection. This type of injection needs a simple factory or something else, that initializes the dependencies.
Extending an interface implemented method
If you can not use dependency injection - for whatever reason - you can extend the method signature from the interface by providing a default value for each additional parameter.
<?php
declare(strict_types=1);
namespace Marcel;
class UserRepository implements UserRepositoryInterface
{
public function getUser(
string $userType,
string $login,
?EmailValidatorInterface $emailValidator = null,
?PhoneValidatorInterface $phoneValidator = null
): Builder
{
// $emailValidator and $phoneValidator are available
// just validate them against null
}
}
This is ugly af but it will work. What happens, when you check if a class implements the UserRepositoryInterface interface? It just secures, that there is a method getUser, which takes two parameters. The interface knows nothing about any other parameters. This is inconsistency in its purest form.
In most DI systems you’d solve this in the constructor instead.
interface UserRepositoryInterface {
public function getUser($userType, $login): Builder;
}
class UserRepository implements UserRepositoryInterface {
public function __construct(
public readonly EmailValidatorInterface $emailValidator,
public readonly PhoneValidatorInterface $phoneValidator
){}
public function getUser('clientA', 'user-login'): Builder {}
}
I have a Task class that extends an abstract class, TaskBase.
// class Task
class Task extends TaskBase {
/**
* #var Processor
*/
private $processor
public function __construct(Processor $processor)
{
$this->processor = $processor;
}
public function process() : ProcessResult
{
return $this->processor->execute();
}
}
// abstract class TaskBase
abstract class TaskBase implements CommanTask {
protected function getKey(): string
{
}
}
This TaskBase implements an interface CommanTask, which contains the following method.
interface CommanTask {
public function process(): ProcessResult;
}
Now I need a new task class TaskMultiple, and it's process() method needs to return an array of ProcessResult instead of one ProcessResult.
How can I extend abstract class TaskBase for this new TaskMultiple class?
If you implement an interface, you need to actually comply with the contract laid out by the interface.
The interface you propose says that process() returns a ProcessResult. If you could change that in an implementing class to returning an array, then consumers of the class wouldn't be able to trust the contract specified by the interface.
They would try to use the result of TaskMultiple::process(), and since they interface says it would return a ProcessResult, a fatal error would soon happen when it tried to treat it as such (e.g. by accessing a method for that class), and it wasn't that.
Your solutions are:
If you are on PHP 8, you could use union types:
interface Task {
public function process(): ProcessResult|iterable;
}
It would work, but it's ugly. Now consumers of the any Task implementing service would need to check on the result of process() to see if it's a single ProcessResult, or a collection (presumably of ProcessResults).
If you are on PHP < 8, you could make it work simply by removing the type hint:
interface Task {
public function process()
}
Now consumers would only know that there is a process() method, but you'd have no type information at all. Uglier still.
Better, simply create different interfaces like Task and MultipleTask:
interface Task {
public function process(): ProcessResult;
}
interface MultipleTask {
public function process(): iterable;
}
Since your BaseTask includes nothing to help implement the class (only includes an abstract method, making it already very similar to an interface), move the implements to each of the concrete task classes:
class ExampleTask extends BaseTask implements Task
{ /** implementation **/ }
class ExampleMultipleTask extends BaseTask implements MultipleTask
{ /** implementation **/ }
I have a factory class that creates different objects and I have a lot of loggers defined for each type of object.
And I want to get all loggers collection in my factory. Before I just used a ContainerInterface in my factory constructor, but since Symfony 5.1 container autowiring is deprecated.
Now I can not find a way to get a collection of loggers. I tried to use
!tagged_iterator { tag: 'monolog.logger' }
and also tried to set a tag for LoggerInterface and get a tagged_iterator for it, but it didn't work. I suggest that it is because loggers are not real classes.
This might be overkill but as you say the logger services are a bit unusual. Seems like they should be tagged or be taggable by LoggerInterface but I ran some test and could not get it to work. Here is a brute force approach which relies on logger services having ids of monolog.logger.name:
# Start with a service locator class
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class LoggerLocator extends ServiceLocator
{
// Just to specify the return type to keep IDEs happy
public function get($id) : LoggerInterface
{
return parent::get($id);
}
}
...
# now make the kernel into a compiler pass
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
...
public function process(ContainerBuilder $container)
{
$loggerServices = [];
foreach($container->getServiceIds() as $id) {
if (!strncmp($id,'monolog.logger.',15)) {
//echo 'Logger ' . $id . "\n";
$loggerServices[$id] = new Reference($id);
}
}
$loggerLocator = $container->getDefinition(LoggerLocator::class);
$loggerLocator->setArguments([$loggerServices]);
}
# and now we can inject the locator where it is needed
class SomeController {
public function index(Request $request, LoggerLocator $loggerLocator)
{
dump($loggerLocator);
$logger = $loggerLocator->get('monolog.logger.' . $name);
Seems like there should be a way to do this just through configuring but a pass is easy enough I guess.
I recently watched this video and wanted to change my Laravel controllers so that they had their dependencies managed with Laravel's IoC container. The video talks about creating an interface for a Model and then implementing that interface for the specific data source used.
My question is: when implementing the interface with a class that extends Eloquent and binding that class to the controller so that it is accessible from $this->model, should I also create interfaces and implementations for the Eloquent models which may be returned when calling methods such as $this->model->find($id)? Should there be different classes for the Model and the ModelRepository?
Put it another way: how do I do new Model when my model is in $this->model.
Generally, yes, people doing that pattern (the repository pattern) have an interface which have some methods defined that your app will use:
interface SomethingInterface {
public function find($id);
public function all();
public function paged($offset, $limit);
}
Then you create an implementation of this. If you're using Eloquent, then you can make an Eloquent implementation
use Illuminate\Database\Model;
class EloquentSomething {
protected $something;
public function __construct(Model $something)
{
$this->something = $something;
}
public function find($id)
{
return $this->something->find($id);
}
public function all() { ... }
public function paged($offset, $limit) { ... }
}
Then you make a service provider to put it all together, and add it into app/config/app.php.
use Something; // Eloquent Model
use Namespace\Path\To\EloquentSomething;
use Illuminate\Support\ServiceProvider;
class RepoServiceProvider extends ServiceProvider {
public function register()
{
$app = $this->app;
$app->bind('Namespace/Path/To/SomethingInterface', function()
{
return new EloquentSomething( new Something );
});
}
}
Finally, your controller can use that interface as a type hint:
use Namespace/Path/To/SomethingInterface;
class SomethingController extends BaseController {
protected $something;
public function __construct(SomethingInterface $something)
{
$this->something = $something;
}
public function home() { return $this->something->paged(0, 10); }
}
That should be it. Apologies on any errors, this isn't tested, but is something I do a lot.
Downsides:
More code :D
Upsides:
Able to switch out implementations (instead of EloquentSomething, can use ArraySomething, MongoSomething, whatever), without changing your controller code or any code that uses an implementation of your interface.
Testable - you can mock your Eloquent class and test the repository, or mock your constructor dependency and test your controller
Re-usable - you can App::make() to get the concrete EloquentSomething anywhere in your app and re-use the Something repository anywhere in your code
Repository is a good place to add additional logic, like a layer of cacheing, or even validation rules. Stock mucking about in your controllers.
Finally:, since I likely typed all that out and STILL DIDN'T ANSWER YOUR QUESTION (wtf?!), you can get a new instance of the model using $this->model. Here's an example for creating a new Something:
// Interface:
public function create(array $data);
// EloquentSomething:
public function create(array $data)
{
$something = this->something->newInstance();
// Continue on with creation logic
}
Key is this method, newInstance().
I've used $newModel = $this->model and it's worked for me.
My Dispatcher is "choosing" correct Controller; then creating Controller's instance (DependencyInjectionContainer is passed to Controller constructor); then calling some Controller's method...
class UserController extends Controller
{
public function __construct(DependencyInjectionContainer $injection) {
$this->container = $injection;
}
public function detailsAction() {
...
}
}
DependencyInjectionContainer contains DB adapter object, Config object etc.
Now let's see what detailsAction() method contains...
public function detailsAction() {
$model = new UserModel();
$model->getDetails(12345);
}
As you see I'm creating new instance of UserModel and calling getDetails methods.
Model's getDetails() method should connect to db to get information about user. To connect to DB UserModel should be able to access DB adapter.
What is the right way to pass DependencyInjectionContainer to the UserModel?
I think that this way is wrong...
public function detailsAction() {
$model = new UserModel($this->container);
$model->getDetails(12345);
}
Instead of injecting the entire DI Container into your classes, you should inject only the dependencies you need.
Your UserController requires a DB Adapter (let's call this interface IDBAdapter). In C# this might look like this:
public class UserController
{
private readonly IDBAdapter db;
public UserController(IDBAdapter db)
{
if (db == null)
{
throw new ArgumentNullException("db");
}
this.db = db;
}
public void DetailsAction()
{
var model = new UserModel(this.db);
model.GetDetails(12345);
}
}
In this case we are injectiing the dependency into the UserModel. In most cases, however, I would tend to consider it a DI smell if the UserController only takes a dependency to pass it on, so a better approach might be for the UserController to take a dependency on an Abstract Factory like this one:
public interface IUserModelFactory
{
UserModel Create();
}
In this variation, the UserController might look like this:
public class UserController
{
private readonly IUserModelFactory factory;
public UserController(IUserModelFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException("factory");
}
this.factory = factory;
}
public void DetailsAction()
{
var model = this.factory.Create();
model.GetDetails(12345);
}
}
and you could define a concrete UserModelFactory that takes a dependency on IDBAdapter:
public class UserModelFactory : IUserModelFactory
{
private readonly IDBAdapter db;
public UserModelFactory(IDBAdapter db)
{
if (db == null)
{
throw new ArgumentNullException("db");
}
this.db = db;
}
public UserModel Create()
{
return new UserModel(this.db);
}
}
This gives you better separation of concerns.
If you need more than one dependency, you just inject them through the constructor. When you start to get too many, it's a sign that you are violating the Single Responsibility Principle, and it's time to refactor to Aggregate Services.
I'd use a singleton object for all config parameters :
You set it up in your bootstrap, then choose to use it directly or pass it as parameter in your objects.
The idea being to have one method all around to retrieve your config data.
You may then provide an abstract class for db manipulation which uses your config. singleton.
DependancyInjection can still be used to override your default data.
The above link in the comment (possible 'duplicate') concludes on using constructor injection : this is close to your current method.
However if I try to figure how your model works, I guess you will have many other model classes other than "userModel". Thus an abstract class using a config singleton might be a good solution : all your next model classes will just extend this abstract class, and you don't have to worry about your config.
On the other hand, your solution is good to me as long as your dependanceInjectionContainer changes often.