Monolog add additional data at the end when something is logged - php

Coming from Yii2 at the end of each request when something is logged, Yii2 adds additional data to your log, for example the $_POST data so you know what paramaters caused the issue.
Is there a way to add these information in Monolog too?
I don't want to use a Processor as it includes all those parameters to each and every record. I would just like to add an additional string in case something is logged at the end of the log after all messages are included/send (for example via BufferHandler when sending Mails via SwiftMailerHandler)

I found a way..
I can extend the default HtmlFormatter or LineFormatter and include my custom information that way
namespace modules\myspa\monolog;
class HtmlFormatter extends \Monolog\Formatter\HtmlFormatter
{
public function formatBatch(array $records): string
{
$formatBatch = parent::formatBatch($records);
$formatBatch .= '<br><br>Foobar.. add some custom info';
return $formatBatch;
}
}
Then I can add this one as a formatter
$swiftMailHandler = new SwiftMailerHandler($swiftMailer, $message, \Monolog\Logger::ERROR);
// add my formatter
$swiftMailHandler->setFormatter(new HtmlFormatter());
$psrLogger->pushHandler(new BufferHandler($swiftMailHandler));

Related

How to get the rendered body of a templated email before sending?

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

Edit email template in Laravel

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.

Symfony2 Service Response

i was trying to setup a general service which handles common function i use very often everywhere in my project. For example if a user wants to purchase something for virtual currency there would be a function which checks, if the user has enough virtual currency in his account.
If the user doesnt have enough virtual currency I want this function to make a JSOn Response, but of cource, only controllers are allowed to response. But this means i have to check in every action I use this function, whether the purchase is valid or not.
Here is the function call in my Controller:
$purchaes= $this->get('global_functions')->payVirtualCurrency($user_id, $currency_amount);
if($change instanceof JsonResponse){
return $change;
}
And the function:
public function payVirtualCurrency($user_id, $currency_amount){
$user = $this->dm->getRepository('LoginBundle:User')->findOneById($user_id);
if($user->getVirtualCurrency() < $currency_amount){
return new JsonResponse(array('error' => $this->trans->trans('Insufficient amount of virtual Currency')));
}
return true;
}
Is there a better way to do this? I really want to avoid doing the same thing in the controller over and over again.
Thanks in advance!
Two options come to my mind, both are quite elegant solutions but both require little work:
1. Create custom exception listener
Create custom exception, let's call it InsufficientMoneyException. Then, your sevice can be as it is, but instead of returning response it throws your custom exception (in case user does not have enough money). Then, you create custom exception listener which listenes to InsufficientMoneyException custom exception and returns your desired JsonResponse.
2. Create custom annotation
You can create custom annotation and flag a controller action with this annotation. It would look something like this
/**
* #MinimumMoneyRequired("50")
*/
public function buyAction()
{
(...)
}
This option is really nice and decoupled but it require quite a lot of configuration. This is nice blog post with detailed description how to create custom annotations

ZF2 namespace for flashmessenger

In my application I am using multiple forms, the forms submission points to another
action but redirects back to the previous action. In the form submission action I handle
the form input / validation. To return an error or success message I use the FlashMessenger.
My point of problem is that it's not clear how I set a namespace for the FlashMessenger. I have serval forms on the same page where I would like to use FlashMessenger messenges.
if ($this->flashMessenger()->hasMessages()) {
$messages = $this->flashMessenger()->getMessages();
foreach($messages as $message) {
echo $message;
}
}
I am guessing I should do something with '$this->flashMessenger('namespace').. In my controller action? But I didn't figure out how exactly make this work. If anyone has an example.. that would be great :)
You can add a message into a particular namespace by using these built-in methods in your action controller:
// in your controller
$this->flashMessenger()->addInfoMessage('info message');
$this->flashMessenger()->addSuccessMessage('success message');
$this->flashMessenger()->addErrorMessage('error message');
// in your view script
$this->flashMessenger()->getInfoMessages();
$this->flashMessenger()->getSuccessMessages();
$this->flashMessenger()->getErrorMessages();
Or if you want to specify your own namespace, you can use something like:
// in your action controller
$defaultNamespace = $this->getNamespace();
$this->setNamespace('yournamespace');
$this->addMessage($message);
$this->setNamespace($defaultNamespace);
// in your view script
$this->flashMessenger()->getMessagesFromNamespace('yournamespace');
For more information, you can see the documentation:
http://framework.zend.com/manual/2.3/en/modules/zend.mvc.plugins.html#flashmessenger-plugin
http://framework.zend.com/manual/2.3/en/modules/zend.view.helpers.flash-messenger.html#basic-usage
The namespace of the flash messenger controller plugin can be manual set using the setNamespace($namespace) method.
$this->flashMessenger()->setNamespace('foo')->addMessage($message);
However there are also convenience functions that will set a different namespace and message at the same time.
For example, if you want to add a success message then you can use:
$this->flashMessenger()->addSuccessMessage($message);
Internally the plugin will set the namespace as success and add the message to it (and then reset the namespace to allow for the next message to be set (defaults to default))
public function addSuccessMessage($message)
{
$namespace = $this->getNamespace();
$this->setNamespace(self::NAMESPACE_SUCCESS);
$this->addMessage($message);
$this->setNamespace($namespace);
return $this;
}

Prestashop Add extra fields to registration form

I am building a prestashop site
The corporate buyers cannot register for an account to view the products unless they can enter a valid EIN number or a valid Duns & Brandstreet number in registration.
How to make it possible?
Also any other e-commerce software that can help me solve this by switching to it?
All you really need to do is add the field to the template and then override the AuthController with additional code to handle you new field. e.g.
<?php
class AuthController extends AuthControllerCore
{
public function preProcess()
{
// Additional pre-processing for the new form field
if (Tools::isSubmit('submitAccount'))
{
if (!MyDBNumberValidationClass::verifyvalidnumber(Tools::getValue('db_number_field', 0)))
$this->errors[] = Tools::displayError('The Dun and Bradstreet number you entered is invalid.');
}
parent::preProcess();
}
}
I'm assuming that you're doing complex verification of the number which is why the static call to MyDBNumberValidationClass::verifyvalidnumber() is in the above, but it could equally by any simple test or an additional function in your AuthController override class definition above that validates. If you add two fields (i.e. the EIN number too) then just alter the logic to handle generating an error only if both are invalid; success of the form validation is based on $this->errors being empty.
This is only part of a general solution since it only handles validating the extra fields. If you need to actually do something with the data entered then the best way is to write a little handler module that installs itself on hookCreateAccount e.g.
public function hookCreateAccount($params)
{
// Get the field data entered on the form
$DB_number = $params['_POST']['db_number_field'];
// Your custom processing...
}
Note that there's no way to back out of the account creation from here so you will want to add code to email the store owner should there be a problem.

Categories