I'm trying to send an email from a CakePHP shell just as you would from the Controller.
Most of the code below was adapted from this dated article on the Bakery and it's comments. The email is sending, however the line $controller->set('result', $results[$i]); throws the following notices:
Notice: Undefined property:
View::$webroot in
/home/jmccreary/www/intranet.sazerac.com/cakephp/cake/libs/view/view.php
on line 813
PHP Notice: Undefined
variable: result in
/home/jmccreary/www/intranet.sazerac.com/cakephp/app/views/elements/email/text/nea/task_reminder_it.ctp
on line 2
So I'm not getting any of the variables passed to my email view.
How can I do this, ideally following the Cake conventions?
class NotificationShell extends Shell {
var $uses = array('Employee', 'Task');
function main() {
// run if no action is passed
}
function nea_task_reminder() {
// build Task to Employee relationship
$this->Task->bindModel(array('belongsTo' => array('Employee' => array('className' => 'Employee', 'foreignKey' => 'object_id'))));
$results = $this->Task->find('all', array('conditions' => array('application_id' => 1, 'completed_by_id' => 0), 'contain' => array('Employee' => array('Contact', 'Position'))));
$count = count($results);
if ($count) {
App::import('Core', 'Controller');
App::import('Component', 'Email');
$controller =& new Controller();
$email =& new EmailComponent();
$email->startup($controller);
// send email
$email->from = Configure::read('Email.from');
$email->to = 'jmccreary#whatever.com';
$email->replyTo = 'no-reply#whatever.com';
$email->template = 'nea/task_reminder_it';
$email->sendAs = 'text';
for ($i = 0; $i < $count; ++$i) {
$email->subject = 'NEA Notification: Task Reminder for ' . $results[$i]['Employee']['Contact']['full_name'];
$controller->set('result', $results[$i]);
$email->send();
}
}
}
}
The problem is the way you're initializing the EmailComponent class. If you look at the source code, the startup() method doesn't actually have a body so it does nothing. Your controller isn't actually assigned to the EmailComponent. The problem isn't $controller->set('results', ...);. You need to use EmailComponent::initialize() instead of EmailComponent::startup().
$controller =& new Controller();
$email =& new EmailComponent(null);
$email->initialize($controller);
Sources:
Comments section of http://bakery.cakephp.org/articles/Jippi/2007/12/02/emailcomponent-in-a-cake-shell
EmailComponent::startup() Source
If you're using CakePHP 2.x, you can ditch the EmailComponent entirely and use the CakeEmail class instead.
App::uses('CakeEmail', 'Network/Email');
class NotificationShell extends Shell {
public function send() {
$email = new CakeEmail();
}
}
That entirely avoids all the thorny issues of loading components inside a shell. For email at least.
If you're using CakePHP 2.x, try to use CakeEmail instead.
CakeEmail#viewVars() provides setting variables to template.
Here is example using CakeEmail from Shell.
https://gist.github.com/tsmsogn/cee9cef2e851e7684021
Try this.
App::import('Core', 'Controller');
App::import('Component', 'Email');
$this->Controller =& new Controller();
$this->Email =& new EmailComponent(null);
$this->Email->initialize($this->Controller);
//use set function as below
$this->controller->set('result', $results[$i]);
for more reference click on below link:
http://ask.cakephp.org/questions/view/cron_shell_-_mail
Related
I'm just trying a very simple test
<?php
require 'vendor/autoload.php';
class Blog
{
public function post ()
{
return 'ok';
}
}
$builder = new \Aura\Di\ContainerBuilder();
$blog = $builder->newInstance('Blog');
echo $blog->post();
This results to:
Fatal error: Uncaught Error: Call to undefined method Aura\Di\Container::post()
Am I missing something?
Yes , you are missing to read the docs. You have created builder. Next you need to get the di via new instance. This is what you assigned to blog variable.
Please consider reading getting started http://auraphp.com/packages/3.x/Di/getting-started.html#1-1-1-2
// autoload and rest of code
$builder = new \Aura\Di\ContainerBuilder();
$di = $builder->newInstance();
Now you create instance of object
$blog = $di->newInstance('Blog');
echo $blog->post();
Please read the docs.
I'm not new to frameworks and work with Symfony quite often but I have a legacy version of CakePHP 2.4 that I must maintain for now and am having an issue. My code is this:
function aff($user=null,$pid=null) {
if (empty($user) || empty($pid)) $this->redirect('/');
$userModel =& ClassRegistry::init('User');
$productModel =& ClassRegistry::init('Product');
$granularModel =& ClassRegistry::init('Granular');
$this->autoRender = false;
$temp = array(
'user' => $user,
'pid' => $pid
);
$granularModel->save($temp);
}
This is a function inside my controller. The User and Product models are being ClassRegistry::init just fine and work correctly BUT my Granular model causes the page to load with a message of "The requested address XXXX was not found on this server." Here is my model:
<?php
class Granular extends AppModel {
var $name = 'Granular';
}
?>
Why is it causing the route not to be found? If I put an "echo 'Hello'; exit();" right after the line $productModel =& ClassRegistry::init('Product'); it loads the route just fine. I feel lost. Please help. TIA
For some reason today after turning on Configure::write('debug', 1); in app/config/core.php, it suddenly started working. I am guessing at this point that it must have been a caching issue of some sort.
I'm using Mockery in my Laravel based PHP project to help test a Laravel MVC controller. Below is the relevant part of my controller class I'm trying to test.
class DevicesController extends Controller
{
private $deviceModel;
private $rfDeviceModel;
private $userModel;
private $userDeviceModel;
public function __construct(Device $deviceModel, RFDevice $rfDeviceModel, User $userModel, UserDevice $userDeviceModel)
{
$this->middleware('guest');
$this->deviceModel = $deviceModel;
$this->rfDeviceModel = $rfDeviceModel;
$this->userModel = $userModel;
$this->userDeviceModel = $userDeviceModel;
}
...
public function add(Request $request)
{
$name = $request->input('name');
$description = $request->input('description');
$onCode = $request->input('onCode');
$offCode = $request->input('offCode');
$pulseLength = $request->input('pulseLength');
$type = 1;
$currentUserId = $this->currentUser()->id;
$newDeviceId = $this->deviceModel->add($name, $description, $type)->id;
$this->rfDeviceModel->add($onCode, $offCode, $pulseLength, $newDeviceId);
$this->userDeviceModel->add($currentUserId, $newDeviceId);
return redirect()->route('devices');
}
}
In particular, I'm writing several unit tests around the controller's add(Request $request) function to make sure that each of the three model add(...) functions are called. My test case to handle this looks like the following:
public function testAdd_CallsAddForModels()
{
$mockDeviceModel = Mockery::mock(Device::class);
$mockDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(Device::class, $mockDeviceModel);
$mockRFDeviceModel = Mockery::mock(RFDevice::class);
$mockRFDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(RFDevice::class, $mockRFDeviceModel);
$mockUserDeviceModel = Mockery::mock(UserDevice::class);
$mockUserDeviceModel->shouldReceive('add')->withAnyArgs()->once();
$this->app->instance(UserDevice::class, $mockUserDeviceModel);
$user = $this->givenSingleUserExists();
$this->addDeviceForUser($user->user_id);
}
private function givenSingleUserExists()
{
$user = new User;
$name = self::$faker->name();
$email = self::$faker->email();
$userId = self::$faker->uuid();
$user = $user->add($name, $email, $userId);
return $user;
}
private function addDeviceForUser($userId)
{
$this->withSession([env('SESSION_USER_ID') => $userId])
->call('POST', '/devices/add', [
'name' => 'Taylor',
'description' => 'abcd',
'onCode' => 1,
'offCode' => 2,
'pulseLength' => 3
]);
}
When I run this test, I get the following output in the console:
There was 1 error:
1) Tests\Unit\Controller\DeviceControllerTest::testAdd_CallsAddForModels
Mockery\Exception\InvalidCountException: Method add() from Mockery_1_App_RFDevice should be called
exactly 1 times but called 0 times.
But the funny and perplexing thing is that if I comment out and combination of 2 of the 3 mockery sections, my test pass. This means to mean, that my code is actually working correctly, but for some reason in this case, I can't inject multiple mocked model objects into my controller and test them all at once. I guess I could split this up into three separate tests that make sure each model's add(...) function is called, but I want to do it all in one test case if possible. I also know I could use a repository pattern to wrap all the business logic in the controller's add(...) function into a single call, but then I would run into the same problem while testing the repository class.
You're not mocking the return values of the methods so this line attempts to access an attribute (id) on a null.
$newDeviceId = $this->deviceModel->add($name, $description, $type)->id;
You can fix this by adding a return value to your Device model mock like so:
$mockDeviceModel = Mockery::mock(Device::class);
$device = new Device;
$mockDeviceModel->shouldReceive('add')->withAnyArgs()->once()->andReturn($device);
To make such problems easier to debug in the future, change your error handler to re-throw the exceptions in a testing environment instead of rendering a valid HTML response.
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
In my controller i'm using a email function with the following code:
public function email($mail = null){
$email = new CakeEmail('default');
$email->config('default');
$email->from(array('test#test.com' => 'testing'));
$email->to('$mail');
$email->subject('Approved');
$email->send('Approved');
At the top i have
App::uses('AppController', 'Controller', 'CakeEmail', 'Network/Email');
However, i receive the error Fatal error: Class 'CakeEmail' not found in.
I'm not sure where i have gone wrong. Can anybody please assist?
You need to change your App::uses and separate the two:
App::uses('AppController', 'Controller');
App::uses('CakeEmail', 'Network/Email');
App::uses() does only allow two arguments: $className and $location. You passed 4 arguments, that's why CakeEmail is not loaded.
See http://api20.cakephp.org/class/app#method-Appuses and http://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::uses for more information
the documentation is pretty clear about it:
http://book.cakephp.org/2.0/en/core-utility-libraries/email.html?highlight=cakeemail#CakeEmail
"First of all, you should ensure the class is loaded"
on a second look: your app::uses() is wrong. check out the way it is documented.
You can use the Email component in the controller
public $components = array('Email');
public function email(){
$this->Email->to = 'yourmail#mail.com';
$this->Email->subject = 'Subject - ';
$this->Email->from = 'sender#mail.com';
$this->Email->send('message');
}