I'm trying to understand dependency injection, and I in theory, I get it, but, I wanted to make an example just to help me. However, I'm getting the following error
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function Main\Services\UserService::__construct(), 0 passed
in ...
Here's my "main" file, I've called it index.php
<?php
#index.php
require_once 'vendor/autoload.php';
use Main\Controllers\UserController;
use Main\Services\UserService;
use Main\Models\UserModel;
use Pimple\Container;
$container = new Container;
$container['UserModel'] = function($c) {
return new UserModel();
};
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
$container['UserController'] = function ($c) {
echo "creating a new UserController\n";
$aUserService = $c['UserService'];
return new UserController($aUserService);
};
$myUserService = new $container['UserService'];
$myResult = $myUserService->parseGet();
echo $myResult, PHP_EOL;
Here's the model that's being passed into the service
<?php
# Models/UserModel.php
namespace Main\Models;
class UserModel
{
private $record;
public function getRecord()
{
return [
'first_name' => 'Bob',
'last_name' => 'Jones',
'email' => 'bj#example.com',
'date_joined' => '11-12-2014',
];
}
}
And, here's the service, which takes the model as it's constructor argument
<?php
namespace Main\Services;
use Main\Models\UserModel;
class UserService
{
private $userModel;
public function __construct(UserModel $userModel)
{
echo "verifying that the userModel passed in was a valid UserModel\n";
$this->userModel = $userModel;
print_r($this->userModel->getRecord());
}
public function parseGet()
{
$retVal = $this->userModel->getRecord();
return json_encode($retVal);
}
}
So, in theory, Pimple should be able to instantiate a UserService object. I even verified that the UserModel passed into the UserService class is a valid UserModel object (which it is as is evident that it prints out an array)
What am I missing? Is there something that I have failed to account for?
Oh, here's the composer.json file
{
"require": {
"pimple/pimple": "~3.0"
},
"autoload": {
"psr-4": {
"Main\\" : "./"
}
}
}
I made a gitHub link so the project can be checked out and ran without having to copy past everything (https://github.com/gitKearney/pimple-example)
SOLUTION
The problem was I have an extra new in the line
$myUserService = new $container['UserService'];
It was so obvious, I couldn't see it
$container['UserService'] already is a UserService object. check yer service definition:
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
That sets $container['UserService'] to be return new UserService($c['UserModel']) when it's invoked, right?
Your code is basically going:
$o1 = new UserService($c['UserModel']);
$o2 = new $o2;
You use dependency injection containers to free yourself form the pain of manipulating objects' dependencies. Creating a new UserService is not necessary (if it really is a service). In that case, you define it in your $container once, and use it whenever you need to.
So instead of creating a new UserService object and calling its method parseGet() (what you did in your code) you can do something like this:
$myResult = $container['UserService']->parseGet();
When you define something like:
$container['UserService'] = function ($c) {
return new UserService($c['UserModel']);
};
you are telling Pimple how to handle creation of a UserService once you try to access $container['UserService']
That's why you define dependencies as functions.
This might be related to your question Why use a closure for assignment instead of directly assigning a value to a key?
Related
We are preparing to move to php 8.0.15 once some third party libraries we require are ready for it.
Our centralized setUp() function for unit tests handles constructorArg population for our class mocks.
Using phpunit v9.5.14 currently, we get failed tests with the response Error : Unknown named parameter $User
We are not using named parameters in our codebase anywhere that we are aware of.
if (empty($this->constructorArgs)) {
$this->constructorArgs = array('User');
}
if (!empty($this->constructorArgs) && is_array($this->constructorArgs)) {
foreach ($this->constructorArgs as $classname) {
if (is_array($classname)) {
$args[key($classname)] = current($classname);
$classname = key($classname);
} else {
if ($classname == "Twig" || $classname == "Twig\Environment") {
$args[$classname] = TwigFactory::mockTwig();
} else {
$args[$classname] = $this->getMockBuilder($classname)->disableOriginalConstructor()->getMock();
}
}
$container->set($classname, $args[$classname]);
}
}
$this->mock = $this->getMockBuilder($this->class)
->setMethods($this->methods)
->setConstructorArgs($args)
->getMock(); <-- Error states this line, unfortunately no stack trace
The constructorArgs are populated into the setup like this:
$this->constructorArgs = array('User','AnotherClass', 'YetAnother');
We've tried everything we can think of, thinking that maybe it was something to do with the casing of the variable name in the class construct, i.e. "User $user" but so far nothing has resolved this.
Before we begin, let's create a minimal, reproducible example:
class User {}
class Example {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock();
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs($args)
->getMock();
$this->assertTrue(true);
}
}
Running with PHP 7.4 and PHPUnit 9.5.14, this passes; with PHP 8.0 and the same library, it gives the error you report:
Error: Unknown named parameter $User
Actually, we can simplify a bit further: rather than $args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock(); we can just say $args['User'] = new User; and get the same error.
Now, let's look at what we're doing:
We create an associative array, mapping class names to (mock) objects
We pass that associative array to a Mock Builder's setConstructorArgs method
Some magic happens...
So, what does happen? Maybe the source of PHPUnit will give some clues.
Well, setConstructorArgs just sets a property, which is used in getMock, then passed through a bunch of different methods; eventually, it ends up passed to MockObject\Generator::getObject, which if we strip out some error handling, does this:
$class = new ReflectionClass($className);
$object = $class->newInstanceArgs($arguments);
So, let's see if we can use that to make an even more minimal example of our problem:
class User {}
class Example {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}
$class = new ReflectionClass(Example::class);
$object = $class->newInstanceArgs(['User' => new User]);
Since this is self-contained code, we can use the handy online tool at https://3v4l.org to compare output in different PHP versions: https://3v4l.org/QU4jS
As expected, PHP 7.4 is happy with it, PHP 8.0 and above give an error:
Fatal error: Uncaught Error: Unknown named parameter $User in /in/QU4jS:14
Stack trace:
#0 /in/QU4jS(14): ReflectionClass->newInstanceArgs(Array)
#1 {main}
thrown in /in/QU4jS on line 14
So, what is going on here? Well, the manual page for ReflectionClass::newInstanceArgs doesn't (currently) say much about what the provided array should look like, or named argument support, but we can take an educated guess: it's trying to match our associative array as named parameters to the constructor. Previous versions, since they had no named parameters, simply ignored the keys and applied the parameters in order.
We can test this theory easily enough by making a class with two parameters to its constructor:
class Example2 {
public function __construct($first, $second) {
echo "$first then $second\n";
}
}
$class = new ReflectionClass(Example2::class);
$object = $class->newInstanceArgs(['second' => 'two', 'first' => 'one']);
When run on multiple versions we can see that older versions of PHP output "two then one", based on the order of the array; and newer versions output "one then two", based on the keys of the array.
So, to cut a long story short, what's the fix? Quite simply, don't use keys in the array of constructor parameters:
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args[] = new User;
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs($args)
->getMock();
$this->assertTrue(true);
}
}
If you need to use them during the setup logic to keep track of things, just use array_values to discard them when you pass them in:
class ExampleTest extends PHPUnit\Framework\TestCase {
public function testExample() {
$args = [];
$args['User'] = new User;
$mock = $this->getMockBuilder(Example::class)
->setConstructorArgs(array_values($args))
->getMock();
$this->assertTrue(true);
}
}
I have a class with a private variable used to store an object.
I have a function that checks first if that variable already contains an object or not; if not, it instantiates the needed object and sets it to that variable, otherwise it just returns the content of that variable.
I was wondering if the getSessionCustomer() here is an overkill/unnecessary or if it has real benefits. I simply based this on the Album tutorial by Zend, but I haven't been able to fully test it out yet to really see the advantages (or disadvantages). As far as I know it wasn't explained in the docs why this additional function was included.
class JobController extends AbstractActionController
{
private $SessionCustomer;
public function saveJobAction()
{
$SessionCustomer = $this->getSessionCustomer();
if(empty($SessionCustomer->offsetGet('customer_id'))) {
return $this->redirect()->toRoute('login');
} else {
$JobService = $this->getServiceLocator()->get('Job\Factory\JobServiceFactory');
$job_id = $JobService->saveJob();
return $this->redirect()->toUrl('/job/' . $job_id);
}
}
public function viewJobAction()
{
$sm = $this->getServiceLocator();
$SessionCustomer = $this->getSessionCustomer();
if(empty($SessionCustomer->offsetGet('customer_id'))) {
return $this->redirect()->toRoute('login');
} else {
$JobTable = $sm->get('Job\Model\JobTable');
$JobItemTable = $sm->get('Job\Model\JobItemTable');
$jobId = $this->params()->fromRoute('id');
$Job = $JobTable->getJobById($jobId);
$JobItems = $JobItemTable->getJobItemsByJobId($jobId);
$this->layout()->setVariable('title', 'Order #' . $jobId);
$viewModel = new ViewModel();
$viewModel->setVariables(array(
'Job' => $Job,
'JobItems' => $JobItems
));
return $viewModel;
}
}
private function getSessionCustomer()
{
if(!$this->SessionCustomer) {
$this->SessionCustomer = $this->getServiceLocator()->get('Session\Customer');
}
return $this->SessionCustomer;
}
}
I don't think its an overkill, but I usually avoid calling getServiceLocator() in Controllers.
What you are asking about is basically making sure that the controller's dependency requirement is met. You can use a Factory for the same purpose and do this more sophisticated way. You can create a factory and inject the dependencies directly into the controller. This you will never make a call to the non-object variables.
For that you will be required to create a class that implements a FactoryInterface which will have a method createService which will provide you with ServiceLocator. You can use that serviceLocator to get all the dependencies and inject them directly into your Class.
I'm trying to write a test for a method in the class below. However, when I run the test I get the error that get_b64 is never run? I don't see how this is not running.
I've had a little look into the mockery documentation for testing static methods, but as far as I can tell this error isn't due to that?
What do I need to change with my testing strategy or be able to mock the function call in the mocked object?
Class:
namespace App\Services\Steam;
use App\Services\Steam\Utils;
class Steam
{
public function profile(string $steamID)
{
$b64 = Utils::get_b64($steamID);
if ($b64 === null) {
throw new \App\Exceptions\InvalidSteamId();
}
return new Profile($b64);
}
}
TestCase:
public function test_create_user_object()
{
$id = "123"
$utilsMock = Mockery::mock(\App\Services\Steam\Utils::class);
$utilsMock->shouldReceive('get_b64')
->once()
->with($id)
->andReturn($id);
$steam = new \App\Services\Steam\Steam();
$steam->profile($id);
}
You call get_b64 statically, which means it is called from the class, not an object.
To mock such calls you need to use aliases:
public function test_create_user_object()
{
$id = "123"
$utilsMock = Mockery::mock('alias:\App\Services\Steam\Utils');
$utilsMock->shouldReceive('get_b64')
->once()
->with($id)
->andReturn($id);
$steam = new \App\Services\Steam\Steam();
$steam->profile($id);
}
Bear in mind that it completely replaces the Utils class, so if you have more static functions called from the class, you need to mock them as well.
I am trying to write unit test for my application. which as logging the information functionality.
To start with i have service called LogInfo, this how my class look like
use Zend\Log\Logger;
class LogInfo {
$logger = new Logger;
return $logger;
}
I have another class which will process data. which is below.
class Processor
{
public $log;
public function processData($file)
{
$this->log = $this->getLoggerObj('data');
$this->log->info("Received File");
}
public function getLoggerObj($logType)
{
return $this->getServiceLocator()->get('Processor\Service\LogInfo')->logger($logType);
}
}
here i am calling service Loginfo and using it and writing information in a file.
now i need to write phpunit for class Processor
below is my unit test cases
class ProcessorTest{
public function setUp() {
$mockLog = $this->getMockBuilder('FileProcessor\Service\LogInfo', array('logger'))->disableOriginalConstructor()->getMock();
$mockLogger = $this->getMockBuilder('Zend\Log\Logger', array('info'))->disableOriginalConstructor()->getMock();
$serviceManager = new ServiceManager();
$serviceManager->setService('FileProcessor\Service\LogInfo', $mockLog);
$serviceManager->setService('Zend\Log\Logger', $mockLogger);
$this->fileProcessor = new Processor();
$this->fileProcessor->setServiceLocator($serviceManager);
}
public function testProcess() {
$data = 'I have data here';
$this->fileProcessor->processData($data);
}
}
I try to run it, i am getting an error "......PHP Fatal error: Call to a member function info() on a non-object in"
i am not sure , how can i mock Zend logger and pass it to class.
Lets check out some of your code first, starting with the actual test class ProcessorTest. This class constructs a new ServiceManager(). This means you are going to have to do this in every test class, which is not efficient (DRY). I would suggest constructing the ServiceMananger like the Zend Framework 2 documentation describes in the headline Bootstrapping your tests. The following code is the method we are interested in.
public static function getServiceManager()
{
return static::$serviceManager;
}
Using this approach makes it possible to obtain the instance of ServiceManager through Bootstrap::getServiceManager(). Lets refactor the test class using this method.
class ProcessorTest
{
protected $serviceManager;
protected $fileProcessor;
public function setUp()
{
$this->serviceManager = Bootstrap::getServiceManager();
$this->serviceManager->setAllowOverride(true);
$fileProcessor = new Processor();
$fileProcessor->setServiceLocator($this->serviceManager);
$this->fileProcessor = $fileProcessor;
}
public function testProcess()
{
$mockLog = $this->getMockBuilder('FileProcessor\Service\LogInfo', array('logger'))
->disableOriginalConstructor()
->getMock();
$mockLogger = $this->getMockBuilder('Zend\Log\Logger', array('info'))
->disableOriginalConstructor()
->getMock();
$serviceManager->setService('FileProcessor\Service\LogInfo', $mockLog);
$serviceManager->setService('Zend\Log\Logger', $mockLogger);
$data = 'I have data here';
$this->fileProcessor->processData($data);
}
}
This method also makes it possible to change expectations on the mock objects per test function. The Processor instance is constructed in ProcessorTest::setUp() which should be possible in this case.
Any way this does not solve your problem yet. I can see Processor::getLoggerObj() asks the ServiceManager for the service 'Processor\Service\LogInfo' but your test class does not set this instance anywhere. Make sure you set this service in your test class like the following example.
$this->serviceManager->setService('Processor\Service\LogInfo', $processor);
I am struggling to get dependency injection to work the way I expect -
I am trying to inject a class, Api, which needs to know which server to connect to for a particular user. This means that overriding constructor properties in a config file is useless, as each user may need to connect to a different server.
class MyController {
private $api;
public function __construct(Api $api) {
$this->api = $api;
}
}
class Api {
private $userServerIp;
public function __construct($serverip) {
$this->userServerIp = $serverip;
}
}
How can I inject this class with the correct parameters? Is it possible to override the definition somehow? Is there some way of getting the class by calling the container with parameters?
To (hopefully) clarify - I'm trying to call the container to instantiate an object, while passing to it the parameters that would otherwise be in a definition.
Since IP depends on the user you probably have some piece of logic that does the user=>serverIP mapping. It might be reading from the db or simple id-based sharding, or whatever. With that logic you can build ApiFactory service that creates Api for a particular user:
class ApiFactory {
private function getIp(User $user) {
// simple sharding between 2 servers based on user id
// in a real app this logic is probably more complex - so you will extract it into a separate class
$ips = ['api1.example.com', 'api2.example.com'];
$i = $user->id % 2;
return $ips[$i];
}
public function createForUser(User $user) {
return new Api($this->getIp($user);
}
}
Now instead of injecting Api into your controller you can inject ApiFactory (assuming your controller knows the user for which it needs the Api instance)
class MyController {
private $apiFactory;
public function __construct(ApiFactory $apiFactory) {
$this->apiFactory = $apiFactory;
}
public function someAction() {
$currentUser = ... // somehow get the user - might be provided by your framework, or might be injected as well
$api = $this->apiFactory->createForUser($currentUser);
$api->makeSomeCall();
}
}
I am not sure I understand your question fully, but you can configure your Api class like this:
return [
'Foo' => function () {
return new Api('127.0.0.1');
},
];
Have a look at the documentation for more examples or details: http://php-di.org/doc/php-definitions.html
Edit:
return [
'foo1' => function () {
return new Api('127.0.0.1');
},
'foo2' => function () {
return new Api('127.0.0.2');
},
];