I am making a messenger which can send email messages or sms messages, and has the possibility to send them now or send them later (the information is saved in the DB). I've made 2 solutions, but neither is satisfying me.
I'm centralising the code in one Factory, and the code of the Factory pattern is very easy:
class MessageFactory
{
static public function get($type,$em)
{
$instance = null;
switch ($type) {
case 'email':
$instance = new EmailMessage($em);
break;
....
return $instance;
}
class EmailMessage implements MessangerInterface
{
...
public function send( $eMessage,array $receivers, $time=NULL)
{
interface MessangerInterface
{
public function send($message,array $receivers);
}
1st solution: Just call as an ordinary static method
$messanger = Factory\MessageFactory::get('email',$em);
$messanger->send($eMessage, array('tom'=>'tom#gmail.com'));
This is a bad solution, because I need to pass in a Doctrine Manager as a parameter to the method
2nd solution: To use it as a Symfony 2 Service
services:
my.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: %messanger.type%
and also pass in Doctrine as an argument. But using such a solution I can't choose messanger.type in my code, it's defined using a configuration parameter as email or sms; I need to have the capability in code to choose the type.
Also I have a problem that inside the class I need to send email or sms, and that means that I need an external service, getting it like this:
class EmailMessage implements MessangerInterface
{
if ('AppCache' == get_class($kernel)) {
$kernel = $kernel->getKernel();
}
$kernel->getContainer()->get('mailer')->send($eMessage);
which seems like very bad practice.
Please, are you able to advise me on any better solutions?
I want to follow the "thin controller fat model" concept.
It seems like option 2, using Symfony 2 Services, would be best.
I considered suggesting that you let the Factory be the Service, and pass the type in to get the Messenger instance, rather than fixing it in config, but if what you want is to only have one of each type of Messenger then that's unhelpful (the Factory would keep creating more and more Messengers). So instead I think you need to define two Services, one for each Messenger.
And if you don't want to have to fetch another Service within your Messenger, you need to inject that in when you get the Messenger.
e.g.
services:
mailer:
class: Mailer
smser:
class: SMSer
email.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: email
sender: #mailer
sms.messanger:
class: Bundle\Factory\MessangerInterface
factory_class: Bundle\Factory\MessageFactory
factory_method: get
arguments:
messanger_type: sms
sender: #smser
And your Factory needs to accept the new $sender argument:
class MessageFactory
{
static public function get($type,$em,$sender)
{
$instance = null;
switch ($type) {
case 'email':
$instance = new EmailMessage($em, $sender);
break;
....
return $instance;
}
interface MessangerInterface
{
public function send($message,$sender, array $receivers);
}
Then when you call it, you ask for either of the Messengers specifically:
$this->get('email.messenger')->send($emailMessage);
$this->get('sms.messenger')->send($smsMessage);
Related
I would like to access my database that contains all my user inside my provider with doctrine. I followed a tutorial (http://symfony.com/doc/current/security/custom_provider.html) to build my provider for my user, so I have an loadUserByUsername function :
public function loadUserByUsername($username)
{
// make a call to your webservice here
$player = new Player();
$player = $this->getDoctrine()
->getRepository('AppBundle:Player')
->findOneByPseudo($username);
// pretend it returns an array on success, false if there is no user
if ($player) {
return $player;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
But of course my getDoctrine() function is undefined. So there is something I don't understand with the provider, I am trying to use it to be authenticated when I login so I need a provider, but why I can't search inside my database? How should I write this function? Thank for your help
EDIT :
When I add doctrine by service.yml (and after writting my constructor inside my provider), I have this error :
FatalThrowableError in PlayerProvider.php line 13:
Type error: Argument 1 passed to AppBundle\Security\PlayerProvider::__construct() must be an instance of Doctrine\Bundle\DoctrineBundle\Registry, instance of Doctrine\ORM\EntityManager given, called in /home/jean/PW6/SkA/SkeletonsOnlineV2/skeleton-online/var/cache/dev/appDevDebugProjectContainer.php on line 327
EDIT 2 : When I just put arguments: ['#doctrine'] inside my service.yml, I get an error that says that doctrine is undefined
EDIT 3 : It works now, I just made a dumb mistake
If you read further, it says the following (emphasis mine):
The real implementation of the user provider will probably have some dependencies or configuration options or other services. Add these as arguments in the service definition.
So in your case it would be something like
# app/config/services.yml
services:
app.webservice_user_provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ['#doctrine']
And your class needs a constructor
class WebserviceUserProvider implements UserProviderInterface
{
protected $doctrine;
public function __construct (\Doctrine\Bundle\DoctrineBundle\Registry $doctrine)
{
$this->doctrine = $doctrine;
}
// ...
}
Then in your method replace $this->getDoctrine() with just $this->doctine
I used this version to implement the 2 bundles. However, I want the user to set a password for his account so after successful authentification and user creation, he'll be refirected to a form where he'll enter that. Since FOSUBUserProvider is a service, I was thinking to make another service that will handle the password form. I injected the second service into the first one but I need #templating which I've set up as a parameter but I have no idea how to take it and I'm getting a warning that he's missing. How do I solve this?
# /FOSUBUserProvider
$passwordSetter = Controller::get('register_social_password_picker');
I understand that it needs a second parameter(for templating), where do I take it from? Or am I approaching this the wrong way?
class RegisterPassword
{
protected $user;
protected $templating;
public function __construct($user, $templating)
{
$this->user = $user;
$this->templating = $templating;
}
...
}
services.yml
my_user_provider:
class: AppBundle\Security\Core\FOSUBUserProvider
arguments: [#fos_user.user_manager,{facebook: facebook_id, twitter: twitter_id, linkedin: linkedin_id}, "#register_password" ]
register_password:
class: AppBundle\Service\RegisterPassword
arguments: ["#templating" ]
LE:
services.yml
register_password:
class: AppBundle\Service\RegisterPassword
arguments: [ setMailer, ["#templating"] ]
class RegisterPassword
{
protected $user;
protected $templating;
public function __construct( )
{
}
public function setPassword(User $user, $templating)
{
$this->templating = $templating;
...
}
Your problem is that the service constructor is expecting two arguments ($user and $templating) and you are passing only one (#templating). These parameters are passed when the service is built, so my guess is that you do not want $user to be a parameter for the constructor (at this point you do not know which user is going to use the service), so just pass the $templating parameter and you can add the user later on with a different call.
Since this seems to generate a circular reference problem, you may use one of three different strategies:
Inject the full service container and get the templating service when you need it (not recommended)
Use setter injection (http://symfony.com/doc/current/components/dependency_injection/types.html)
Since this calls the setter method after the service is built my guess is that the circular reference will be avoided (not 100% sure)
If this does not work, create the setter method but do not call it at service construction point but rather call it later when you want to use the service
However, the approach I tried was a little off, the redirection after login can be easily set in security.yml>security>firewalls>firewall_name>default_target_path
Mine looks like:
default_target_path: /redirect
I have model class that calls mailer class inside one of its methods:
class someModel{
public function sendEmail($data){
$mailer = new Mailer();
$mailer->setFrom($data['from']);
$mailer->setTo($data['to']);
$mailer->setSubject($data['subject']);
return $mailer->send();
}
}
How can I test sendEmail method? Maybe I should mock mailer class and check if all these mailer methods were called in sendMail method?
Your help would be appreciated.
IMO wrapping the Mailer class does not solve the problem you're facing, which is you don't have control over the Mail instance being used.
The problem comes from creating the dependencies inside the object that needs them instead of injecting them externally like this:
class someModel{
private $mailer;
public function __construct(Mailer $mailer) {
$this->mailer = $mailer;
}
public function sendEmail($data){
$this->mailer->setFrom($data['from']);
$this->mailer->setTo($data['to']);
$this->mailer->setSubject($data['subject']);
return $this->mailer->send();
}
}
When creating the someModel instance, you must pass a Mail instance (which is an external dependency). And in the test you can pass a Mail mock that will check that the correct calls are being made.
Alternative:
If you feel that injecting a Mail instance is bad (maybe because there are lots of someModel instances), or you just can't change your code this way, then you could use a Services repository, that will keep a single Mail instance and that allows you to set it externally (again, in the test you would set a mock).
Try a simple one like Pimple.
I would (and have in my own code with Mailer!) wrap your instance of Mailer inside a class that you write. In other words, make your own Email class that uses Mailer under the hood. That allows you to simplify the interface of Mailer down to just what you need and more easily mock it. It also gives you the ability to replace Mailer seamlessly at a later date.
The most important thing to keep in mind when you wrap classes to hide external dependencies is keep the wrapper class simple. It's only purpose is to let you swap out the Email libraries class, not provide any complicated logic.
Example:
class Emailer {
private $mailer = new Mailer();
public function send($to, $from, $subject, $data) {
$this->mailer->setFrom($from);
$this->mailer->setTo($to);
...
return $mailer->send();
}
}
class EmailerMock extends Emailer {
public function send($to, $from, $subject, $data) {
... Store whatever test data you want to verify ...
}
//Accessors for testing the right data was sent in your unit test
public function getTo() { ... }
...
}
I follow the same pattern for all classes/libraries that want to touch things external to my software. Other good candidates are database connections, web services connections, cache connections, etc.
EDIT:
gontrollez raised a good point in his answer about dependency injection. I failed to explicitly mention it, but after creating the wrapper the way you would want to use some form of dependency injection to get it into the code where you want to use it. Passing in the instance makes it possible to setup the test case with a Mocked instance.
One method of doing this is passing in the instance to the constructor as gontrollez recommends. There are a lot of cases where that is the best way to do it. However, for "external services" that I am mocking I found that method became tedious because so many classes ended up needing the instance passed in. Consider for example a database driver that you want to Mock for your tests, but you use in many many different classes. So instead what I do is create a singleton class with a method that lets me mock the whole thing at once. Any client code can then just use the singleton to get access to a service without knowing that it was mocked. It looked something like this:
class Externals {
static private $instance = null;
private $db = null;
private $email = null;
...
private function __construct() {
$this->db = new RealDB();
$this->mail = new RealMail();
}
static function initTest() {
self::get(); //Ensure instance created
$db = new MockDB();
$email = new MockEmail();
}
static function get() {
if(!self::$instance)
self::$instance = new Externals();
return self::$instance;
}
function getDB() { return $this->db; }
function getMail() { return $this->mail; }
....
}
Then you can use phpunit's bootstrap file feature to call Externals::initTest() and all your tests will be setup with the mocked externals!
First, as RyanW says, you should write your own wrapper for Mailer.
Second, to test it, use a mock:
<?php
class someModelTest extends \PHPUnit_Framework_TestCase
{
public function testSendEmail()
{
// Mock the class so we can verify that the methods are called
$model = $this->getMock('someModel', array('setFrom', 'setTo', 'setSubject', 'send'));
$controller->expects($this->once())
->method('setFrom');
$controller->expects($this->once())
->method('setTo');
$controller->expects($this->once())
->method('setSubject');
$controller->expects($this->once())
->method('send');
$model->sendEmail();
}
}
The above code is untested, but it basically mocks the someModel class, creating dummy functions for each each function called within sendEmail. It then tests to make sure each of the functions called by sendEmail is called exactly once when sendEmail is called.
See the PHPUnit docs for more info on mocking.
New to Symfony2, I'm building an app that uses an external API to get data. I created a lot of client classes to retrieve and transform each entity from the API, and I defined those classes as services - e.g., I have a FooClient with methods like getAll() or getThoseThatInterestMe($me), which return data from the API.
Now I wanted to create a ApiClientFacade class, which acts as an interface in front of all the XxxClient classes, following the Facade Pattern - e.g., this facade class would have a method getAllFoo(), which in turn would call FooClient::getAll(), and so on...
I could define my facade class as a service as well, but it'd have too many dependencies - I have around 30 client classes. Also, afaik with this approach I'd be loading all 30 dependencies every time, while most of the times I'd only need one dependency...
So, is there a better way to do this?
Use additional ApiClientFactory to move responsibility about "instantiation of ApiClient objects" from your ApiFacade class (which is your initial idea, as I understood).
In some pseudo-php code my idea is:
$api = new ApiFacade(new ApiClientFactory);
$api->sendNotificationAboutUserLogin('username', time());
An example of method:
class ApiFacade {
private $apiFactory;
public function __construct(ApiClientFactory $factory)
{
$this->apiFactory = $factory;
}
public function sendNotificationAboutUserLogin($username, $timeOfOperation)
{
return $this->apiFactory
->createApi('User')
->post(
'notifications',
array('operation' => 'login', 'username' => $username, 'timeOfOperation' => $timeOfOperation)
);
}
}
In this case your Facade class stays injectable (testable), but also becomes simpler instantiatable (you don't need to pass all dependencies into it anymore).
The ApiClientFactory should look like that:
class ApiClientFactory {
private $apiBaseUrl;
public function __construct($apiBaseUrl)
{
$this->apiBaseUrl = $apiBaseUrl;
}
public function createApi($apiName)
{
switch ($apiName) {
case 'User': return new \My\UserApi($this->apiBaseUrl);
default: // throw an exception?
}
}
}
I am trying to send SMS to users who register in my website through Twilio, I got the vresh/twilio-bundle and it works fine.
I am trying to pass twilio instance to the event but I think I am missing something, here is what I am doing:
In config.yml i set the servide like this:
services:
registration.completed.listener:
class: Jaguar\AloBundle\EventListener\RegistrationEventListener
arguments:
entityManager: ["#doctrine.orm.voipswitch_entity_manager", "vresh_twilio"]
tags:
- { name: kernel.event_subscriber, event: performOnRegistrationCompleted }
I have declared the twilio config:
vresh_twilio:
sid: 'xxx'
authToken: 'xxx'
version: '2010-04-01'
retryAttempts: 3
Then, in my method I try to get the instance:
public function performOnRegistrationCompleted(UserEvent $event)
{
$twilio = $event->get('vresh_twilio');
}
But it fails...
Any help on this, please?
Thanks a lot!
There are a few issues with your service setup.
You are not actually passing the Twilio instance as you have no # sign preceding the service name. #vresh_twilio is a service, vresh_twilio is just a string.
You are passing in an associative array with a key of entityManager and a value that is also an array with the values of the service #doctrine.orm.voipswitch_entity_manager and the string vresh_twilio.
You're not passing the Twilio instance in your event you are building a listener with the Twilio instance in the constructor.
Your service should actually look like...
services:
registration.completed.listener:
class: Jaguar\AloBundle\EventListener\RegistrationEventListener
arguments:
entityManager: "#doctrine.orm.voipswitch_entity_manager"
twilio: "#vresh_twilio"
// Or
// - #doctrine.orm.voipswitch_entity_manager
// - #vresh_twilio
// Or
// [#doctrine.orm.voipswitch_entity_manager, #vresh_twilio]
//
// As they all mean the same thing and the keys aren't
// used in your actual service __construct
tags:
- { name: kernel.event_subscriber, event: performOnRegistrationCompleted }
This would mean your listener would then have a constructor to receive those services like..
protected $entityManager;
protected $twilio;
public function __conctruct(ObjectManager $entityManager, TwilioWrapper $twilio)
{
$this->entityManager = $entityManager;
$this->twilio = $twilio;
}
Meaning that you could then call it in your class using $this->twilio.
Also, from looking at the services that the Vresh\TwilioBundle creates it looks like the service that you would want to be injecting would be #twilio.api rather than #vresh_twilio as it doesn't seem to exist but I may be wrong there (I haven't used the bundle myself).