I'm developing a pretty simple Symfony console application. It has just one command with one argument, and a few options.
I followed this guide to create an extension of the Application class.
This is the normal usage for the app, and it works fine:
php application <argument>
This also works fine (argument with options):
php application.php <argument> --some-option
If someone runs php application.php without any arguments or options, I want it to run as though the user had run php application.php --help.
I do have a working solution but it isn't optimal and is perhaps slightly brittle. In my extended Application class, I overrode the run() method as follows:
/**
* Override parent method so that --help options is used when app is called with no arguments or options
*
* #param InputInterface|null $input
* #param OutputInterface|null $output
* #return int
* #throws \Exception
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
if ($input === null) {
if (count($_SERVER["argv"]) <= 1) {
$args = array_merge($_SERVER["argv"], ["--help"]);
$input = new ArgvInput($args);
}
}
return parent::run($input, $output);
}
By default, Application::run() is called with a null InputInterface, so here I figured I could just check the raw value of the arguments and forcefully add a help option to pass to the parent method.
Is there a better way to achieve this?
I managed to work out a solution which didn't involve touching the Application class at all. To call the help command from within another command:
/**
* #param InputInterface $input
* #param OutputInterface $output
* #return int
* #throws \Symfony\Component\Console\Exception\ExceptionInterface
*/
protected function outputHelp(InputInterface $input, OutputInterface $output)
{
$help = new HelpCommand();
$help->setCommand($this);
return $help->run($input, $output);
}
To do a specific action depending on command, you can use an EventListener which is called when the onConsoleCommand is fired.
The listener class should work as follows :
<?php
namespace AppBundle\EventListener;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Command\HelpCommand;
class ConsoleEventListener
{
public function onConsoleCommand(ConsoleCommandEvent $event)
{
$application = $event->getCommand()->getApplication();
$inputDefinition = $application->getDefinition();
if ($inputDefinition->getArgumentCount() < 2) {
$help = new HelpCommand();
$help->setCommand($event->getCommand());
return $help->run($event->getInput(), $event->getOutput());
}
}
}
The service declaration :
services:
# ...
app.console_event_listener:
class: AppBundle\EventListener\ConsoleEventListener
tags:
- { name: kernel.event_listener, event: console.command, method: onConsoleCommand }
Related
Hello I'm trying to create a unit-test for a symfony4 console command but I can't inject the dependencies correctly. I'm very new to symfony4 so maybe this is a basic question for you guys.
My unit test looks like this:
<?php
namespace App\Tests\Command;
use App\Command\ExecuteSalesTaskCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Psr\Log\LoggerInterface;
use App\Repository\TaskRepository;
class ExeculteSalesTaskCommandTest extends KernelTestCase
{
/**
* #param LoggerInterface $logger
* #param TaskRepository $taskRepository
*/
public function testExecute(LoggerInterface $logger, TaskRepository $taskRepository)
{
$kernel = self::bootKernel();
$application = new Application($kernel);
$application->add(new ExecuteSalesTaskCommand($logger,$taskRepository));
# UPDATED
$logger = self::$kernel->getContainer()->get(LoggerInterface::class);
$taskRepository = self::$kernel->getContainer()->get(TaskRepository::class);
$command = $application->find('app:execute-sales-task');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
]
);
// the output of the command in the console
$output = $commandTester->getDisplay();
$this->assertContains('Execute sales resulted: ', $output);
}
}
My problem is that I get injection errors like this:
ArgumentCountError: Too few arguments to function
App\Tests\Command\ExeculteSalesTaskCommandTest::testExecute(), 0
passed and exactly 2 expected
UPDATE:
When I fetch the dependencies out of the container I get this kind of error:
There was 1 error:
1) App\Tests\Command\ExeculteSalesTaskCommandTest::testExecute
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException:
The "Psr\Log\LoggerInterface" service or alias has been removed or
inlined when the container was compiled. You should either make it
public, or stop using the container directly and use dependency
injection instead.
How can I inject the necessary dependencies correctly, so I can create an instance of the ExecuteSalesTaskCommand?
Well I found out that the issue was that I tried to load the dependencies manually. Use the autowiring instead like this:
public function testExecute()
{
$dotenv = new Dotenv();
$dotenv->load(__DIR__.'/.env.test');
$kernel = self::bootKernel();
$application = new Application($kernel);
$executeSalesCommand = self::$kernel->getContainer()->get(
'console.command.public_alias.App\Command\ExecuteSalesTaskCommand'
);
$application->add($executeSalesCommand);
$command = $application->find('app:execute-sales-task');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
]
);
// the output of the command in the console
$output = $commandTester->getDisplay();
// do your asserting stuff
}
You need to get the command itself from the kernel container. Now it works.
I was also encountered similar issue. But following worked for me. I don't think you need to add command to the application and again to find it? Please find following solution. It might help others.
<?php
// BaseCommandTester.php
/**
* This is basis for the writing tests, that will cover commands
*
*/
namespace App\Tests\Base;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Dotenv\Dotenv;
class BaseCommandTester extends KernelTestCase
{
/**
* #var
*/
private $application;
/**
* to set test environment and initiate application kernel
*/
public function setUp()
{
/**
* get test env
*/
$dotenv = new Dotenv();
$dotenv->load('/var/www/.env.test');
/**
* boot kernel
*/
$kernel = self::bootKernel();
$this->application = new Application($kernel);
parent::setUp();
}
/**
* #return mixed
*/
public function getApplication()
{
return $this->application;
}
/**
* #param mixed $application
*/
public function setApplication($application)
{
$this->application = $application;
}
}
Test Case
// FeedUpdaterCommandTest.php
<?php
namespace App\Tests\Command;
use App\Tests\Base\BaseCommandTester;
use Symfony\Component\Console\Tester\CommandTester;
class FeedUpdaterCommandTest extends BaseCommandTester
{
/**
* test to update all feeds
*/
public function testExecuteUpdateAll() {
/**
* init command tester and executre
*/
$commandName = 'app:feedUpdater';
$expectedResult = '[OK] Update Success Feed Type : All';
$command = $this->getApplication()->find($commandName);
$commandTester = new CommandTester($command);
$commandTester->execute(array(
'command' => $command->getName()
));
/**
* get result and compare output
*/
$result = trim($commandTester->getDisplay());
$this->assertEquals($result, $expectedResult);
}
}
Result of Test run
#Run tests
root#xxx:/var/www# bin/phpunit tests/Command
#!/usr/bin/env php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
Testing tests/Command
2018-11-28T07:47:39+00:00 [alert] Successful update of popularProducts Feeds!
2018-11-28T07:47:39+00:00 [alert] Successful update of topVouchers Feeds!
. 1 / 1 (100%)
Time: 1.44 seconds, Memory: 12.00MB
OK (1 test, 1 assertion)
I am using following Sf4 version
-------------------- --------------------------------------
Symfony
-------------------- --------------------------------------
Version 4.1.7
Service Defination and its by default private
#config/services.yml
App\Service\FeedGenerator:
arguments:
$feeds: '%feed_generator%'
I don't think you need to autowire again.
I create some basic command with symfony3.2 to generate some newsletter periodically
I'm dealing with some issue when i want to test my symfony command with phpunit 5.5.4.
It fail from the beginning:
/**
* #param InputInterface $input
* #param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output){
$output->writeln("<info>Script start</info>");
//...
$output->writeln("<info>done</info>");
}
with this unit test:
use MyBundle\Command\MyCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
class MyCommandTest extends KernelTestCase
{
public function testExecute(){
$kernel = static::createKernel();
$kernel->boot();
$application = new Application($kernel);
$application->add(new MyCommand());
$command = $application->find('generate:newsletter');
$commandTester = new CommandTester($command);
$commandTester->execute(array(
'command' => $command->getName()
));
$output = $commandTester->getDisplay();
$this->assertContains('done',$output);
}
}
I follow this step by step but in my case i get :
Error: Call to a member function writeln() on string
MyBundle/Command/MyCommand.php:197
vendor/symfony/symfony/src/Symfony/Component/Console/Command/Command.php:262
vendor/symfony/symfony/src/Symfony/Component/Console/Tester/CommandTester.php:84
MyBundle/Command/MyCommandTest.php:34
It seems like commandTester don't put correct parameter in execute method from MyCommand.
I'm wondering if it's not CommandTesterClass issue.
That's why i'm here, to share with you that and find some solution together.
Thank you in advance
Method 'getDisplay()' returns a string as you can see from the Api doc:
http://api.symfony.com/3.0/Symfony/Component/Console/Tester/CommandTester.html and you're assigning that string to your $output variable.
I think what you need is 'getOutput()'
I'm running Symfony 2.7 and I'm trying output an object (Doctrine entity) as JSON.
When I'm normalizing the object I want to convert some of it's values. To do this I found the "setCallbacks" method in the documentation but I'm kinda stumped on how to apply it to my case.
Is there any way to call the "setCallbacks" method on the normalizer that is set when calling Symfonys serializer service?
Here is a short example of what I'm trying to achieve:
//ExampleController.php
public function getJSONOrderByIdAction($id) {
$serializer = $this->get('serializer');
$normalizer = $serializer->getNormalizer(); // <- This is what I'm unable to do
$dateTimeToString = function ($dateTime) {
return $dateTime instanceof \DateTime ? $dateTime->format(\DateTime::ISO8601) : '';
};
$normalizer->setCallbacks(['time' => $dateTimeToString]);
$order = $this->getDoctrine()->find("AppBundle:Order", $id);
return new JsonResponse(["order" => $serializer->normalize($order, null, ["groups" => ["public"]])]);
}
I'm aware that most people have switched to the JMS serializer. It just seems as if the built in serializer should be able to handle what I'm trying to achieve.
The default Serializer service is created during dependency injection phase, and the Serializer interface do not allow editing of (full) retrieval of normalizers.
I think you have (at least) three choice here:
add your custom normalizer to the default Serializer service
add NormalizableInterface to your entities
create a new Serializer service (or a local object as suggested by the docs) as you were trying to do.
I think in your scenario, case 1 is preferred (since 2 becomes boring pretty fast).
I would do something like this; first create a custom Normalizer
<?php
namespace AppBundle;
class DateTimeNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return $object->format(\DateTime::ISO8601);
}
/**
* {#inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
return new $class($data);
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to normalize.
* #param string $format The format being (de-)serialized from or into.
*
* #return bool
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \DateTime;
}
/**
* Checks if the given class is a DateTime.
*
* #param mixed $data Data to denormalize from.
* #param string $type The class to which the data should be denormalized.
* #param string $format The format being deserialized from.
*
* #return bool
*/
public function supportsDenormalization($data, $type, $format = null)
{
$class = new \ReflectionClass($type);
return $class->isSubclassOf('\DateTime');
}
}
Then register it to your services:
# app/config/services.yml
services:
datetime_normalizer:
class: AppBundle\DateTimeNormalizer
tags:
- { name: serializer.normalizer }
My own solution
Following the advice from giosh94mhz I tried switching to JMS Serializer but ended up going back to Symfonys serializer.
JMS Serializer presented it's own issues and while searching for answers for those I stumbled upon a blog post by Thomas Jarrand that did an excellent job explaining how to make and implement your own normalizers in Symfony.
You can use callback normalizer of K. Dunglas component.
You can see that in ObjectNormalizer (in normalize method)
if (isset($this->callbacks[$attribute])) {
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
}
This mean that you must use in callback array key the name of property you wan't to normalize.
For example in my entity I have field named "name" of type "pgarray" (like array for postgresql). I wan't to normalize this data. Instead array I want a string.
/**
* $object represent the property "name" because callback is attached to name property (setCallback)
*/
$nameCallback = function ($object, $outerObject = null) {
return $object[0];
};
$this->normalizer->setCallbacks(['name' => $dateCallback]);
Just remember since Symfony 4.2 you must use $context in DI to use callback.
In my opinion, you seem to be trying to over-complicate things. Here's the approach I've taken when I needed to serialize my entities as JSON:
PHP 2.5 and above allows you to implement the jsonSerialize method on your objects and just call json_encode directly on your object.
If you are still using PHP 2.4, you just need to manually call jsonSerialize() on your objects.
For example:
/**
* #ORM\Entity
*/
class MyEntity {
...
public function jsonSerialize() {
$data = array("foo" => $this->bar());
// add other data here ...
return $data
}
}
And then in calling code:
// for PHP 2.5 and up:
$normalized = json_encode($myEntityInstance);
// for PHP 2.4 and below
$normalized = json_encode($myEntityInstance->jsonSerialize());
I'm just beginning with PHPUnit and TDD.
Among others, I can't really answer to this question: Is this a good test? Am i actually testing my code or something already tested (i.e. the framework or PHP itself)?
Little example, this is the test subject:
class DateMax extends Constraint
{
/**
* #var string
*/
public $limit;
/**
* #var string
*/
private $invalidLimit = 'Option "limit" should be a valid date/time string.';
public function __construct($options = null)
{
parent::__construct($options);
if(false === strtotime($this->limit)) {
throw new InvalidOptionsException($this->invalidLimit, ['limit']);
}
}
}
I want to test that InvalidOptionsException is expected when invalid "limit" options are passed, otherwise $constraint->limit holds the correct value:
/**
* #dataProvider getInvalidLimits
* #expectedException InvalidOptionsException
*/
public function testInvalidLimits($testLimit)
{
new DateMax($testLimit);
}
/**
* #dataProvider getValidLimits
*/
public function testValidLimits($testLimit)
{
$constraint = new DateMax($testLimit);
$this->assertEquals($testLimit, $constraint->limit);
}
/**
* #return array[]
*/
public function getInvalidLimits()
{
return array(array('invalid specification'), array('tomorr'));
}
/**
* #return array[]
*/
public function getValidLimits()
{
return array(array('now'), array('+1 day'),array('last Monday'));
}
So question is does this make any sense or I'm testing the framework/PHP itself?
Of course it has sense, because you override constructor of Constraint class and there is possibility that you'll break something inside it. So basing on your constructor logic basically you want to test two things:
check if you call parent's constructor with the same options, exactly once (you can use mock for this purpose, you don't care about setting appropriate limit value, because this should be tested in Constraint class)
check if an appropriate exception has been thrown when limit has wrong value (eg. null)
edit: Some use case where first test will be useful may be this one:
Let say at some moment you want to extend your DateMax constructor in this way:
public function __construct($options = null)
{
$this->optionsWithDecrementedValues = $this->doWeirdThings($options);
parent::__construct($options);
if(false === strtotime($this->limit)) {
throw new InvalidOptionsException($this->invalidLimit, ['limit']);
}
}
but for example you didn't notice that method "doWeirdThings" takes a reference as argument. So in fact it changes $options value, what you didn't expect, but first test fails so you won't miss it.
Currently I am trying to learn the Zend Framework and therefore I bought the book "Zend Framework in Action".
In chapter 3, a basic model and controller is introduced along with unit tests for both of them. The basic controller looks like this:
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$this->view->title = 'Welcome';
$placesFinder = new Places();
$this->view->places = $placesFinder->fetchLatest();
}
}
Places is the model class that fetches the latest places from the database. What bugs me here: how should I test the IndexController in isolation? As the reference to the Places class is "hardcoded", I cant inject any stubs or mocks in IndexController.
What I would rather like to have is something like this:
class IndexController extends Zend_Controller_Action
{
private $placesFinder;
// Here I can inject anything: mock, stub, the real instance
public function setPlacesFinder($places)
{
$this->placesFinder = $places;
}
public function indexAction()
{
$this->view->title = 'Welcome';
$this->view->places = $this->placesFinder->fetchLatest();
}
}
The first code sample I posted is most definately NOT unit test friendly as IndexController cannot be tested in isolation. The second one is much better. Now I just need some way to inject the model instances into the controller objects.
I know that the Zend Framework per se has no component for dependency injection. But there are some good frameworks out there for PHP, can any be used together with Zend Framework? Or is there some other way to do this in Zend Framework?
Logic to models
First of all, it's worth to mention, that controllers should need only functional tests, though all the logic belongs to models.
My implementation
Here is an excerpt from my Action Controller implementation, which solves the following problems:
allows inject any dependency to actions
validates the action parameters, e.g. you may not pass array in $_GET when integer is expected
My full code allows also to generate canonical URL (for SEO or unique page hash for stats) based or required or handled action params. For this, I use this abstract Action Controller and custom Request object, but this is not the case we discuss here.
Obviously, I use Reflections to automatically determine action parameters and dependency objects.
This is a huge advantage and simplifies the code, but also has an impact in performance (minimal and not important in case of my app and server), but you may implement some caching to speed it up. Calculate the benefits and the drawbacks, then decide.
DocBlock annotations are becoming a pretty well known industry standard, and parsing it for evaluation purposes becomes more popular (e.g. Doctrine 2). I used this technique for many apps and it worked nicely.
Writing this class I was inspired by Actions, now with params! and Jani Hartikainen's blog post.
So, here is the code:
<?php
/**
* Enchanced action controller
*
* Map request parameters to action method
*
* Important:
* When you declare optional arguments with default parameters,
* they may not be perceded by optional arguments,
* e.g.
* #example
* indexAction($username = 'tom', $pageid); // wrong
* indexAction($pageid, $username = 'tom'); // OK
*
* Each argument must have #param DocBlock
* Order of #param DocBlocks *is* important
*
* Allows to inject object dependency on actions:
* #example
* * #param int $pageid
* * #param Default_Form_Test $form
* public function indexAction($pageid, Default_Form_Test $form = null)
*
*/
abstract class Your_Controller_Action extends Zend_Controller_Action
{
/**
*
* #var array
*/
protected $_basicTypes = array(
'int', 'integer', 'bool', 'boolean',
'string', 'array', 'object',
'double', 'float'
);
/**
* Detect whether dispatched action exists
*
* #param string $action
* #return bool
*/
protected function _hasAction($action)
{
if ($this->getInvokeArg('useCaseSensitiveActions')) {
trigger_error(
'Using case sensitive actions without word separators' .
'is deprecated; please do not rely on this "feature"'
);
return true;
}
if (method_exists($this, $action)) {
return true;
}
return false;
}
/**
*
* #param string $action
* #return array of Zend_Reflection_Parameter objects
*/
protected function _actionReflectionParams($action)
{
$reflMethod = new Zend_Reflection_Method($this, $action);
$parameters = $reflMethod->getParameters();
return $parameters;
}
/**
*
* #param Zend_Reflection_Parameter $parameter
* #return string
* #throws Your_Controller_Action_Exception when required #param is missing
*/
protected function _getParameterType(Zend_Reflection_Parameter $parameter)
{
// get parameter type
$reflClass = $parameter->getClass();
if ($reflClass instanceof Zend_Reflection_Class) {
$type = $reflClass->getName();
} else if ($parameter->isArray()) {
$type = 'array';
} else {
$type = $parameter->getType();
}
if (null === $type) {
throw new Your_Controller_Action_Exception(
sprintf(
"Required #param DocBlock not found for '%s'", $parameter->getName()
)
);
}
return $type;
}
/**
*
* #param Zend_Reflection_Parameter $parameter
* #return mixed
* #throws Your_Controller_Action_Exception when required argument is missing
*/
protected function _getParameterValue(Zend_Reflection_Parameter $parameter)
{
$name = $parameter->getName();
$requestValue = $this->getRequest()->getParam($name);
if (null !== $requestValue) {
$value = $requestValue;
} else if ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} else {
if (!$parameter->isOptional()) {
throw new Your_Controller_Action_Exception(
sprintf("Missing required value for argument: '%s'", $name));
}
$value = null;
}
return $value;
}
/**
*
* #param mixed $value
*/
protected function _fixValueType($value, $type)
{
if (in_array($type, $this->_basicTypes)) {
settype($value, $type);
}
return $value;
}
/**
* Dispatch the requested action
*
* #param string $action Method name of action
* #return void
*/
public function dispatch($action)
{
$request = $this->getRequest();
// Notify helpers of action preDispatch state
$this->_helper->notifyPreDispatch();
$this->preDispatch();
if ($request->isDispatched()) {
// preDispatch() didn't change the action, so we can continue
if ($this->_hasAction($action)) {
$requestArgs = array();
$dependencyObjects = array();
$requiredArgs = array();
foreach ($this->_actionReflectionParams($action) as $parameter) {
$type = $this->_getParameterType($parameter);
$name = $parameter->getName();
$value = $this->_getParameterValue($parameter);
if (!in_array($type, $this->_basicTypes)) {
if (!is_object($value)) {
$value = new $type($value);
}
$dependencyObjects[$name] = $value;
} else {
$value = $this->_fixValueType($value, $type);
$requestArgs[$name] = $value;
}
if (!$parameter->isOptional()) {
$requiredArgs[$name] = $value;
}
}
// handle canonical URLs here
$allArgs = array_merge($requestArgs, $dependencyObjects);
// dispatch the action with arguments
call_user_func_array(array($this, $action), $allArgs);
} else {
$this->__call($action, array());
}
$this->postDispatch();
}
$this->_helper->notifyPostDispatch();
}
}
To use this, just:
Your_FineController extends Your_Controller_Action {}
and provide annotations to actions, as usual (at least you already should ;).
e.g.
/**
* #param int $id Mandatory parameter
* #param string $sorting Not required parameter
* #param Your_Model_Name $model Optional dependency object
*/
public function indexAction($id, $sorting = null, Your_Model_Name $model = null)
{
// model has been already automatically instantiated if null
$entry = $model->getOneById($id, $sorting);
}
(DocBlock is required, however I use Netbeans IDE, so the DocBlock is automatically generated based on action arguments)
Ok, this is how I did it:
As IoC Framework I used this component of the symfony framework (but I didnt download the latest version, I used an older one I used on projects before... keep that in mind!). I added its classes under /library/ioc/lib/.
I added these init function in my Bootstrap.php in order to register the autoloader of the IoC framework:
protected function _initIocFrameworkAutoloader()
{
require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');
sfServiceContainerAutoloader::register();
}
Next, I made some settings in application.ini which set the path to the wiring xml and allow to disable automatic dependency injection e. g. in unit tests:
ioc.controllers.wiringXml = APPLICATION_PATH "/objectconfiguration/controllers.xml"
ioc.controllers.enableIoc = 1
Then, I created a custom builder class, which extends sfServiceContainerBuilder and put it under /library/MyStuff/Ioc/Builder.php. In this test project I keep all my classes under /library/MyStuff/.
class MyStuff_Ioc_Builder extends sfServiceContainerBuilder
{
public function initializeServiceInstance($service)
{
$serviceClass = get_class($service);
$definition = $this->getServiceDefinition($serviceClass);
foreach ($definition->getMethodCalls() as $call)
{
call_user_func_array(array($service, $call[0]), $this->resolveServices($this->resolveValue($call[1])));
}
if ($callable = $definition->getConfigurator())
{
if (is_array($callable) && is_object($callable[0]) && $callable[0] instanceof sfServiceReference)
{
$callable[0] = $this->getService((string) $callable[0]);
}
elseif (is_array($callable))
{
$callable[0] = $this->resolveValue($callable[0]);
}
if (!is_callable($callable))
{
throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service)));
}
call_user_func($callable, $service);
}
}
}
Last, I created a custom controller class in /library/MyStuff/Controller.php which all my controllers inherit from:
class MyStuff_Controller extends Zend_Controller_Action {
/**
* #override
*/
public function dispatch($action)
{
// NOTE: the application settings have to be saved
// in the registry with key "config"
$config = Zend_Registry::get('config');
if($config['ioc']['controllers']['enableIoc'])
{
$sc = new MyStuff_Ioc_Builder();
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load($config['ioc']['controllers']['wiringXml']);
$sc->initializeServiceInstance($this);
}
parent::dispatch($action);
}
}
What this basically does is using the IoC Framework in order to initialize the already created controller instance ($this). Simple tests I did seemed to do what I want... let´s see how this performs in real life situations. ;)
It´s still monkey patching somehow, but the Zend Framework doesn´t seem to provide a hook where I can create the controller instance with a custom controller factory, so this is the best I came up with...
I'm currently working on the same question, and after deep research I've decide to use Symfony Dependency Injection component. You can get good info from official website http://symfony.com/doc/current/book/service_container.html.
I've build custom getContainer() method in bootstrap, which resturns now service container, and it simply can be used in controllers like
public function init()
{
$sc = $this->getInvokeArg('bootstrap')->getContainer();
$this->placesService = $sc->get('PlacesService');
}
Here you can find how to do that http://blog.starreveld.com/2009/11/using-symfony-di-container-with.html. But I changed ContainerFactory, because of using Symfony2 component, instead of first version.
You could also just use the PHP-DI ZF bridge: http://php-di.org/doc/frameworks/zf1.html
I know this question is really old but it pops up rather high in search engines when looking for DI in ZF1 so I thought I'd add a solution that doesn't require you to write it all by yourself.
With the Service Manager at Zend Framework 3.
Official Documentation:
https://zendframework.github.io/zend-servicemanager/
Dependencies at your Controller are usually be injected by DI Constructor injector.
I could provide one example, that inject a Factory responsible to create the ViewModel instance into the controller.
Example:
Controller
`
class JsonController extends AbstractActionController
{
private $_jsonFactory;
private $_smsRepository;
public function __construct(JsonFactory $jsonFactory, SmsRepository $smsRepository)
{
$this->_jsonFactory = $jsonFactory;
$this->_smsRepository = $smsRepository;
}
...
}
Creates the Controller
class JsonControllerFactory implements FactoryInterface
{
/**
* #param ContainerInterface $serviceManager
* #param string $requestedName
* #param array|null $options
* #return JsonController
*/
public function __invoke(ContainerInterface $serviceManager, $requestedName, array $options = null)
{
//improve using get method and callable
$jsonModelFactory = new JsonFactory();
$smsRepositoryClass = $serviceManager->get(SmsRepository::class);
return new JsonController($jsonModelFactory, $smsRepositoryClass);
}
}
`
Complete example at https://github.com/fmacias/SMSDispatcher
I hope it helps someone