How to access Mail data from MessageSent event on Laravel? - php

I'm using laravel 5.5, and trying to send emails with an image that is the sign of the client. To make the image accessible from the views I'm copying it into public folder and queued emails will access to it.
With a single action I can send multiple emails to the client, with sign into email, and pdf like the email attached, with sign image too. Then, the same image can be called multiple times from different emails. Is for this that I copy one image with a codified name for each email and passing the name of the image to the Mailable.
The problem is to make a sign of client public with a limited time. Then I'm trying to make listener for Illuminate\Mail\Events\MessageSent event that deletes the image of public folder getting the image name from the event... but I can't access to it.
How can I access to data of mailable from the event?
Do you know a better way to do this?
Thanks in advance.
Mailable class
class SEPA extends Mailable
{
use Queueable, SerializesModels;
public $client;
/**
* Create a new message instance.
*
* #param Client $client
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$date = Carbon::now();
// Name codified
$fileName = md5(microtime()).".png";
// Making the image accessible from views
Storage::copy("clients/{$this->client->id}/firma.png", "public/tmp/{$fileName}");
$pdfName = "SEPA - {$this->client->name}{$this->client->cognom1}{$this->client->cognom2}.pdf";
$dades = [
'data' => $date,
'client' => $this->client,
'firma' => $fileName
];
// Generating PDF
$pdf = PDF::loadView('pdfs.SEPA', $dades);
if (!Storage::has("tmp/clients/{$this->client->id}")) Storage::makeDirectory("tmp/clients/{$this->client->id}");
$pdf->save(storage_path()."/app/tmp/clients/{$this->client->id}/".$pdfName);
return $this
->from(['address' => 'email#random.com'])
->view('emails.SEPA')
->with($dades)
->attach(storage_path()."/app/tmp/clients/{$this->client->id}/".$pdfName);
}
}
EventServiceProvider.php
protected $listen = [
'Illuminate\Mail\Events\MessageSent' => [
'App\Listeners\DeleteTempResources'
]
];
Listener
public function handle(MessageSent $event)
{
// Trying to access on data message
Log::info($event->message->firma);
}

You might be able to just set the additional data you need to access from the event via the withSwiftMessage() method, as extra fields on the actual swiftMessage, since that's what will be accessible in the event, as $message.
I saw someone did this here, e.g. to add a $user object:
$this->withSwiftMessage(function ($message) {
$message->user = $this->user; // any crazy field of your choosing
});
This seemed pretty unorthodox to me - adding rogue fields like that.
Note you don't need to use the $user object to get it into the closure as it's available in scope via $this, so long as it's a member property of the containing class.
To see it in the event when the message comes off the queue, you can Log::info('The user: ', [$event->message->user]) in the MessageSending event.
I've just tested this, and it works (I'm on 5.5), but I'm not yet using this myself in code as it does seem a little strange, adding a rogue field like that. I mention it though as it might actually solve your problem if you're comfortable with the method! If anyone knows a less ugly way to do it, I'm all ears...
P.S. I may consider just tacking on $message->lara_user_id = $this->user->id in the closure, for my own case, as that seems unlikely to collide with anything, and can be conveniently pulled back in the event. discussion welcome!

Related

How to call other function from the same controller in Laravel application?

I'm very new to Laravel and I was given a Laravel project, where I need to add some new features. The person, who has previously worked on that project hadn't left even a single comment in the code and now I must make my own scenarios about the features.
I have a controller, defined with some functions (dashboard, show_project, save_project etc.) and in one of my function, I need to use the result of calling other function.
In the concrete example, the call is made from "http://127.0.0.1:8000/username/project_slug" - there is a button "Save" and post function, called on onClick event. The function, whose output I need is normally called on "http://127.0.0.1:8000/username/project_slug/svg", which returns a view.
For better understanding, there's an example of the flow:
The user wants to save his/her project (an UML diagram) but in order to have a thumbnail, a function which generates a view (SVG format) will be called and the idea is, to take the HTML content of the page, which is on "http://127.0.0.1:8000/username/project_slug/svg" and to pass it to another API in order an image to be generated.
So far, I tried with cURL, file_get_contents, file_get_html, render methods but when I return the output, the server just keeps waiting and shows no error messages.
//The both functions are in ProjectController.php
/**
* A function, for saving the json file, where the whole of the diagram
* components are described. From the frontend we receive the project_id and
* the project_data(the json content).
*/
public function save_project(Request $request) {
$input = $request->only(['project_id', 'project_data']);
/*
Here we need to call the other function, to render the HTML content
and to pass it to the other API. Then we save the result with the
other information.
*/
/*
What I've tried?
$new_link = 'http://' . $_SERVER['HTTP_HOST'] . "/$username"
."/$project_slug" . "/svg";
$contents = file_get_contents($new_link);
return $contents;
*/
//In the same way with cURL.
$project = Project::where('user_id',session('userid'))
->where('id',$input['project_id'])->first();
$project->project_data = json_encode($input['project_data']);
if($project->save()) {
return ["status"=>"saved"];
}
else {
return ["status"=>"error"];
}
}
/**
* A function, which takes the the Json content (project_data) from the
* database and passes it to the view, where the Json is transformed in HTML
* tags.
*/
public function generate_svg(Request $request,$username,$project_slug) {
if(session('username')!=$username) {
return redirect("/");
}
$userid = session('userid');
$project = Project::where([
'user_id' => $userid,
'slug' => $project_slug,
])->first();
if(!is_null($project)) {
return view('svg',compact('project'));
}
}
I've read about some possible ways, including Guzzle request but maybe I haven't understood correctly the idea:
If I need to make a Guzzle request from my controller to the other function inside my controller, do I need an API configuration?
What I mean? Example:
Before saving the project, the user is on this URL address "http://127.0.0.1:8000/hristo/16test". Inside the controller, I have in session variables the token, the username(hristo) and i can get the project_name(16test) from the URL but after passing this URL to the generate_svg function, there is no indication of error or success.
So I'm missing some kind of token information?
If you just need the response of the other function you can just use
$response = $this->generate_svg($request, $username, $project_slug);
If you'll need to use this function from a different controller you can use this
app('App\Http\Controllers\UsernameController')->generate_svg($request, $username, $project_slug);

Mautic hook is not called (ON_SENT_EMAIL_TO_USER)

I am writing a plug-on that should add an (dynamic) attachment to the email that is send to the end user. But I am stuck on one thing.
Firstly I was using the EMAIL_ON_SEND hook to add an attachment to the email. But it seems that it will add a attachment to each email everytime it is called.
For each email it is called two times. So to the first mail it will add 2 attachments and for the second one 4, etc etc.
The second approach was to use the ON_SENT_EMAIL_TO_USER hook. But this one does not seems to be called before the email (in a segment) is send.
class EmailSubscriber extends CommonSubscriber
{
protected $helper;
public function __construct(IntegrationHelper $helper)
{
$this->helper = $helper;
$this->parser = new ApiParser();
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return [
// EmailEvents::EMAIL_ON_SEND => ['onEmailSend', 100],
EmailEvents::ON_SENT_EMAIL_TO_USER => ['onEmailSend', 100],
];
}
/**
* Search and replace tokens with content
*
* #param EmailSendEvent $event
*/
public function onEmailSend(EmailSendEvent $event)
{
error_log('123');
}
Someway I have to hook on the actual action that is sending the email instead of the event (?). But I can't figure out which one
I can't answer directly but might be able to point you at some useful resources!
Firstly, are you trying to send the email to the Mautic user (e.g. the administrator or the owner of the lead), or to the lead? Just wanted to double check we're looking at the right thing as they are often confused!
It also depends on what you're trying to do, attach a file which isn't currently part of Mautic (e.g. an invoice or something like that) or if you're trying to attach a file which you want to track in Mautic as an asset.
In terms of attachments, these resources from the developer documentation may be useful:
Mailhelper - https://developer.mautic.org/#mail-helper
Attachments - https://developer.mautic.org/#attachments
It references attachFile() but there is also attachAsset() which allows you to attach a Mautic asset you have already uploaded (\Mautic\AssetBundle\Entity\Asset).
You may also want to take a look at https://forums.mautic.org where there may be more developers from the community able to give some further insight!
Hey Firstly thank you for the response.
The hook is called multiple times so I needed to tweak it.
So we track where it is called and filter it.
Besides that we need to clean the attachments every time.
Anyway even if it is not that clean, it does the trick
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
if (strpos($trace[4]['file'], 'SendEmailToContact.php') !== false) {
$helper = $event->getHelper();
$messageChildren = $helper->message->getChildren();
if (count($messageChildren) > 0) {
$helper->message->detach($messageChildren[0]);
}

Add Account token to routing

I'm trying to add an Account to the Silex route. My goal is to have routes like:
/{_account}/{_locale}/
/{_account}/{_locale}/products
/{_account}/{_locale}/block
You can find my Code here on github. It's a small sample. I can read the account token from the request and save the Account in the AccountListener.
I try to handle the _account like _locale. Once set or updated the application don't have to bother about it. That means that the _account param will be set automatically if I call $app['url_generator']->generate('blog').
This is my current problem. I don't know how to inform the UrlGenerator to set these param.
Maybe my approach is completely wrong.
I Hope you can send me some examples or Cookbooks or somethink. Or a merge request.
The UrlGenerator uses parameters from the request_context (as you can see in the code) so you could set those in your listener.
src/app.php
$dispatcher = $app['dispatcher']->addSubscriber(
new AccountListener(
new AccountRepository(),
$app['request_context'],
$app['monolog']
)
);
SilexLab\Listener\AccountListener
public function __construct(
AccountProvider $accountProvider,
RequestContext $requestContext,
Logger $logger
) {
//...
$this->requestContext = $requestContext;
}
public function onKernelRequest(GetResponseEvent $event)
{
//...
$request->attributes->set('_account', $account);
$this->requestContext->setParameter('_account', $account);
}

Forms with Symfony2 - Doctrine Entity with some immutable constructor parameters and OneToMany association

I have a OneToMany association between a Server entity and Client entities in the database. One server can have many clients. I want to make a form where the user can choose a server from a dropdown, fill in some details for a new client, and submit it.
Goal
To create a form where a user can input data into fields for the Client, choose a Server from the dropdown, then click submit and have this data (and the association) persisted via Doctrine.
Simple, right? Hell no. We'll get to that. Here's the pretty form as it stands:
Things of note:
Server is populated from the Server entities (EntityRepository::findAll())
Client is a dropdown with hardcoded values
Port, endpoint, username and password are all text fields
Client Entity
In my infinite wisdom I have declared that my Client entity has the following constructor signature:
class Client
{
/** -- SNIP -- **/
public function __construct($type, $port, $endPoint, $authPassword, $authUsername);
/** -- SNIP -- **/
}
This will not change. To create a valid Client object, the above constructor parameters exist. They are not optional, and this object cannot be created without the above parameters being given upon object instantiation.
Potential Problems:
The type property is immutable. Once you've created a client, you cannot change the type.
I do not have a setter for type. It is a constructor parameter only. This is because once a client is created, you cannot change the type. Therefore I am enforcing this at the entity level. As a result, there is no setType() or changeType() method.
I do not have the standard setObject naming convention. I state that to change the port, for example, the method name is changePort() not setPort(). This is how I require my object API to function, before the use of an ORM.
Server Entity
I'm using __toString() to concatenate the name and ipAddress members to display in the form dropdown:
class Server
{
/** -- SNIP -- **/
public function __toString()
{
return sprintf('%s - %s', $this->name, $this->ipAddress);
}
/** -- SNIP -- **/
}
Custom Form Type
I used Building Forms with Entities as a baseline for my code.
Here is the ClientType I created to build the form for me:
class ClientType extends AbstractType
{
/**
* #var UrlGenerator
*/
protected $urlGenerator;
/**
* #constructor
*
* #param UrlGenerator $urlGenerator
*/
public function __construct(UrlGenerator $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** Dropdown box containing the server name **/
$builder->add('server', 'entity', [
'class' => 'App\Model\Entity\Server',
'query_builder' => function(ServerRepository $serverRepository) {
return $serverRepository->createQueryBuilder('s');
},
'empty_data' => '--- NO SERVERS ---'
]);
/** Dropdown box containing the client names **/
$builder->add('client', 'choice', [
'choices' => [
'transmission' => 'transmission',
'deluge' => 'deluge'
],
'mapped' => false
]);
/** The rest of the form elements **/
$builder->add('port')
->add('authUsername')
->add('authPassword')
->add('endPoint')
->add('addClient', 'submit');
$builder->setAction($this->urlGenerator->generate('admin_servers_add_client'))->setMethod('POST');
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\Model\Entity\Client',
'empty_data' => function(FormInterface $form) {
return new Client(
$form->getData()['client'],
$form->getData()['port'],
$form->getData()['endPoint'],
$form->getData()['authPassword'],
$form->getData()['authUsername']
);
}
]);
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'client';
}
}
The above code is what actually generates the form to be used client-side (via twig).
The Problems
First and foremost, with the above code, submitting the form gives me:
NoSuchPropertyException in PropertyAccessor.php line 456:
Neither the property "port" nor one of the methods "addPort()"/"removePort()", "setPort()", "port()", "__set()" or "__call()" exist and have public access in class "App\Model\Entity\Client".
So it can't find the port method. That's because it's changePort() as I explained earlier. How do I tell it that it should use changePort() instead? According to the docs I would have to use the entity type for port, endPoint etc. But they're just text fields. How do I go about this the right way?
I have tried:
Setting ['mapped' => false] on port, authUsername etc. This gives me null for all the client fields, but it does seem to have the relevant server details with it. Regardless, $form->isValid() return false. Here's what var_dump() shows me:
A combination of other things involving setting each on field to "entity", and more..
Basically, "it's not working". But this is as far as I've got. What am I doing wrong? I am reading the manual over and over but everything is so far apart that I don't know if I should be using a DataTransformer, the Entity Field Type, or otherwise. I'm close to scrapping Symfony/Forms altogether and just writing this myself in a tenth of the time.
Could someone please give me a solid answer on how to get where I want to be? Also this may help future users :-)
There are a few problems with the above solution, so here's how I got it working!
Nulls
It turns out that in setDefaultOptions(), the code: $form->getData['key'] was returning null, hence all the nulls in the screenshot. This needed to be changed to $form->get('key')->getData()
return new Client(
$form->get('client')->getData(),
$form->get('port')->getData(),
$form->get('endPoint')->getData(),
$form->get('authPassword')->getData(),
$form->get('authUsername')->getData()
);
As a result, the data came through as expected, with all the values intact (apart from the id).
Twig Csrf
According to the documentation you can set csrf_protection => false in your form options. If you don't do this, you will need to render the hidden csrf field in your form:
{{ form_rest(form) }}
This renders the rest of the form fields for you, including the hidden _token one:
Symfony2 has a mechanism that helps to prevent cross-site scripting: they generate a CSRF token that have to be used for form validation. Here, in your example, you're not displaying (so not submitting) it with form_rest(form). Basically form_rest(form) will "render" every field that you didn't render before but that is contained into the form object that you've passed to your view. CSRF token is one of those values.
Silex
Here's the error I was getting after solving the above issue:
The CSRF token is invalid. Please try to resubmit the form.
I'm using Silex, and when registering the FormServiceProvider, I had the following:
$app->register(new FormServiceProvider, [
'form.secret' => uniqid(rand(), true)
]);
This Post shows how Silex is giving you some deprecated CsrfProvider code:
Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.
As a result, I had to remove the form.secret option and also add the following to my application bootstrap, before registering the form provider:
/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
$storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});
With the above modifications, the form now posts and the data is persisted in the database correctly, including the doctrine association!

