Symfony is not using the Injected Services - php

I am writing an Custom Symfony Service, a Mailer, to send all of my mails.
Therefore I am trying to inject the mailer and the template Service inside the custom Service Class.
I already tried other solutions from stackoverflow, like e.g.
10304468, but this didn't helped me.
This is my Mailer Service Class
namespace Acme\DemoBundle\Mail;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Swift_Message;
class Mailer {
protected $mailer;
protected $templating;
public function __construct(Swift_Message $mailer, EngineInterface $templating) {
$this->mailer = $mailer;
$this->templating = $templating;
}
public function NewUserMail($newuser){
$message = new Swift_Message();
$message->newInstance()
->setSubject('Demo Subject')
->setFrom(array('info#example.com' => 'Your Company'))
->setTo($newuser->getEmail())
->setBody($this->templating->render('AcmeDemoBundle:Mail:newuser.html.twig'), 'text/html');
$this->sendMail($message);
}
public function sendMail($message) {
$this->mailer->send($message);
}
}
This is my Service.yml (which gets loaded through the Dependecy Injection Extension file:
acme_demo_bundle.mailer:
class: Acme\DemoBundle\Mail\Mailer
arguments: ["#mailer", "#templating"]
Now when I am calling the Mailer Service inside a Controller, I get this Error:
Catchable Fatal Error: Argument 1 passed to
Acme\DemoBundle\Mail\Mailer::__construct() must be an instance
of Swift_Message, none given,
How I call the Mailer Service inside the Controller:
/...
$message = new Mailer();
$message->NewUserMail($newuser);
/...
If I insert the mailer and templating service from the container, when generating the new Mailer Class, i won't get the error. But I thought, the Service.yml is insertig the services into my class, but obviously is does not.
I also tried this inside another symfony project, same problem. It looks like I am missing something here.... :(

Symfony's DependencyInjection doesn't actually change the way php works. Hence the service container. It's just a series of components that helps you centralize your service (which is a class really) instantiation. You can think of it in very very simple terms as this:
class ServiceContainer{
protected $definitions;
protected $instantiated;
/**
* #param array $definitions the service definitions, e.g: the result of parsing the yml file
*/
public function __construct($definitions){
$this->definitions = $definitions;
}
public function get($service){
if(isset($this->instantiated[$service])
return $this->instantiated[$service]);
$definition = $this->definitions[$service];
$class = $definition['class'];
$arguments = $definition['arguments'];
// the resolved arguments
$args = array();
foreach($arguments as $a){
// resolveArguments will return the corresponding object/value depending on the value of the arg
// i.e: it will return an instantiated service for "#mailer"
// or the parameter's value for "%some_parameter%"
$args[] = $this->resolveArguments($a);
}
// now instantiate the object base on the resolved arguments
$reflector = new ReflectionClass($class);
$object = $reflector->newInstanceArgs($args);
$this->instantiated[$service] = $object;
return $object;
}
}

Related

PHP DI pattern for function driven application

I have some classes that require dependencies injected into their constructors. This allows me to inject mocks (e.g. from prophecy) for testing.
I'm interested in using a container to help configure and access these objects, and I've looked at Pimple for this (I also looked at PHP-DI although I couldn't get that to resolve stuff on a quick attempt).
All good so far. BUT, the problem I have is that the application (Drupal 7) is built around thousands of functions which do not belong to an object that can have dependencies injected into.
So I need these functions to be able to access the services from the container. Further more, for testing purposes, I need to replace the services with mocks and new mocks.
So the pattern is like:
<?php
/**
* Some controller class that uses an injected mailing service.
*/
class Supporter
{
protected $mailer;
public function __construct(MailingServiceInterface $mailer) {
$this->mailer = $mailer;
}
public function signUpForMalings($supporter_id) {
$email = $this->getSupporterEmail($supporter_id);
$this->mailer->signup($email);
}
}
Then peppered in various functions I'd use:
<?php
/**
* A form submit handler called by the platform app,
* with a signature I can't touch.
*/
function my_form_submit($values) {
global $container;
if ($values['subscribe']) {
$supporter = $container->get('supporter');
$supporter->signUpForMailings($values['supporter_id']);
}
}
Elsewhere I may need to access the mailer directly...
<?php
/**
* example function requires mailer service.
*/
function is_signed_up($email) {
global $container;
return $container->get('mailer')->isSignedUp($email);
}
And elsewhere a function that calls those functions...
<?php
/**
* example function that uses both the above functions
*/
function sign_em_up($email, $supporter_id) {
if (!is_signed_up($email)) {
my_form_submit(['supporter_id'=>$supporter_id);
return TRUE;
}
}
Let's acknowledge that these functions are a mess - that's a deliberate representation of the problem. But let's say I want to test the sign_em_up function:
<?php
public testSignUpNewPerson() {
$mock_mailer = createAMockMailer()
->thatWill()
->return(FALSE)
->whenFunctionCalled('isSignedUp', 'wilma#example.com');
// Somehow install the mock malier in the container.
$result = sign_em_up('wilma#example.com', 123);
$this->assertTrue($result);
}
// ... imagine other tests which also need to inject mocks.
While I recognise that this is using the container as a Service Locator in the various global functions, I think this is unavoidable given the nature of the platform. If there's a cleaner way, please let me know.
However my main question is:
There's a problem with injecting mocks, because the mocks need to change for various tests. Lets say I swap out the mailer service (in Pimple: $container->offsetUnset('mailer'); $container['mailer'] = $mock_mailer;), but if Pimple had already instantiated the supporter service, then that service will have the old, unmocked mailer object. Is this a limitation of the containter software, or the general container pattern, or am I Doing It Wrong, or is it just a mess because of the old-school function-centred application?
Here's what I've gone for, in absence of any other suggestions!
Container uses Pimple\Psr11\ServiceLocator
I'm using Pimple, so the container's factories may look like this
<?php
use Pimple\Container;
use Pimple\Psr11\ServiceLocator;
$container = new Container();
$container['mailer'] = function ($c) { return new SomeMailer(); }
$container['supporters'] = function ($c) {
// Create a service locator for the 'Supporters' class.
$services = new ServiceLocator($c, ['mailer']);
return new Supporter($services);
}
Then the Supporter class now instead of storing references to the objects extracted from the container when it was created, now fetches them from the ServiceLocator:
<?php
use \Pimple\Psr11\ServiceLocator;
/**
* Some controller class that uses an injected mailing service.
*/
class Supporter
{
protected $services;
public function __construct(ServiceLocator $services) {
$this->services = $services;
}
// This is a convenience function.
public function __get($prop) {
if ($prop == 'mailer') {
return $this->services->get('mailer');
}
throw new \InvalidArgumentException("Unknown property '$prop'");
}
public function signUpForMalings($supporter_id) {
$email = $this->getSupporterEmail($supporter_id);
$this->mailer->signup($email);
}
}
In the various CMS functions I just use global $container; $mailer = $container['mailer'];, but it means that that in tests I can now mock any service and know that all code that needs that service will now have my mocked service. e.g.
<?php
class SomeTest extends \PHPUnit\Framework\TestCase
{
function testSupporterGetsMailed() {
global $container;
$supporter = $container['supporter'];
// e.g. mock the mailer component
$container->offsetUnset('mailer');
$container['mailer'] = $this->getMockedMailer();
// Do something with supporter.
$supporter->doSomething();
// ...
}
}

Dependency Injection with Adapter Pattern and Type Hinting

I'm trying to get my head around type hinting in combination with adapters.
The system fetches XML feeds via different services and updates the database with the changes - I am refactoring to help learn design patterns.
Log Interface:
interface LoggerAdapterInterface {
public function debug($string);
public function info($string);
public function error($string);
}
MonoLog Adapter
class MonoLogAdapter implements LoggerAdapterInterface
{
public $logger;
public function __construct(\Monolog\Logger $logger)
{
$this->logger = $logger;
}
public function debug($string)
{
$this->logger->debug($string);
}
public function info($string)
{
$this->logger->info($string);
}
public function error($string)
{
$this->logger->error($string);
}
}
FeedFactory
class FeedFactory
{
public function __construct()
{
}
public static function build(LoggerAdapter $logger, $feedType)
{
// eg, $feedType = 'Xml2u'
$className = 'Feed' . ucfirst($feedType);
// eg, returns FeedXml2u
return new $className($logger);
}
}
Implementation
// get mono logger
$monoLogger = $this->getLogger();
// create adapter and inject monologger
$loggerAdapter = new MonoLogAdapter($monoLogger);
// build feed object
$Feed = FeedFactory::build($loggerAdapter, 'Xml2u');
Error
PHP Catchable fatal error: Argument 1 passed to FeedFactory::build()
must be an instance of LoggerAdapter, instance of MonoLogAdapter
given, called in /src/shell/feedShell.php on line 64 and defined in
/src/Feeds/FeedFactory.php on line 25
So I am using the LoggerAdapter so that I am not tied to one logging platform. The problem is that when I create a new instance of MonoLogger and try to inject it into the factory - PHP type-hinting does not realize that MonoLogger implements LoggerAdapter.
Am I doing something wrong here?
As #Ironcache suggested - use interface as argument in your build method.
public static function build(LoggerAdapterInterface $logger, $feedType)
{
// eg, $feedType = 'Xml2u'
$className = 'Feed' . ucfirst($feedType);
// eg, returns FeedXml2u
return new $className($logger);
}
note: also check namespace

ZF2 phpunit Zend Logger

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);

Symfony 2.8: Can't manage to use container in custom class

I would like to use $this->container->get in a custom class I've created.
I've done my reading and found out that I should use ContainerInterface in the constructor, which I do, but I still get this error:
Error: Call to a member function get() on a non-object
Here is the code:
MyClass.php
namespace path\to\MyClass;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyClass {
private $container;
public $user_id;
public function __contruct(ContainerInterface $container) {
$this->container = $container;
$this->user_id = $user_id;
return $this;
}
/**
* #param string $data Some data
* #return array A response
*/
public function generatePDF($data)
{
// Create the folders if needed
$pdf_folder = __DIR__.'/../../../../web/pdf/'.$this->user_id.'/';
if(!file_exists($pdf_folder))
mkdir($pdf_folder, 0755, TRUE);
$file_id = "abc1";
// Set the file name
$file = $pdf_folder.$file_id.'.pdf';
// Remove the file if it exists to prevent errors
if(file_exists($file)) {
unlink($file);
}
// Generate the PDF
$this->container->get('knp_snappy.pdf')->generateFromHtml(
$this->renderView(
'StrimeGlobalBundle:PDF:invoice.html.twig',
$data
),
$file
);
}
}
Do you guys have any idea of what could be the problem?
Thanks for your help.
You need to declare your class as a service in the Symfony configuration.
Please have a look to the Symfony service container page.
Here is an explanation to inject the container in the constructor:
# services.yml
services:
app.my_class:
class: TheBundle\Service\MyClass
arguments: ['#service_container']
Or as said JimL in comment, you can inject the service you need (which is recommanded):
class MyClass
{
private $pdfService;
public function __construct(\Your\Service\Namespace\Class $pdfService)
{
$this->pdfService = $pdfService;
}
// ...
}
And in your service.yml file
# services.yml
services:
app.my_class:
class: TheBundle\Service\MyClass
arguments: ['#knp_snappy.pdf']
The container can also be injected with a setter. See this link
Hope this helps!
Your class cannot "see" any service (including the container) unless you "inject" it. In YourCustomBundle/Resources/config/services.xml you need to define the service and it's dependencies. Read up on Dependency Injection and it should make more sense.
Also, #JimL is right, you shouldn't inject the entire container to access one service, simply inject the one service (knp_snappy.pdf)

Dependency on mock method with PHPUnit

I'm attempting to write PHPUnit tests for an Email abstraction class i'm using. The class interacts with the Mailgun API but I don't want to touch this in my test, I just want to return the response I would expect from Mailgun.
Within my test I have a setup method:
class EmailTest extends PHPUnit_Framework_TestCase
{
private $emailService;
public function setUp()
{
$mailgun = $this->getMockBuilder('SlmMail\Service\MailgunService')
->disableOriginalConstructor()
->getMock();
$mailgun->method('send')
->willReturn('<2342423#sandbox54533434.mailgun.org>');
$this->emailService = new Email($mailgun);
parent::setUp();
}
public function testEmailServiceCanSend()
{
$output = $this->emailService->send("me#test.com");
var_dump($output);
}
}
This is the basic outline of the email class
use Zend\Http\Exception\RuntimeException as ZendRuntimeException;
use Zend\Mail\Message;
use SlmMail\Service\MailgunService;
class Email
{
public function __construct($service = MailgunService::class){
$config = ['domain' => $this->domain, 'key' => $this->key];
$this->service = new $service($config['domain'], $config['key']);
}
public function send($to){
$message = new Message;
$message->setTo($to);
$message->setSubject("test subject");
$message->setFrom($this->fromAddress);
$message->setBody("test content");
try {
$result = $this->service->send($message);
return $result;
} catch(ZendRuntimeException $e) {
/**
* HTTP exception - (probably) triggered by network connectivity issue with Mailgun
*/
$error = $e->getMessage();
}
}
}
var_dump($output); is currently outputting NULL rather than the string i'm expecting. The method send i'm stubbing in the mock object has a dependency through an argument, and when I call $mailgun->send() directly it errors based on this so I wonder if this is what is failing behind the scenes. Is there a way to pass this argument in, or should I approach this a different way?
It is strange it does not throw an exception in Email::__construct.
The expected parameter is a string, and the MailgunService object is instantiated within the email constructor. In your test you are passing the object, so I would expect and error at line
$this->service = new $service($config['domain'], $config['key']);
What you need is:
class Email
{
public function __construct($service = null){
$config = ['domain' => $this->domain, 'key' => $this->key];
$this->service = $service?: new MailgunService($config['domain'], $config['key']);
}
Also, it may not be a good idea to catch an exception and return nothing in Email::send.

Categories