I have a very simple model. I want to add a send email routine to on of the methods for the model:
$this->Email->delivery = 'smtp';
$this->Email->template = 'default';
$this->Email->sendAs = 'text';
$this->Email->from = 'email';
$this->Email->to = 'email';
$this->Email->subject = 'Error';
I've tried putting
App::import('Component', 'Email');
at the top, to no avail. The error I get is:
Fatal error: Call to undefined method stdClass::send() in E:\xampp\htdocs8080\app\models\debug.php on line 23
Any ideas?
I'm running CakePHP 1.2
even if it is not best practice, you actually can use the EmailComponent in a model, but you need to instanciate it (in the Models there is no automatic Component loading) and you need to pass it a controller. The EmailComponent relies on the Controller because of the connection it needs to the view, for rendering email templates and layouts.
With a method like this in your model
function sendEmail(&$controller) {
App::import('Component', 'Email');
$email = new EmailComponent();
$email->startup($controller);
}
You can use it in your Controller like this:
$this->Model->sendEmail($this);
(omit the & in the method signature if you're on PHP5)
Well, you're doing it wrong way. You should place email send routine in your AppController:
function _sendMail($to,$subject,$template) {
$this->Email->to = $to;
// $this->Email->bcc = array('secret#example.com'); // copies
$this->Email->subject = $subject;
$this->Email->replyTo = 'noreply#domain.com';
$this->Email->from = 'MyName <noreply#domain.com>';
$this->Email->template = $template;
$this->Email->sendAs = 'text'; //Send as 'html', 'text' or 'both' (default is 'text')
$this->Email->send();
}
Then use it from ANY controller like this:
$this->_sendMail($this->data['User']['email'],'Thanks for registering!','register');
And don't forget to place
var $components = array('Email');
in controllers, in which you 're using _sendMail function.
CakePHP 2.0 has the new CakeEmail class that works anywhere:
http://book.cakephp.org/2.0/en/core-utility-libraries/email.html
Components are supposed to be used in controllers, not models.
In your controller use
var $components = array('Email');
There is no need to use App::import();
Without knowing your app and reasons for wanting to use it in a model, I might suggest you re-think your system architecture and move this logic to your controller.
If you definitely need it in your mode, your code included something like...
$this->Email->delivery = ...
Have you put created a new instance of the component and set it to a property of your model called Email? (No idea if this will work mind.)
The error you are getting is because you are calling the send() method on a the stdClass object i.e. not an instance of the EmailComponent.
OK, true it isn't good to use components in models. My problem was I didn't want to have to write the email block a million times in my controllers:
$this->Email->delivery = 'smtp';
$this->Email->template = $template;
$this->Email->sendAs = 'text';
$this->Email->from = $from;
$this->Email->to = $to;
$this->Email->subject = $subject;
$this->Email->send();
Hardly DRY if I use this 5 times in a controller. So I created a component called Wrapper, and added a sendEmail routine, so I can just write:
$this->Wrapper->sendEmail($from,$to,$subject,$template,$body);
I'm with you Justin.
I have a series of Models that trigger emails based on certain actions that can come from Users, Admins and shell scripts for automated processes.
It is FAR easier to centralize an email response in the Model (like when an Order record is 'cancelled') than to rewrite the email in multiple locations.
Also, I have automated processes in the Models that handle some core 'logic' that cascade to other hasOne, belongsTo or hasMany Models that are biz rules.
For example, a crontabbed shell script calls Offer->expire() to 'expire' an Offer which then calls Offer->make() to make another Offer, but if it can't then it calls Request->expire() to 'expire' the original request. Emails must be sent to first expired Offer recipient, to any new Offer recipients and/or to the requestor if it expires. These can be called by crontabbed shell or by Users or by Admins who can manage Requests and Offers manually. All using different Controllers or interfaces.
This is what I did and can call it inside Models and Controllers:
if(empty($this->MailsController)) {
App::import('Controller','Mails');
$this->MailsController = new MailsController();
$this->MailsController->constructClasses();
$this->MailsController->Email->startup($this->MailsController);
}
Now I am able to call this from just about anywhere and centralize all the logic for what data to find(), what email to generate, whom to send it to, etc. via the following called inside the Model:
$this->MailsController->orderMail($user_id,$this->id,$mode);
Since all of the email logic is basically called by the Models indirectly via MailsController, I am going to give rscherer's code a try.
Hope this helps,
oh4real
I have an app where I add a single row to a model (model1). That model then triggers entries into another model (model2). model1 hasmany model2. I wanted to only send emails if model2 inserts successfully. So I was looking to have model1 send emails each time it inserts successfully into model2.
Restructuring the system architecture is too much time at this point.
My solution? When I do a Model1->addRecord, it does a model2->addRecord(). I have model1 keep track of each failure or success from model2->addRecord. From model1->addRecord I then return an array of success/failure to the invoking controller (model1_controller). From there I will then have model1_controller issue the emails.
So it is possible to do this correctly without having to completely rewrite the architecture. Just return more information from Model1 to the controller, then have the controller (properly) send the emails.
Sorry for the somewhat unclear post. My brain is in codemode right now.
Look into a plugin called Eventful
It will allow you to broadcast and listen for custom events. For the poster below me : you could broadcast a Model1Saved event, listen for it in model2, then perform the second save and them broadcast a success event back to model1, or even a controller. From there you can send your email.
Related
I want to get the render of an email before to send it.
I created a TemplatedEmail with htmlTemplate and context, it works fine for sending but how get the generated template with context to save it in database ? (customer needs)
I tried the getBody() but seems to work only with text template as I get A message must have a text or an HTML part or attachments.
$email = new TemplatedEmail();
$email->htmlTemplate($htmlTemplate);
$email->from($from)->to(...$to)->subject($subject);
$email->context($context);
dd($email->getBody());
I thought to use the render method but I'm in a service and not sure if it's a good way to store in database.
Symfony only renders the message when actually sending it, via an Event Listener. The class responsible from doing the rendering is BodyRenderer, from the Twig Bridge.
But nothing stops you from rendering the message yourself.
You have the template and the context variables, so you could simply inject Twig wherever you are doing the sending, render the template to a string and do whatever you need with that.
You could also register your own MessageEvent::class listener, set it with lower priority than the one registered by the Twig Bundle (it uses the default priority) so it's executed after that one, and then you could access the message body since it would have been rendered already. This is (very) slightly more complex, but you'd gain some performance since you wouldn't be rendering the template twice.
Which approach to use would depend on your application, your constraints, etc. But the important bit is to realize on what part of the process you'll find the body actually rendered, or render it yourself if you want it before that.
For future information it's possible to render the template in a service using the documentation here https://symfony.com/doc/current/templates.html#rendering-a-template-in-services
Here is solution (for logging templated email to DB, but can be easily customized to anything else) using EventListener
use App\Entity\EmailLog;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
#[AsEventListener(event: MessageEvent::class, method: 'onEmailSent', priority: -1)]
class EmailLogListener
{
public function __construct(
private EntityManagerInterface $entityManager
) {}
public function onEmailSent(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Email) {
return;
}
$log = new EmailLog();
$log
->setSentFrom(array_map(function (Address $address) {
return $address->getAddress();
}, $message->getFrom()))
->setSentTo(array_map(function (Address $address) {
return $address->getAddress();
}, $message->getTo()))
->setSubject($message->getSubject())
->setText($message->getHtmlBody());
$this->entityManager->persist($log);
$this->entityManager->flush();
}
}
I need to edit template which used for emails in admin panel. Any Ideas?
I think about several ways:
Save email template in DB in the text field, edit it in the admin panel and then display the text in the blade's view.
The problem of this way's realization is I have to display php variables in blade template and then use the final code as the html for email. I think, it's so difficult for Laravel.
And the additional problem is if I store {{ $var }} in template's text in DB - it will display as the text, blade compiler doesn't process it.
Store only the static text information from email in the DB and then display it in the template. PHP variables will transfer separately.
This way will solve the problem with the php var's display, but I still don't know how to use the final code in the Mail::send, because Laravel allows using the template's name only, not a HTML, as I know...
I think about the following way:
$view = view('template')->render();
mail(..., $view, ...);
But I don't want to use it because I want use Mail::queue() for querying emails and I don't know how to use it with PHP mail().
Thanks to everybody for replies.
You can create your own variable syntax and store email template as text in your DB. Foe example, you can store each variable as ${VARIABLE_KEY} string.
Then during email preparation you should resolve all such constructions into their real values. I don't know which variables are required, but during email preparation you should execute these steps:
Load email template from DB.
Replace all ${VARIABLE_KEY} with their real values.
You can use regular expressions for the searching and replacement, but also you can use functions such str_replace. For example, if you want to paste email of the current user into your email (and your table for model User has an email field), then you can create variable: ${user.name} and then replace this manually with simple str_replace function:
$variables['${user.name}'] = Auth::user()->email;
str_replace(array_keys($variables), array_values($variables), $yourEmailTemplateBody);
Also you can do replacements by the same method not only in the email template body, but in the email subject too.
Then you have to create your own class which extends Laravel Illuminate\Mail\Mailable class. In this class you should define build method, where you can use not only the name of the view, but also some additional parameters, like in the "regular" view, for example:
class SomeClassName extends Mailable
{
public function build()
{
$email = $this->view('mail.common', [
'mail_header' => 'some header',
'mail_footer' => 'some footer',
])->subject('Your subject');
...
return $email;
}
For example, in your view you can store layout for entire email with some extra parameters: footer and header as in my example.
Also you can create more complex syntax for ${VARIABLE_NAME} constructions, for example, VARIABLE_NAME can be a method definition in PHP or Laravel notation, i.e.: SomeClass::someStaticMethod. You can detect this case and resolve SomeClass via Laravel Service Container. Also it can be an object.field notation, for example, user.email, where user is the current Auth::user().
But be carefull in this cases: if you will grant ability to edit email templates with this variables for all users, you should filter fields or available methods and classes for calling to prevent executing any method of any available class in your email template or to prevent display private information.
You can read about writing mailables in Laravel documentation
I was doing this for a project yesterday and found a good post describing Alexander's answer in more detail. The core is creating an EmailTemplate model with this method:
public function parse($data)
{
$parsed = preg_replace_callback('/{{(.*?)}}/', function ($matches) use ($data) {
list($shortCode, $index) = $matches;
if( isset($data[$index]) ) {
return $data[$index];
} else {
throw new Exception("Shortcode {$shortCode} not found in template id {$this->id}", 1);
}
}, $this->content);
return $parsed;
}
Example usage:
$template = EmailTemplate::where('name', 'welcome-email')->first();
Mail::send([], [], function($message) use ($template, $user)
{
$data = [
'firstname' => $user->firstname
];
$message->to($user->email, $user->fullname)
->subject($template->subject)
->setBody($template->parse($data));
});
For all the details (db migration, unit test, etc), see the original post at http://tnt.studio/blog/email-templates-from-database
You can simply use this awesome laravel package:
https://github.com/Qoraiche/laravel-mail-editor
Features (from readme file):
Create mailables without using command line.
Preview/Edit all your mailables at a single place.
Templates (more than 20+ ready to use email templates).
WYSIWYG Email HTML/Markdown editor.
if i want send email with SwiftMailer in symfony then i must in action:
$message = $this->getMailer()->compose(
array('user#gmail.com' => 'user'),
$affiliate->getEmail(),
'Jobeet affiliate token',
body
);
$this->getMailer()->send($message);
but this doesnt working in template and in model (i would like create function for this).
I wouldn't recommend sending mail using a template - put a link in the template and call the action to send the mail ... you can send from a model although again i wouldn't recommend it as using sfContext::getInstance() inside the model is a really bad practice as it makes the model class rely on the context. So, your model class can't be unit tested as it needs a context to work...
You need an instance of the current sfContext to do it ... i would suggest passing it as a parameter when you create the model
In some special cases where just logging an error isn't enough I would like to pass an error array or a custom error string from a Model to the calling Controller in order to send that data to me in an email.
I thought about just sending an email from the Model itself but I have read somewhere that it angers the MVC best practices gods. I looked around the CakePHP API and didn't find anything that looks like what I need so I'm asking here to see if I missed anything.
Edit: I'm doing some special processing in the beforeSave() method.
Thanks!
Jason
Haha, going forward - in CakePHP 2.0 - the Email class will be a first-class citizen and not a component.
As such, I wouldn't worry about angering the MVC gods by sending email from (god-forbid) models or shells or other useful places.
You do have to jump through a few hoops though:
// we will need a controller, so lets make one:
App::import('Core', 'Controller');
$controller =& new Controller();
// lets grab the email component
App::import('Component', 'Email');
$email =& new EmailComponent();
// give it the reference to the controller
$email->initialize($controller);
// off we go...
$email->from = 'Name <noreply#example.com>';
$email->replyTo = 'noreply#example.com';
$email->sendAs = $format;
$email->to = $destination;
$email->subject = $subject;
// oh, this is why we needed the controller
$email->template = $template;
$controller->set(compact('items', 'subject'));
// done.
$sent = $email->send();
It's a MVC question.
Here is the situation:
I am writing an application where I have "groups".
You can invite other persons to your groups by typing their email and clicking "invite".
There are two ways this functionality can be called: a) web interface and b) API
After the mail sending is over I want to report to the user which mails were sent successfully (i.e., if the SMTP send succeeded. Currently, I am not interested in reporting mail bounces).
So, I am thinking how should I design so that there is no code duplication. That is, API and web-interface should share the bulk of the code.
To do this, I can create the method "invite" inside the model "group". So, the API and and the Web-interface can just call:
group->invite($emailList);
This method can send the emails. But the, problem is, then I have to access the mail templates, create the views for the mails, and then send the mails. Which should actually be in the "View" part or at least in the "Controller" part.
What is the most elegant design in this situation?
Note: I am really thinking to write this in the Model. My only doubt is: previously I thought sending mails also as "presentation". Since it is may be considered as a different form of generating output.
Added after edit
I understand that View does not necessarily have to be output to the browser. And that is where my doubt is. Now the problem is, say I have a "task list" in my app. We can assign a task to some folks. Now the "assignTo" method can be called in two situations: 1) While creating the task 2) reassign a task to someone else.
In both cases the new assignee should get the email notification. So if the method "assignTo" is not sending the mail, we have to duplicate the mailing part in two places: "task create controller" and "task reassign controller".
I wanted to avoid this duplication.
The View doesn't necessarily have to be an output to the browser that the user gets to see. It can also be the template that's being emailed. The Controller receives input on what to do, requests the necessary information from the Model, and users the View to output that information. In this case, the View would be the template of the email, and the Controller has the task to email that output.
View: click "invite" (or for the API: send an invite command);
Controller: user clicked "invite", get data from model;
Model: Controller requests data (emails for specific folks or whatnot), return;
Controller: receives data from Model, setup data for the View (template) and email the "View".
After that, return something to the API, or tell the Controller to output a View for the web interface that tells the user that the invite has been processed.
I had same doubts with mailing system in Kohana myself, in the end figured out the best way to do it was like this;
Have one main 'mailer' class ( wrapper ) which will contain methods to send mails, extend it for each separately used mailer classes ( factories );
application/classes/mailer.php:
abstract class Mailer {
protected $from;
protected $to;
protected $cc;
protected $bcc;
protected $subject;
protected $body;
protected $reply_to;
protected $sent_on;
protected $content_type = 'text/html';
protected $headers;
protected $template;
public static function factory($name)
{
$class = 'Mailer_'.$name;
return new $class();
}
public function __construct()
{
return $this;
}
public function send($save = FALSE)
{
// send the email using swift mailer, zend_mail, phpmailer, whatever..
}
protected function save($to, $subject, $body, $headers)
{
}
}
application/classes/mailer/user.php
class Mailer_User extends Mailer {
// Who is sending the mail
protected $from = "users#domain.com";
// Content type of the email
protected $content_type = 'text/html';
public function email_activated($name, $email)
{
$this->to = $email;
$this->subject = 'Your email has been verified';
$this->body = View::factory('mailer/user/email_verified')
->set('name', $name)
->render();
return $this;
}
}
and later on in code use it like factory;
Mailer::factory('user')
->email_activated( $user->username, $user->email)
->send();
sending the email from wherever you want it to.
The API itself is group of all public methods in model - no additional wrapper is needed, because model does all the wrapping itself (and keeps other methods non-public). The application itself should not use another api than the official one is (so you prevent the duplication).
I'd do something like:
sendMail(array $recipients);
sendGroupMail($group_id) not{
sendMail(/*get recipients by group_id*/);
};