I try writing functional tests for my controllers using Codeception testing framework. I want to replace real service in DI with fake one.
Controller code example:
<?php
namespace App\Controllers;
class IndexController extends ControllerBase
{
public function indexAction()
{
// some logic here
$service = $this->getDI()->get('myService');
$service->doSomething();
// some logic here
}
}
Test code example:
<?php
namespace App\Functional;
class IndexControllerCest
{
public function testIndexAction(FunctionalTester $I)
{
// Here i want to mock myService, replace real object that in controller with fake one
$I->amOnRoute('index.route');
}
}
I already try different combinations with Codeception Phalcon module like addServiceToContainer.
I setup Codeception using bootstrap.php file almost the same as for real app.
Phalcon version: 3.4.1
Codeception version: 3.1
So my question in last code fragment on comment section. Thank you for any help.
I would like suggest you start from creating a separated helpers to create and inject dependencies as follows:
# functional.suite.yml
class_name: FunctionalTester
modules:
enabled:
- Helper\MyService
- Phalcon:
part: services
# path to the bootstrap
bootstrap: 'app/config/bootstrap.php'
# Another modules ...
Create a separated service:
<?php
namespace Helper;
use Codeception\Module;
/** #var \Codeception\Module\Phalcon */
protected $phalcon;
class MyService extends Module
{
public function _initialize()
{
$this->phalcon = $this->getModule('Phalcon');
}
public function haveMyServiceInDi()
{
$this->phalcon->addServiceToContainer(
'myService',
['className' => '\My\Awesome\Service']
);
}
}
And use it in tests as follows:
<?php
namespace App\Functional;
use Helper\MyService;
class IndexControllerCest
{
/** #var MyService */
protected $myService;
protected function _inject(MyService $myService)
{
$this->myService = $myService;
}
public function testIndexAction(FunctionalTester $I)
{
$I->wantTo(
'mock myService, replace real object that in controller with fake one'
);
$this->myService->haveMyServiceInDi();
$I->amOnRoute('index.route');
}
}
Related
I can't understand how the Dependency Injection works for custom services in SF 4.3 and PHP 7.2
In a controller, this simple code dumps an object Logger correctly initialized :
use Psr\Log\LoggerInterface;
/**
* #Route("/mytest", name="default_mytest")
*/
public function MyTestLoggerAction(LoggerInterface $logger) {
dump($logger);
return $this->render('default/index.html.twig');
}
But in a custom service called Guards in Guards.php, $logger is a null value :
namespace App\Workflow\CompanyDeploying\Transitions\Guards;
use Psr\Log\LoggerInterface;
class Guards {
private $logger;
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}
public function isValid() {
dump($this->logger);
}
}
i tried with :
using LoggerAwareTrait but nothing more happen, $logger always null.
adding #required on getLogger() and setting public to true in services.yml, $logger always null.
using public function isValid(LoggerInterface $logger) but all code who is asking for this isValid method returns "Too few arguments to function isValid()"
using a __contruct(LoggerInterface $logger) but anywhere i need this class, the code returns "Too few arguments to function __construct()"
First Edit
my services.yaml
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\EventListener\CompanyIndexer:
tags:
- { name: doctrine.event_listener, event: prePersist }
I tried to force autowire, autoconfig and visibility in services.yaml
App\Workflow\CompanyDeploying\Transitions\Guards\Guards:
autowire: true
autoconfigure: true
public: true
and add in my Guards.php
private $logger;
/**
* #required
*/
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}
public function isValid() {
dump($this->logger);
}
But no success. I always dump a null value.
2nd Edit
I call the Guard service from an EventSubscriberInterface who are listening to Wokflow event :
public static function getSubscribedEvents()
{
return [
'workflow.company_deploying.enter.mystate' => 'onEnter',
'workflow.company_deploying.leave.mystate' => 'onLeave',
'workflow.company_deploying.guard.mystate' => 'guardMyTransition',
];
}
public function guardMyTransition(Event $event) {
$this->event = $event;
if (! $this->guardFactory(__FUNCTION__)->isValid()) {
$event->setBlocked(true);
}
}
protected function guardFactory($guardName) {
$guard = GuardsFactory::create($guardName);
$guard->setCompany($this->event->getSubject());
if (isset($this->entityManager)) $guard->setEntityManager($this->entityManager);
if (isset($this->previousState)) $guard->setPreviousState($this->previousState);
return $guard;
}
My GuardFactory initialize a sub-class of Guards.
In the var/cache, i have a getGuardService.php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the public 'App\Workflow\CompanyDeploying\Transitions\Guards\Guards' shared autowired service.
include_once $this->targetDirs[3].'/src/Workflow/CompanyDeploying/Transitions/Guards/Guards.php';
$this->services['App\\Workflow\\CompanyDeploying\\Transitions\\Guards\\Guards'] = $instance = new \App\Workflow\CompanyDeploying\Transitions\Guards\Guards();
$instance->setLogger(($this->privates['monolog.logger'] ?? $this->getMonolog_LoggerService()));
return $instance;
I just need to easily use $logger (or any other service) in every class i need without writing ton of codes with a lot of setters.
Thanks for you help.
Solution
Dependency Injection doesn't work with this kind of factory call.
I have a problem with Symfony DependencyInjection Component. I want to inject interfaces into controllers, so I could only use interface methods. But, I notice I can use any public method from class that implement the interface and this is wrong. I follow the great article: http://php-and-symfony.matthiasnoback.nl/2014/05/inject-a-repository-instead-of-an-entity-manager/
Write the test service class and interface
interface ITestService
{
public function interfaceFunction();
}
class TestService implements ITestService
{
public function interfaceFunction() {/* do somenthing */}
public function classFunction() {/*do somenthing*/}
}
Configure my application service class as a service (test_service)
# file: app/config/services.yml
test_service:
class: MyApp\Application\Services\TestService
Configure my controller as a service:
# file: app/config/services.yml
test_controller:
class: MyApp\AppBundle\Controller\TestController
arguments:
- '#test_service'
Using service in controller
class TestController extends Controller
{
private testService;
function _construct(ITestService $testService)
{
$this->testService = $testService;
}
public function indexAction()
{
// This should be inaccesible but it works :(
$this->testService->classFunction();
// This is the only function I should use.
$this->testService->interfaceFunction();
}
As #Timurib says, this is because despite having Type Hintings, PHP doesn't evaluate the methods to call until runtime. This could be seen as something undesirable, but it allows to use some technics such as Duck Typing.
Here you have a simplified example based on the one you're providing (it doesn't put the Symfony Container into the mix, because this is something purely related to PHP). You can run it on 3v4l.org:
interface IService
{
public function interfaceFunction();
}
final class ServiceWithOtherFunction implements IService
{
public function interfaceFunction() { echo "ServiceWithOtherFunction interfaceFunction\n"; }
public function otherFunction() { echo "ServiceWithOtherFunction otherFunction\n"; }
}
final class Controller
{
private $service;
public function __construct(IService $service)
{
$this->service = $service;
}
public function indexAction()
{
$this->service->interfaceFunction();
$this->service->otherFunction();
}
}
$controllerWithOtherFunction = new Controller(new ServiceWithOtherFunction);
$controllerWithOtherFunction->indexAction();
Output:
ServiceWithOtherFunction interfaceFunction
ServiceWithOtherFunction otherFunction
But when we inject another implementation that does not contains the otherFunction, the code throws an Error at runtime:
final class ServiceWithoutOtherFunction implements IService
{
public function interfaceFunction() { echo "ServiceWithoutOtherFunction interfaceFunction\n"; }
}
$controllerWithoutOtherFunction = new Controller(new ServiceWithoutOtherFunction);
$controllerWithoutOtherFunction->indexAction();
Output:
ServiceWithoutOtherFunction interfaceFunction
Fatal error: Uncaught Error: Call to undefined method ServiceWithoutOtherFunction::otherFunction() in /in/mZcRq:28
Stack trace:
#0 /in/mZcRq(43): Controller->indexAction()
#1 {main}
thrown in /in/mZcRq on line 28
Process exited with code 255.
If you're going towards the use of interfaces, DI, and DIC, you should not call any public method rather than the exposed by the interface. This is the only way to really take advantadge of the benefits of having an interface: Decoupling from the implementation details, and be able to change the class to be injected without changing anything inside your Controller.
I am writing a new ZF2 app. I have noticed that ServiceLocator usage pattern of calling services "from anywhere" has been deprecated from ZF3. I want to write code in mind for ZF3.
I was able to set up my Controller to call all dependencies at constructor time. But that means loading i.e. Doctrine object upfront before I need it.
Question
How do I set it up so that it is only loaded when I need it immediately? (lazy-loaded). I understand that ZF3 moves loading to Controller construction, which makes it not apparent as to how to load something Just-In-Time.
Old Code
class CommissionRepository
{
protected $em;
function getRepository()
{
//Initialize Doctrine ONLY when getRepository is called
//it is not always called, and Doctrine is not always set up
if (! $this->em)
$this->em = $this->serviceLocator->get('doctrine');
return $this->em;
}
}
Current Code after Refactor of ServiceLocator pattern
class CommissionRepository
{
protected $em;
function getRepository()
{
return $this->em;
}
function setRepository($em)
{
$this->em = $em;
}
function useRepository($id)
{
return $this->em->find($id);
}
}
class CommissionControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $controllerManager->getServiceLocator();
// set up repository
$repository = new CommissionRepository();
$repository->setRepository($parentLocator->get('doctrine'));
// set up controller
$controller = new CommissionController($repository);
$controller->setRepository();
return $controller;
}
}
class CommissionController extends AbstractActionController
{
protected $repository;
public function setRepository(CommissionRepository $repository)
{
$this->repository = $repository;
}
public function indexAction()
{
//$this->repository already contains Doctrine but it should not
//I want it to be initialized upon use. How?
//Recall that it has been set up during Repository construction time
//and I cannot call it from "anywhere" any more in ZF3
//is there a lazy loading solution to this?
$this->repository->useRepository();
}
If you don't have any valid/strong reason to instantiate a custom entity repository, you should prefer extending of Doctrine\ORM\EntityRepository in your repositories like CommissionRepository. For example;
use Doctrine\ORM\EntityRepository;
class CommissionRepository extends EntityRepository
{
// No need to think about $em here. It will be automatically
// injected by doctrine when you call getRepository().
//
function fetchCommissionById($id)
{
// You can easily get the object manager directly (_em) or
// using getEntityManager() accessor method in a repository
return $this->_em->find($id);
}
}
By this way, entity manager will be automatically injected to the repository on construction when you call the $em->getRepository('App\Entity\Commission') method.
I assume that you already have a Commission entity in your app's Entity namespace:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repo\CommissionRepository")
* #ORM\Table
*/
class Commission
{
}
Then you can simplify the injecting process of the repository in your factory something like:
// ZF2 Way
class CommissionControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $services)
{
$em = $services->getServiceLocator()->get('doctrine');
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
}
UPDATE - With the release of Service Manager V3, FactoryInterface has been moved to Zend\ServiceManager\Factory namespace (1), factories are literally invokables (2) and works with any container-interop compatible DIC (3) Updated factory would be like below:
// ZF3 Way
use Zend\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
use Doctrine\ORM\EntityManager;
class CommissionControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $dic, $name, array $options = null) {
$em = $dic->get(EntityManager::class);
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
}
For the question; as of marcosh's said, Lazy Services are way to go to create services when need it immediately. ZF3 will use the zend-servicemanager 3.0 component when released. (Currently zend-expressive uses it) As of servicemanager v3 you can create some proxied services by defining lazy_services and delegators in your service configuration:
'factories' => [],
'invokables' => [],
'delegators' => [
FooService::class => [
FooServiceDelegatorFactory::class,
],
],
'lazy_services' => [
// map of service names and their relative class names - this
// is required since the service manager cannot know the
// class name of defined services up front
'class_map' => [
// 'foo' => 'MyApplication\Foo',
],
// directory where proxy classes will be written - default to system_get_tmp_dir()
'proxies_target_dir' => null,
// namespace of the generated proxies, default to "ProxyManagerGeneratedProxy"
'proxies_namespace' => null,
// whether the generated proxy classes should be written to disk or generated on-the-fly
'write_proxy_files' => false,
];
Also, starting with service manager v3 factories are compatible with the ContainerInterface. For the forward-compatibility, you may want to keep both __invoke() and createService() methods in your factories for a smooth migration.
In the end, your ZF3 compatible factory may look like:
class CommissionControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$em = $container->get('doctrine');
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
public function createService(ServiceLocatorInterface $container, $name = null, $requestedName = null)
{
return $this($container, $requestedName, []);
}
}
Hope it helps.
I am facing problem while creating adapter object in controller file named Listcontroller.My code is
namespace Blog\Controller;
use Blog\Service\PostServiceInterface;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Db\Sql\Sql;
use Zend\Db\Adapter\Adapter;
class ListController extends AbstractActionController
{
/**
* #var \Blog\Service\PostServiceInterface
*/
protected $postService;
public function __construct(PostServiceInterface $postService)
{
$this->postService = $postService;
}
public function indexAction()
{
$adapter = new Zend\Db\Adapter\Adapter($configArray);
print_r($adapter);
//code ....
}
}
here it is serching to find Zend\Db\Adapter\Adapter inside Blog\Controller.
Error is -> Fatal error: Class 'Blog\Controller\Zend\Db\Adapter\Adapter' not found. Can anybody tell me please how can i move two folder back from above path. so that i can get proper object??
You don't need the fully qualified class name (FQCN) when instantiating a new Adapter since you declare that class path via use statement:
use Zend\Db\Adapter\Adapter;
Change this block
public function indexAction()
{
$adapter = new Zend\Db\Adapter\Adapter($configArray);
print_r($adapter);
}
to
public function indexAction()
{
$adapter = new Adapter($configArray);
print_r($adapter);
}
It should work.
Anyway, new \Zend\Db\Adapter\Adapter($configArray) also works (notice the first backslash) but its longer, harder to type and less readable than first example.
You may also want to read namespace aliasing/importing section of the documentation.
I want to write some code to run before every actions in my module. I have tried hooking onto onBootstrap() but the code run on the other modules too.
Any suggestions for me?
There are two ways to do this.
One way is to create a serice and call it in every controllers dispatch method
Use onDispatch method in controller.
class IndexController extends AbstractActionController {
/**
*
* #param \Zend\Mvc\MvcEvent $e
* #return type
*/
public function onDispatch(MvcEvent $e) {
//Call your service here
return parent::onDispatch($e);
}
public function indexAction() {
return new ViewModel();
}
}
don't forget to include following library on top of your code
use Zend\Mvc\MvcEvent;
Second method is to do this via Module.php using event on dispatch
namespace Application;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module {
public function onBootstrap(MvcEvent $e) {
$sharedEvents = $e->getApplication()->getEventManager()->getSharedManager();
$sharedEvents->attach(__NAMESPACE__, 'dispatch', array($this, 'addViewVariables'), 201);
}
public function addViewVariables(Event $e) {
//your code goes here
}
// rest of the Module methods goes here...
//...
//...
}
How to create simple service using ZF2
reference2
reference3