I'm trying to create a custom batch action using Sonata Admin Bundle, that sends an email.
The problem is, I'm not sure how to access swiftmailer. I have the following:
public function batchActionSend(ProxyQueryInterface $selectedModelQuery)
{
if($this->admin->isGranted('EDIT')=== false) {
throw new AccessDeniedException();
}
$request = $this->get('request');
$modelManager = $this->admin->getModelManager();
$selectedModels = $selectedModelQuery->execute();
try {
foreach ($selectedModels as $selectedModel) {
// send the email here?
$selectedModel->send();
$modelManager->update($selectedModel);
}
} catch (\Exception $e) {
$this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());
return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
}
$this->get('session')->getFlashBag()->add('sonata_flash_success', sprintf('The selected requests have been sent'));
return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
In my entity I have the following:
public function send()
{
// send email here?
}
What is the best way to access the mailer? via the Admin Class or via the Entity or via a service?
Thanks
Use Swift_Message::newInstance()
public function send()
{
$message = \Swift_Message::newInstance()
->setSubject('Hello Email')
->setFrom('send#example.com')
->setTo('recipient#example.com')
->setBody(
$this->renderView(
'HelloBundle:Hello:email.txt.twig',
array('name' => $name)
)
)
;
$this->get('mailer')->send($message);
}
Related
I've set up websockets by this manual:
http://socketo.me/docs/hello-world
And it's work.
But now I need to send messages to clients from php, and I don't know how to do this.
I've found some manuals with rabbitMQ which I use in my project? like this:
https://github.com/ratchetphp/Ratchet/issues/659
but I can't understand how to use it.
Maybe someone knows?
You can create Symfony console command to start RabbitMQ consumer, start WebSocket server and send messages to a client once ones received from queue.
protected function execute(InputInterface $input, OutputInterface $output): int
{
$loop = LoopFactory::create();
$pusher = new MessageHandler();
$queueAsyncClient = new AsyncClient($loop, [
"host" => $_ENV['RABBITMQ_HOST'],
"port" => $_ENV['RABBITMQ_PORT'],
"vhost" => $_ENV['RABBITMQ_VHOST'],
"user" => $_ENV['RABBITMQ_USERNAME'],
"password" => $_ENV['RABBITMQ_PASSWORD'],
]);
$connect = $queueAsyncClient->connect();
// When client has connected, retrieve channel (as promise)
$connect->then(function (AsyncClient $client) {
return $client->channel();
// Then declare the queue and exchange
})->then(function (Channel $channel) {
// These method calls all return promises, so we need to combine them
return \React\Promise\all([
$channel,
// Create the queue we'll be using
$channel->queueDeclare($_ENV['RABBITMQ_QUEUE'], false, true),
// Declare an exchange
$channel->exchangeDeclare($_ENV['RABBITMQ_EXCHANGE'], 'direct', false, true),
// Bind the queue to the exchange
$channel->queueBind($_ENV['RABBITMQ_QUEUE'], $_ENV['RABBITMQ_EXCHANGE']),
]);
// Then, when the exchange is all hooked up, hook up the pusher
})->then(function ($connection) use ($pusher) {
/** #var Channel $channel (see first section of all() promise above) */
$channel = $connection[0];
// On messages, consume them using the pusher
return $channel->consume(
function (Message $message) use ($pusher, $channel) {
$content = json_decode($message->content, true);
$connections = $pusher->getUserConnections($content["uid"]);
foreach ($connections as $connection) {
$connection->send(json_encode($content["event"]));
}
},
$_ENV['RABBITMQ_QUEUE'],
'',
false,
true // Acknowledges messages
);
})->done();
$webSocketServer = new \React\Socket\TcpServer("tcp://0.0.0.0:" . $_ENV['WEBSOCKET_PORT'] . "/ws", $loop);
$wsServer = new WsServer($pusher);
$wsServer->enableKeepAlive($loop, 30);
$ioServer = new IoServer(
new HttpServer($wsServer),
$webSocketServer
);
$loop->run();
}
MessageHandler class (like Pusher from the Github issue) basically it stores all connections and handles connection open/close and message events.
class MessageHandler implements MessageComponentInterface
{
protected $connections;
public function __construct()
{
$this->connections = new SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn)
{
$this->connections->attach($conn);
}
public function onMessage(ConnectionInterface $from, $msg)
{
try {
$content = json_decode($msg, true);
if ($content || !isset($content["uid"])) {
$this->connections->rewind();
while ($this->connections->valid()) {
if ($this->connections->current() === $from) {
$this->connections->setInfo($content["uid"]);
}
$this->connections->next();
}
} else {
echo "Invalid message content: " . $msg;
}
} catch (\Throwable $t) {
echo $t->getMessage();
}
}
public function onClose(ConnectionInterface $conn)
{
$this->connections->detach($conn);
}
public function onError(ConnectionInterface $conn, Exception $e)
{
$this->connections->detach($conn);
$conn->close();
}
/**
* #param string|null $uid
* #return ConnectionInterface[]|null
*/
public function getUserConnections($uid)
{
$connections = [];
$this->connections->rewind();
while ($this->connections->valid()) {
if ($this->connections->getInfo() == $uid) {
$connections[] = $this->connections->current();
}
$this->connections->next();
}
return $connections;
}
/**
* #return ConnectionInterface[]|null
*/
public function allConnections()
{
$connections = [];
$this->connections->rewind();
while ($this->connections->valid()) {
$connections[] = $this->connections->current();
$this->connections->next();
}
return $connections;
}
}
Here is the method to test.
public function performPoolRequest(RecreationRequestsCollection $requests): RecreatedPaymentsPoolReading
{
$count_request = count($requests);
if($count_request) {
$pool_recreate_payments = new RecreatedPaymentsPool();
$this->http_client->sendPoolRequest(
$this->generateRequests($requests),
$count_request,
function (ResponseInterface $response, $index) use ($requests, $pool_recreate_payments) {
$this->successHandler($response, $requests, $index, $pool_recreate_payments);
},
function (BadResponseException $reason, $index) use ($requests, $pool_recreate_payments) {
$this->failureHandler($reason, $requests, $index, $pool_recreate_payments);
}
);
return $pool_recreate_payments;
} else {
throw new PoolRequestException('Incorrect amount of requests: ' . $count_request);
}
}
Difficulties arose with this piece of code:
$this->http_client->sendPoolRequest(
$this->generateRequests($requests),
$count_request,
function (ResponseInterface $response, $index) use ($requests, $pool_recreate_payments) {
$this->successHandler($response, $requests, $index, $pool_recreate_payments);
},
function (BadResponseException $reason, $index) use ($requests, $pool_recreate_payments) {
$this->failureHandler($reason, $requests, $index, $pool_recreate_payments);
}
);
I made a mock object $this->http_client
But I don't know how to test methods in the argumets ($this->successHandler, $this->failureHandler, this->generateRequests($requests)) that are sent to the method sendPoolRequest.
All of these methods are protected. I understand how to test them using a reflection object, but I want to know if there is an option to test them within a single test by checking the values in $ pool_recreate_payments.
You have to fake what the http client is doing in some way. That means: actually calling the callback functions. While it might be possible to do that with a PHPUnit mock object, writing own test doubles is often easier.
Here are a few examples to get you started.
public function testAllFailing()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
$errorHandler(new BadResponseException(/*...*/), $index);
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
public function testAllSucceeding()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
$successHandler(new Response(/*...*/), $index);
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
public function testEveryOtherFails()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
if ($index % 2 === 0) {
$successHandler(new Response(/*...*/), $index);
} else {
$errorHandler(new BadResponseException(/*...*/), $index);
}
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
Since Guzzle is used as the http client, I decided to use its capabilities in terms of creating stubs for the response object. Here is a link to the documentation Guzzle documentation. The solution turned out to be simple and allowed us to extensively test the business logic of the response.
I have created a Model Admin called 'Clients'. Under the "Security" tab I created a new group called 'clients'. This Model Admin is managing just the clients and not other members.
When creating a new member in the CMS using a model admin, I want to automatically generate a password for them (instead of them having to create their own one) one for them and then email it to them.
What I want to happen:
After the staff member clicks "Add member" the password and password confirmation textboxs are automatically populated with the generated password. - This is the most ideal way I believe. - Then once the staff member clicks save it will send the client and email with the username and newly generated password.
Question is how do you do this?
ClientAdmin.php
<?php
class ClientAdmin extends ModelAdmin {
private static $menu_icon = 'themes/cadence/images/icons/person.png';
public $showImportForm = false;
private static $managed_models = array(
'Member'
);
private static $url_segment = 'clients';
private static $menu_title = 'Clients';
public function getList() {
$list = parent::getList();
$clientGroup = Group::get()->filter('code', 'clients')->first();
$list = $list->filter('Groups.ID', $clientGroup->ID);
return $list;
}
}
MemberClientExtension.php
<?php
class MemberClientExtension extends DataExtension implements PermissionProvider
{
private static $db = array(
);
public function providePermissions() {
return array(
'CLIENTS' => 'Can access the site as a client',
);
}
public function updateCMSFields(FieldList $fields) {
}
public function generatePasswordForClient(){
$plainPassword = $this->owner->create_new_password();
$encryptedPassword = $this->owner->encryptWithUserSettings($plainPassword);
// Need to set password in database here?
return $plainPassword;
}
public function sendClientWelcomeEmail() {
$email = new Email('email#gmail.com', 'email#gmail.com', 'New member sign up');
$email->setTemplate('NewClientSignUp');
$email->populateTemplate(array(
'Email' => $this->owner->Email,
'Password' => $this->generatePasswordForClient()
));
return $email->send();
}
public function onBeforeWrite()
{
parent::onBeforeWrite();
}
public function onAfterWrite()
{
parent::onAfterWrite();
// Seems to send 2x emails.. Only want to send one
$this->sendClientWelcomeEmail();
}
}
You should set temporary plain text password in SetPassword field, and manage the context when onBeforeWrite and onAfterWrite hooks are called.
class MemberClientExtension extends DataExtension
{
protected $sendWelcomeEmail = false;
...
// onBeforeWrite on extension is called after password is encrypted and set
public function validate(ValidationResult $validationResult) {
if (!$this->owner->isInDB()) {
$this->sendWelcomeEmail = true;
}
}
public function onAfterWrite() {
if ($this->sendWelcomeEmail) {
// reset for password change
$this->sendWelcomeEmail = false;
$password = $this->generatePasswordForClient();
$this->owner->changePassword($password);
$this->sendClientWelcomeEmail(array(
'Email' => $this->owner->Email,
'Password' => $password;
));
}
}
}
You could use the populateDefaults() function in your Member extension.
public function populateDefaults() {
$this->owner->changePassword($this->generatePasswordForClient());
}
But let me say this: This is a bad idea. You don't want to send plain text passwords over something as insecure as an email. To let the user to choose its own password is by far the better way.
I have this issue where I'm attempting to create a subrequest in Silex and basically forward my parameters to another controllers. Exhibit A is broken below (after attempts to refactor), and Exhibit B, the original version, works:
Exhibit A ($this->app is lost after creating the request):
class EntriesController {
private $app;
private $req;
public function __construct($app, $req) {
$this->app = $app;
$this->req = $req;
}
public function updateAction() {
//...
//$url defined here (eyesore-ingly long, so not shown)
$subRequest = Request::create($url, 'GET', $params, $this->req->cookies->all(), array(), $this->req->server->all());
//$this->app **no longer** exists here
return $this->app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
}
EntriesController instance is created below:
class AppControllerProvider implements ControllerProviderInterface {
public function connect(Application $app) {
$controllers = $app['controllers_factory'];
//...
$controllers->patch('/edit', function (Request $req) use ($app) {
$entriesCtrl = new \EntriesController($app, $req);
return $entriesCtrl->updateAction();
});
//...
}
Exhibit B (works just fine):
class AppControllerProvider implements ControllerProviderInterface {
public function connect(Application $app) {
$controllers = $app['controllers_factory'];
$controllers->patch('/edit', function (Request $req) use ($app) {
//...
//$url defined here
$subRequest = Request::create($url, 'GET', $params, $this->req->cookies->all(), array(), $this->req->server->all());
return $this->app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
//...
});
I basically just reorganized the logic from Exhibit B's 'PATCH' /edit method body into a controller class, and I passed the Silex Application instance $app to a new instance of the controller class.
The only difference between Exhibit A and Exhibit B as far as I can tell is that you instantiate a controller object in the path method callback. Maybe there is something wrong with how this controller is setup or a namespace issue? Shootin' in the dark here.
I can confirm that in my silex application the following code does not produce a null $app container:
GlobalControllerProvider.php
<?php
namespace Dev\Pub\Provider\Controller;
use Silex\Application;
use Silex\ControllerProviderInterface;
use Symfony\Component\HttpFoundation\Request;
class GlobalControllerProvider implements ControllerProviderInterface
{
public function connect(Application $app)
{
$controllers = $app['controllers_factory'];
$controllers
->get('/', 'Dev\Pub\Controller\GlobalController::indexAction')
->bind('homepage')
;
$controllers
->patch('/edit', function (Request $req) use ($app) {
$entriesCtrl = new \Dev\Pub\Controller\GlobalController();
return $entriesCtrl->updateAction($app, $req);
});
return $controllers;
}
}
GlobalController.php
<?php
namespace Dev\Pub\Controller;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class GlobalController
{
public function indexAction(Application $app, Request $request)
{
return new Response($app['twig']->render('index.html.twig'));
}
public function updateAction(Application $app, Request $request)
{
$url = 'http://silex.local/index_dev.php/';
$params = array();
$subRequest = Request::create($url, 'GET', $params, $request->cookies->all(), array(), $request->server->all());
// outputs: 'Silex\Application'
error_log(print_r(get_class($app),1).' '.__FILE__.' '.__LINE__,0);
// outputs: 1
error_log(print_r(is_object($app),1).' '.__FILE__.' '.__LINE__,0);
return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
}
index.main.js
$(function(){
console.log('index.main.js');
$.ajax({
url: "http://silex.local/index_dev.php/edit",
method: "PATCH"
}).done(function( data ) {
console.log(data);
}).fail(function( data ) {
console.log(data);
});
});
How do I add a button to the backend of the CMS that fires an action? I can display the button where I want using:
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldsToTab("Root.ButtonTest", array(
FormAction::create('doAction', 'Action button')
)
);
return $fields;
}
public function doAction()
{
//Do something
}
However the button added does nothing when clicked.
I've seen one example of how to put a button on the main action bar (next to save/publish) but that's not what I'm trying to do.
Looking at the only page of documentation I can find, do I need to do something within:
public function getCMSActions()
{
$actions = parent::getCMSActions();
//Something here?
}
It isn't very clear how to create the action that the button calls.
You'll have to extend/decorate LeftAndMain with your own extension and the action you want to call. Here's an example:
<?php
class MyExtension extends LeftAndMainExtension
{
private static $allowed_actions = array(
'doAction'
);
public function doAction($data, $form){
$className = $this->owner->stat('tree_class');
$SQL_id = Convert::raw2sql($data['ID']);
$record = DataObject::get_by_id($className, $SQL_id);
if(!$record || !$record->ID){
throw new SS_HTTPResponse_Exception(
"Bad record ID #" . (int)$data['ID'], 404);
}
// at this point you have a $record,
// which is your page you can work with!
// this generates a message that will show up in the CMS
$this->owner->response->addHeader(
'X-Status',
rawurlencode('Success message!')
);
return $this->owner->getResponseNegotiator()
->respond($this->owner->request);
}
}
Once you have written an extension like this, you'll have to apply it to LeftAndMain by adding the following to your mysite/_config/config.yml:
LeftAndMain:
extensions:
- MyExtension
That's it. Your doAction button should now actually do something!
Not sure if this is helpful, but here's how you can add action-buttons to a ModelAdmin.
(does reload the page)
...in the admin class:
public function getEditForm($id = null, $fields = null)
{
$form = parent::getEditForm($id, $fields);
$form
->Fields()
->fieldByName($this->sanitiseClassName($this->modelClass))
->getConfig()
->getComponentByType('GridFieldDetailForm')
->setItemRequestClass('MyGridFieldDetailForm_ItemRequest');
return $form;
}
MyGridFieldDetailForm_ItemRequest.php
class MyGridFieldDetailForm_ItemRequest extends GridFieldDetailForm_ItemRequest
{
function ItemEditForm()
{
$form = parent::ItemEditForm();
$formActions = $form->Actions();
$button = FormAction::create('myAction');
$button->setTitle('button label');
$button->addExtraClass('ss-ui-action-constructive');
$formActions->push($button);
$form->setActions($formActions);
return $form;
}
public function myAction(){ //do things }
}