Symfony 2 Class Constructer Not Getting ContainerInterface? - php

ok, So I am still a little new to Symfony 2 but I have been all over the web trying to see what I am doing wrong, but going over and over the Symfony docs, I can't see why its not working.
So I have tow points in my app that will need to send two new user emails, one with a password and one with an active path to verify the email. I am using MailGun to send the emails, this works fine. But has I have two controllers that will send these emails, I thought that if I wanted to change/edit them, it would be best if they where in the same place. So I build my own class for them to go in.
This is the problem, as it is not an controller I am trying to 'render' an Standard Email template layout. And for the life of me can not figure out why its not working.
So my Class:
namespace xxxBundle\Classes;
//use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class NewUserEmails {
//private $templating;
//public function __construct(EngineInterface $templating) {
// $this->templating = $templating;
//}
private $container;
public function __construct(ContainerInterface $container) {
$this->templating = $container->get('templating');
}
public function SendActivePathEmail($GetNewUserID, $Token) {
/* Send Active Path To New User - Before Password */
$Email_Header_ActiveUser = 'Access';
$Email_Content_ActiveUser = 'Please active your account, by clicking the link below.';
$Email_Content_ActiveUser .= '<br/><br/><strong>PLEASE NOTE:</strong> Your account is not active untill you have vifyied your email';
$Email_Content_ActiveUser .= '<br/><br/><p>Active Account Link</p>';
$ActiveUserEmail = $this->templating->render('xxxBundle:Emails:StandardEmail.html.twig',
['EmailContent' => $Email_Header_ActiveUser,
'EmailMessage' => $Email_Content_ActiveUser],
'text/html');
return $ActiveUserEmail;
}
public function SendPswEmail($PlainPwd) {
/* Send Password To New User */
$Email_Header_NewPsw = 'Access';
$Email_Content_NewPsw = 'This is your password <strong>' .$PlainPwd. '</strong> for xxxxx login';
$PasswordEmail = $this->templating->render('xxxBundle:Emails:StandardEmail.html.twig',
['EmailContent' => $Email_Header_NewPsw,
'EmailMessage' => $Email_Content_NewPsw],
'text/html');
return $PasswordEmail;
}
} //Class End
Now this is what I have in my services YML file,
new_user_emails:
class: xxxBundle\Classes\NewUserEmails
arguments: [#service_container]
This is the services file within my bundle, which I know is being loaded has I have a login handler which works without any problems.
And this is how I am calling the class within my Controller,
$GetNewUserEmails = new NewUserEmails();
$ActiveUserEmail = $GetNewUserEmails->SendActivePathEmail($GetNewUserID, $Token);
$PasswordEmail = $GetNewUserEmails->SendPswEmail($PlainPwd);
So has far as I can tell I am doing it right, but when I try to save the user (which does save without any problems) I get the following error,
Catchable Fatal Error: Argument 1 passed to xxxxBundle\Classes\NewUserEmails::__construct() must implement interface Symfony\Component\DependencyInjection\ContainerInterface, none given
P.S. I have tried to insert just the tempting, but that gave me the same error!
All help most welcome,
What am I doing wrong?
Thanks,

You need to retrieve your service from the symfony container.
try this:
$GetNewUserEmails = $this->get('new_user_emails');
$ActiveUserEmail = $GetNewUserEmails->SendActivePathEmail($GetNewUserID, $Token);
$PasswordEmail = $GetNewUserEmails->SendPswEmail($PlainPwd);
Instead of this:
$GetNewUserEmails = new NewUserEmails();
$ActiveUserEmail = $GetNewUserEmails->SendActivePathEmail($GetNewUserID, $Token);
$PasswordEmail = $GetNewUserEmails->SendPswEmail($PlainPwd);
PS: Is not a good practice to pass the whole container to the service, but only the service it really needed.
Hope this help

Related

Design pattern that handles multiple steps

So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences

Mocking a service called by a controller from a WebTestCase

I have an API written using Symfony2 that I'm trying to write post hoc tests for. One of the endpoints uses an email service to send a password reset email to the user. I'd like to mock out this service so that I can check that the right information is sent to the service, and also prevent an email from actually being sent.
Here's the route I'm trying to test:
/**
* #Route("/me/password/resets")
* #Method({"POST"})
*/
public function requestResetAction(Request $request)
{
$userRepository = $this->get('app.repository.user_repository');
$userPasswordResetRepository = $this->get('app.repository.user_password_reset_repository');
$emailService = $this->get('app.service.email_service');
$authenticationLimitsService = $this->get('app.service.authentication_limits_service');
$now = new \DateTime();
$requestParams = $this->getRequestParams($request);
if (empty($requestParams->username)) {
throw new BadRequestHttpException("username parameter is missing");
}
$user = $userRepository->findOneByUsername($requestParams->username);
if ($user) {
if ($authenticationLimitsService->isUserBanned($user, $now)) {
throw new BadRequestHttpException("User temporarily banned because of repeated authentication failures");
}
$userPasswordResetRepository->deleteAllForUser($user);
$reset = $userPasswordResetRepository->createForUser($user);
$userPasswordResetRepository->saveUserPasswordReset($reset);
$authenticationLimitsService->logUserAction($user, UserAuthenticationLog::ACTION_PASSWORD_RESET, $now);
$emailService->sendPasswordResetEmail($user, $reset);
}
// We return 201 Created for every request so that we don't accidently
// leak the existence of usernames
return $this->jsonResponse("Created", $code=201);
}
I then have an ApiTestCase class that extends the Symfony WebTestCase to provide helper methods. This class contains a setup method that tries to mock the email service:
class ApiTestCase extends WebTestCase {
public function setup() {
$this->client = static::createClient(array(
'environment' => 'test'
));
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$this->mockEmailService = $mockEmailService;
}
And then in my actual test cases I'm trying to do something like this:
class CreatePasswordResetTest extends ApiTestCase {
public function testSendsEmail() {
$this->mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
}
So now the trick is to get the controller to use the mocked version of the email service. I have read about several different ways to achieve this, so far I've not had much luck.
Method 1: Use container->set()
See How to mock Symfony 2 service in a functional test?
In the setup() method tell the container what it should return when it's asked for the email service:
static::$kernel->getContainer()->set('app.service.email_service', $this->mockEmailService);
# or
$this->client->getContainer()->set('app.service.email_service', $this->mockEmailService);
This does not effect the controller at all. It still calls the original service. Some write ups I've seen mention that the mocked service is 'reset' after a single call. I'm not even seeing my first call mocked out so I'm not certain this issue is affecting me yet.
Is there another container I should be calling set on?
Or am I mocking out the service too late?
Method 2: AppTestKernel
See: http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
See: Symfony2 phpunit functional test custom user authentication fails after redirect (session related)
This one pulls me out of my depth when it comes to PHP and Symfony2 stuff (I'm not really a PHP dev).
The goal seems to be to change some kind of foundation class of the website to allow my mock service to be injected very early in the request.
I have a new AppTestKernel:
<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
And a new method in my ApiTestCase:
// https://stackoverflow.com/a/19705215
protected static function getKernelClass(){
$dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name('*TestKernel.php')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
}
$file = current($results);
$class = $file->getBasename('.php');
require_once $file;
return $class;
}
Then I alter my setup() to use the kernel modifier:
public function setup() {
...
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
static::$kernel->setKernelModifier(function($kernel) use ($mockEmailService) {
$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
});
$this->mockEmailService = $mockEmailService;
}
This works! However I now can't access the container in my other tests when I'm trying to do something like this:
$c = $this->client->getKernel()->getContainer();
$repo = $c->get('app.repository.user_password_reset_repository');
$resets = $repo->findByUser($user);
The getContainer() method returns null.
Should I be using the container differently?
Do I need to inject the container into the new kernel? It extends the original kernel so I don't really know why/how it's any different when it comes to the container stuff.
Method 3: Replace the service in config_test.yml
See: Symfony/PHPUnit mock services
This method requires that I write a new service class that overrides the email service. Writing a fixed mock class like this seems less useful than a regular dynamic mock. How can I test that certain methods have been called with certain parameters?
Method 4: Setup everything inside the test
Going on #Matteo's suggestion I wrote a test that did this:
public function testSendsEmail() {
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
static::$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
$this->client->getContainer()->set('app.service.email_service', $mockEmailService);
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
This test fails because the expected method sendPasswordResetEmail wasn't called:
There was 1 failure:
1) Tests\Integration\Api\MePassword\CreatePasswordResetTest::testSendsEmail
Expectation failed for method name is equal to <string:sendPasswordResetEmail> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
Thanks to Cered's advice I've managed to get something working that can test that the emails I expect to be sent actually are. I haven't been able to actually get the mocking to work so I'm a bit reluctant to mark this as "the" answer.
Here's a test that checks that an email is sent:
public function testSendsEmail() {
$this->client->enableProfiler();
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
$mailCollector = $this->client->getProfile()->getCollector('swiftmailer');
$this->assertEquals(1, $mailCollector->getMessageCount());
$collectedMessages = $mailCollector->getMessages();
$message = $collectedMessages[0];
$this->assertInstanceOf('Swift_Message', $message);
$this->assertEquals('Reset your password', $message->getSubject());
$this->assertEquals('info#example.com', key($message->getFrom()));
$this->assertEquals($this->user->getEmail(), key($message->getTo()));
$this->assertContains(
'This link is valid for 24 hours only.',
$message->getBody()
);
$resets = $this->getResets($this->user);
$this->assertContains(
$resets[0]->getToken(),
$message->getBody()
);
}
It works by enabling the Symfony profiler and inspecting the swiftmailer service. It's documented here: http://symfony.com/doc/current/email/testing.html

How to all actions under a controller as resource in Zend Acl

I am trying to follow a tutorial for Zend Auth and Zend Acl using 1.11 framework Link here!
I have setup the authentication successfully and am able to use the authentication for the controller::action pairs given in the Acl.php page. Firstly I would like to test two additional parameter on the users table that whether the user account is activated and if the user is banned by administrator before allowing access to the site. How do I implement that in this code.
Secondly I would like to know how to include all actions under one controller to a User authorization level. i.e. I have a masters controller which has numerous actions under it for various tables. Could you tell me how to restrict access to Masters controller all actions to admin role only. Without adding resources and allow resources for each action in Acl.php. Also please tell me if this logic can be extended to allow access over entire modules instead of just the controllers(by one add resource and allow resource)? If yes how?
Firstly I would like to test two additional parameter on the users
table that whether the user account is activated and if the user is
banned by administrator before allowing access to the site.
The tutorial code uses a vanilla version of Zend_Auth_Adapter_DbTable which uses a specific api for authentication. To make Zend_Auth work how you want it to is not very difficult but will require some thought as you'll need to implement Zend_Auth_Adapter_Interface. Sounds worse then it is, you only have to implement the authenticate() method. Here is an example of an auth adapter that can be used in place of Zend_Auth_Adapter_DbTable:
<?php
//some code truncated for length and relevance
class My_Auth_Adapter implements Zend_Auth_Adapter_Interface
{
protected $identity = null;
protected $credential = null;
protected $usersMapper = null;
public function __construct($username, $password, My_Model_Mapper_Abstract $userMapper = null)
{
if (!is_null($userMapper)) {
$this->setMapper($userMapper);
} else {
$this->usersMapper = new Users_Model_Mapper_User();
}
$this->setIdentity($username);
$this->setCredential($password);
}
/**
* #return \Zend_Auth_Result
*/
public function authenticate()
{
// Fetch user information according to username
$user = $this->getUserObject();
if (is_null($user)) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND,
$this->getIdentity(),
array('Invalid username')
);
}
// check whether or not the hash matches
$check = Password::comparePassword($this->getCredential(), $user->password);
if (!$check) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
$this->getIdentity(),
array('Incorrect password')
);
}
// Success!
return new Zend_Auth_Result(
Zend_Auth_Result::SUCCESS,
$this->getIdentity(),
array()
);
}
// public function setIdentity($userName)
// public function setCredential($password)
// public function setMapper($mapper)
/**
* #return object
*/
private function getUserObject()
{
return $this->getMapper()->findOneByColumn('username', $this->getIdentity());
}
/**
* #return object
*/
public function getUser()
{
$object = $this->getUserObject();
$array = array(
'id' => $object->id,
'username' => $object->username,
'role' => $object->getRoleId()
);
return (object) $array;
}
// public function getIdentity()
// public function getCredential()
// public function getMapper()
}
You can modify the auth adapter to do pretty much anything you need.
As far as your access list is concerned, the thing to remember is that you resources are defined by a string. In the case of this tutorial a resource is defined as:
$this->add(new Zend_Acl_Resource('error::error'));
where the string on the left side of the colon represents the controller and the string on the right side of the colon represents the action. it's this line in the acl plugin that tell's us what the resources are:
if(!$acl->isAllowed($user->role, $request->getControllerName() . '::' . $request->getActionName()))
you can change this definition of what your resources represent to anything that works for you.
It's very difficult to provide hard and fast rules on how to implement an ACL because it seems that every project needs something different.
Look around the web and you'll find several different implementations of a Zend Framework ACL, some of them can be very complex.
Here is one that might provide some more insight. http://codeutopia.net/blog/2009/02/06/zend_acl-part-1-misconceptions-and-simple-acls/
good luck