Non-public accessible function in CakePHP

I have built a simple Notification system in my Cake app that I want to have a function that will create a new notification when I call a certain method. Because this is not something the user would actually access directly and is only database logic I have put it in the Notification model like so:
class Notification extends AppModel
{
public $name = 'Notification';
public function createNotification($userId, $content, $url)
{
$this->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
}
And then whenever I want to create a notification within my app I just do:
$this->Notification->createNotification($userId,'Test','Test');
However this doesn't work! The controller is talking to the model fine, but it doesn't create the row in the database... I'm not sure why... but it would seem I'm doing this wrong by just doing all the code in the model and then calling it across the app.
Edit: Based on answers and comments below, I have tried the following the code to create a protected method in my notifications controller:
protected function _createNotification($userId, $content, $url)
{
$this->Notification->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
Now the thing that is stumping me still (apologies if this is quite simple to others, but I have not used protected methods in CakePHP before) is how do I then call this from another controller? So for example If have a method in my PostsController and want to create a notification on successful save, how would I do this?
I thought about in my PostsController add method:
if($this->save($this->request-data){
$this->Notification->_createNotification($userId,'Test','Test');
}
But being protected I wouldn't be able to access the method from outside of the NotificationsController. Also I'm using the same syntax as if I was calling a function from a model so again it doesn't feel right.
Hopefully someone can help me out and get me back on track as this is a new area to me.
the controller should pass all data to the model
$this->createNotification($this->request->data);
the model then can use the data:
public function createNotification(array $data) {
$key = $data[$this->alias]['key'];
$data[...] = ...;
$this->create();
return $this->save($data);
}
you never ever try to access the controller (and/or its request object) from within a model.
you can also invoke the method from other models, of course:
public function otherModelsMethod() {
$this->Notification = ClassRegistry::init('Notification');
$data = array(
'Notification' => array(...)
);
$this->Notification->createNotification($data);
}
and you can make your methods verbose, but that usually makes it harder to read/understand/maintain with more and more arguments:
public function createNotification($userId, $content, $url) {
$data = array();
// assign the vars to $data
$data['user_id'] = $userId;
...
$this->create();
return $this->save($data);
}
so this is often not the cake way..
Methods in a model are not "publicly accessible" by definition. A user cannot call or invoke a method in a model. A user can only cause a controller action to be initiated, never anything in the model. If you don't call your model method from any controller, it's never going to be invoked. So forget about the "non-public" part of the question.
Your problem is that you're working in the model as if you were in a controller. There is no request object in a model. You just pass a data array into the model method and save that array. No need for $this->request. Just make a regular array(), put the data that was passed by the controller in there and save it.
The whole approach is totally wrong in the MVC context IMO and screams for the use of the CakePHP event system. Because what you want is in fact trigger some kind of event. Read http://book.cakephp.org/2.0/en/core-libraries/events.html
Trigger an Event and attach a global event listener that will listen for this kind of events and execute whatever it should do (save something to db) when an event happens. It's clean, flexible and extendible.
If you did a proper MVC stack for your app most, if not all, events aka notifications should be fired from within a model like when a post was saved successfully for example.
This is what I have ended up doing. While it certainly isn't glamorous. It works for what I want it to do and is a nice quick win as the notifications are only used in a few methods so I'm not creating a large amount of code that needs improving in the future.
First to create a notification I do the following:
$notificationContent = '<strong>'.$user['User']['username'].'</strong> has requested to be friends with you.';
$notificationUrl = Router::url(array('controller'=>'friends','action'=>'requests'));
$this->Notification->createNotification($friendId,$notificationContent,$notificationUrl);
Here I pass the content I want and the URL where the user can do something, in this case see the friend request they have been notified about. The url can be null if it's an information only notification.
The createNotification function is in the model only and looks like:
public function createNotification($userId, $content, $url = null)
{
$this->saveField('user_id',$userId);
$this->saveField('content',$content);
$this->saveField('url',$url);
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
This creates a new record in the table with the passed content, sets its status to 0 (which means unread) and the date it was created. The notification is then set as read when a user visits the notifications page.
Again this is most probably not an ideal solution to the problem outlined in this question... but it works and is easy to work with And may prove useful to others who are learning CakePHP who want to run functions from models when building prototype apps.
Remember nothing to stop you improving things in the future!
First of all, you can improve your last solution to do one save() (instead of 5) the following way:
public function createNotification($userId, $content, $url = null){
$data = array(
'user_id' => $userId,
'content' => $content,
'url' => $url,
'datetime' => date('Y-m-d H:i:s'),
'status' => 0
);
$this->create();
$this->save($data);
}
When I began programming CakePHP(1.3) more than a year ago I also had this problem.
(I wanted to use a function of a controller in any other controller.)
Because I didn't know/researched where to place code like this I've done it wrong for over a year in a very big project. Because the project is really really big I decided to leave it that way. This is what i do:
I add a function (without a view, underscored) to the app_controller.php:
class AppController extends Controller {
//........begin of controller..... skipped here
function _doSomething(){
//don't forget to load the used model
$this->loadModel('Notification');
//do ur magic (save or delete or find ;) )
$tadaaa = $this->Notification->find('first');
//return something
return $tadaaa;
}
}
This way you can access the function from your Notification controller and your Posts controller with:
$this->_doSomething();
I use this kind of functions to do things that have nothing to do with data submittance or reading, so i decided to keep them in the app_controller. In my project these functions are used to submit e-mails to users for example.. or post user actions to facebook from different controllers.
Hope I could make someone happy with this ;) but if you're planning to make a lot of these functions, it would be much better to place them in the model!

Categories