different module based on hostname

I have build a CMS using Zend Framework (1.11). In the application I have two modules, one called 'cms' which contains all the CMS logic and an other 'web' which enables a user to build their own website around the CMS. This involves adding controllers/views/models etc all in that module.
The application allows you to serve multiple instances of the app with their own themes. These instances are identified by the hostname. During preDispatch(), a database lookup is done on the hostname. Based on the database field 'theme' the app then loads the required css files and calls Zend_Layout::setLayout() to change the layout file for that specific instance.
I want to extend this functionality to also allow the user to run different web modules based on the 'theme' db field. However, this is where I am stuck. As it is now, the web module serves the content for all the instances of the application.
I need the application to switch to a different web module based on the 'theme' database value (so indirectly the hostname). Any ideas?
Well, in my opinion,
You should write a front controller plugin for the web module, and do it so, that when you need another plugin, you can do so easily.
The front controller plugin should look something like this:
class My_Controller_Plugin_Web extends My_Controller_Plugin_Abstract implements My_Controller_Plugin_Interface
{
public function init()
{
// If user is not logged in - send him to login page
if(!Zend_Auth::getInstance()->hasIdentity()) {
// Do something
return false;
} else {
// You then take the domain name
$domainName = $this->_request->getParam( 'domainName', null );
// Retrieve the module name from the database
$moduleName = Module_fetcher::getModuleName( $domainName );
// And set the module name of the request
$this->_request->setModuleName( $moduleName );
if(!$this->_request->isDispatched()) {
// Good place to alter the route of the request further
// the way you want, if you want
$this->_request->setControllerName( $someController );
$this->_request->setActionName( $someAction );
$this->setLayout( $someLayout );
}
}
}
/**
* Set up layout
*/
public function setLayout( $layout )
{
$layout = Zend_Layout::getMvcInstance();
$layout->setLayout( $layout );
}
}
And the My_Controller_Plugin_Abstract, which is not an actual abstract and which your controller plugin extends,looks like this:
class My_Controller_Plugin_Abstract
{
protected $_request;
public function __construct($request)
{
$this->setRequest($request);
$this->init();
}
private function setRequest($request)
{
$this->_request = $request;
}
}
And in the end the front controller plugin itself, which decides which one of the specific front controller plugins you should execute.
require_once 'Zend/Controller/Plugin/Abstract.php';
require_once 'Zend/Locale.php';
require_once 'Zend/Translate.php';
class My_Controller_Plugin extends Zend_Controller_Plugin_Abstract
{
/**
* before dispatch set up module/controller/action
*
* #param Zend_Controller_Request_Abstract $request
*/
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
// Make sure you get something
if(is_null($this->_request->getModuleName())) $this->_request->setModuleName('web');
// You should use zend - to camelCase convertor when doing things like this
$zendFilter = new Zend_Filter_Word_SeparatorToCamelCase('-');
$pluginClass = 'My_Controller_Plugin_'
. $zendFilter->filter($this->_request->getModuleName());
// Check if it exists
if(!class_exists($pluginClass)) {
throw new Exception('The front controller plugin class "'
. $pluginClass. ' does not exist');
}
Check if it is written correctly
if(!in_array('My_Controller_Plugin_Interface', class_implements($pluginClass))) {
throw new Exception('The front controller plugin class "'
. $pluginClass.'" must implement My_Controller_Plugin_Interface');
}
// If all is well instantiate it , and you will call the construct of the
// quasi - abstract , which will then call the init method of your
// My_Plugin_Controller_Web :)
$specificController = new $pluginClass($this->_request);
}
}
If you have never done this, now is the time. :)
Also, to register your front controller plugin with the application, you should edit the frontController entry in your app configuration. I will give you a json example, i'm sure you can translate it to ini / xml / yaml if you need...
"frontController": {
"moduleDirectory": "APPLICATION_PATH/modules",
"defaultModule": "web",
"modules[]": "",
"layout": "layout",
"layoutPath": "APPLICATION_PATH/layouts/scripts",
// This is the part where you register it!
"plugins": {
"plugin": "My_Controller_Plugin"
}
This might be a tad confusing, feel free to ask for a more detailed explanation if you need it.

zend session exception on zend_session::start with forms

I'm having issues with trying to use Zend_Form_SubForm and sessions. My controller is in essance acting a wizard showing different subforms depending on the stage of the wizard. Using the example I am planning on storing the forms in a session namespace.
My controller looks like this.
include 'mylib/Form/addTaskWizardForm.php';
class AddtaskController extends Zend_Controller_Action{
private $config = null;
private $log = null;
private $subFormSession = null;
/**
* This function is called and initialises the global variables to this object
* which is the configuration details and the logger to write to the log file.
*/
public function init(){
$this->config = Zend_Registry::getInstance()->get('config');
$this->log = Zend_Registry::getInstance()->get('log');
//set layout
$this->_helper->layout->setLayout('no-sidemenus');
//we need to get the subforms and
$wizardForms = new addTaskWizardForm();
$this->subFormSession = new Zend_Session_Namespace('addTaskWizardForms');
if(!isset($this->subFormSession->subforms)){
$this->subFormSession->subforms = $wizardForms;
}
}
/**
* The Landing page controller for the site.
*/
public function indexAction(){
$form = $this->subFormSession->subforms->getSubForm('start');
$this->view->form = $form;
}
However this is causing the application session to crash out with
Uncaught exception 'Zend_Session_Exception' with message 'Zend_Session::start()
Any idea why this is having issues with the Zend Session??
thanks.
Make sure there are no spaces, new lines or any other characters sent before the session start. Especially if you have include and your <?php is prepended with space or starts on 2nd line of the file.
It's very weird, the only place I see that a message like that is sent is on lines 435 to 446 of Zend/Session.php.
Are you trying to run that code through a unit test?, check that there isn't any headers sent before initializing the Session.

Categories