Symfony 6 - Creating excel with PHPSpreadsheet and downloading it asynchronously - php

I'm stuck on something, and it seems internet haven't had this problem (or i haven't got the right keyword to find the answer)
Keep in mind that I'm still learning Symfony 6 and I'm a bit by myself for now ... So I'm open if you tell me that everything I did is garbage.
I'm creating an application to export datas in excels for our customers.
I create a call on a database, with a specific SQL request to get my datas. Then i send the datas in a SpreadsheetService to create my spreadsheet and launch the download for the user.
<?php
namespace App\Service;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SpreadsheetService {
public function export(string $title, $datas, $rsm) {
$streamedResponse = new StreamedResponse();
$streamedResponse->setCallback(function () use ($title, $datas, $rsm) {
// Generating SpreadSheet
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle($title);
// Generating First Row with column name
$sheet->fromArray($rsm->scalarMappings);
// Generating other rows with datas
$count = 2;
foreach ($datas as $data) {
$sheet->fromArray($data, null, 'A' . $count);
$count++;
}
// Write and send created spreadsheet
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
// This exit(); is required to prevent errors while opening the generated .xlsx
exit();
});
// Puting headers on response and sending it
$streamedResponse->setStatusCode(Response::HTTP_OK);
$streamedResponse->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$streamedResponse->headers->set('Content-Disposition', 'attachment; filename="' . $title . '.xlsx"');
$streamedResponse->send();
return;
}
So, this is working like a charm. BUT, my chief want it to be asynchronous.
After some research on Symfony 6 and async in Symfony, I happened to find something called symfony/messenger, which at first sounded like it was only for send messages (mail, chat, sms ...) but after some reading, sounded like the async library for Symfony 6.
So, i tried by following step by step to setup the async.
First, i created an ExportMessage.php
<?php
namespace App\Message;
class ExportMessage {
// I need a slug to know for which customer i want the export
private string $slug;
// I need this string to know if it is an export for their clients, or their candidats etc ... (Wanted to setup an enum, but failed trying for now ...)
private string $typeExport;
public function __construct(string $slug, string $typeExport) {
$this->slug = $slug;
$this->typeExport = $typeExport;
}
/**
* Get the value of slug
*/
public function getSlug() {
return $this->slug;
}
/**
* Get the value of typeExport
*/
public function getTypeExport() {
return $this->typeExport;
}
}
Then i created an ExportHandler.php that will do some work when i send an ExportMessage (They go together)
<?php
namespace App\MessageHandler;
use App\Message\ExportMessage;
use App\Service\ClientRequestService;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class ExportHandler implements MessageHandlerInterface {
private ClientRequestService $clientRequestService;
// clientRequestService is the service that send the sql request and then call the SpreadsheetService to create the excel file
public function __construct(ClientRequestService $clientRequestService) {
$this->clientRequestService = $clientRequestService;
}
public function __invoke(ExportMessage $message) {
return $this->clientRequestService->export($message->getSlug(), $message->getTypeExport());
}
}
Finally, in my Controller, I don't call clientRequestService->export anymore, i create a messageBus that will keep track of any messages i send, and will process them correctly (Something like that, I didn't understand every aspect of it for now I think)
class DashboardController extends AbstractController {
private MessageBusInterface $messageBus;
public function __construct([...], MessageBusInterface $messageBus) {
[...]
$this->messageBus = $messageBus;
}
[...]
#[Route('{slug}/export-candidats', name: 'app_export_candidats')]
public function exportCandidats(string $slug) {
$this->messageBus->dispatch(new ExportMessage($slug, 'candidats'));
// Not anymore --> $this->requestService->export($slug, 'candidats');
return $this->redirectToRoute('app_dashboard', ['slug' => $slug]);
}
[...]
And just for the sake of it, here's the clientRequestService.php in case
<?php
namespace App\Service;
use App\Service\MappingService;
use App\Service\SpreadsheetService;
use App\Factory\EntityManagerFactory;
use App\Repository\Istrator\DatabaseGroupRepository;
class ClientRequestService {
private $factory;
private $databaseGroupRepository;
private $mappingService;
private $spreadsheetService;
private $rootpath_sql_request;
public function __construct(EntityManagerFactory $factory, DatabaseGroupRepository $databaseGroupRepository, MappingService $mappingService, SpreadsheetService $spreadsheetService, string $rootpath_sql_request) {
$this->factory = $factory;
$this->databaseGroupRepository = $databaseGroupRepository;
$this->mappingService = $mappingService;
$this->spreadsheetService = $spreadsheetService;
$this->rootpath_sql_request = $rootpath_sql_request;
}
public function export(string $slug, $export) {
$databaseGroup = $this->databaseGroupRepository->findBySlug($slug);
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
switch ($export) {
case 'candidats':
$rsm = $this->mappingService->getMappingExportCandidats($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_candidats.sql");
break;
case 'clients':
$rsm = $this->mappingService->getMappingExportClients($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_clients.sql");
break;
case 'pieces_jointes':
$rsm = $this->mappingService->getMappingPiecesJointes($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_noms_pj.sql");
break;
case 'notes_suivi':
$rsm = $this->mappingService->getMappingNotesSuivi($entityManager);
$query = file_get_contents($this->rootpath_sql_request . "export_notes_suivi.sql");
break;
default:
return;
}
$results = $entityManager->createNativeQuery($query, $rsm)->execute();
$this->spreadsheetService->export($export, $results, $rsm);
}
}
It seems to be okay, but this doesn't trigger the download ...
Can someone help me understand this problem ?
EDIT 1:
After some research, i found out that the Handler isn't even called.
I tried some thing :
In the messenger.yaml i defined my ExportMessage as async :
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
// --------------- Here ----------------
App\Message\ExportMessage: async
And then in my services.yaml I defined my handler as a Service
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
App\MessageHandler\ExportHandler:
tags: [messenger.message_handler]
[...]
Maybe this can narrow down some problems. My handlers isn't called and i don't understand why.
My message is sent (It's created in the database)
| 30 | O:36:\"Symfony\\Component\\Messenger\\Envelope\":2:{s:44:\"\0Symfony\\Component\\Messenger\\Envelope\0stamps\";a:1:{s:46:\"Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\";a:1:{i:0;O:46:\"Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\":1:{s:55:\"\0Symfony\\Component\\Messenger\\Stamp\\BusNameStamp\0busName\";s:21:\"messenger.bus.default\";}}}s:45:\"\0Symfony\\Component\\Messenger\\Envelope\0message\";O:25:\"App\\Message\\ExportMessage\":2:{s:31:\"\0App\\Message\\ExportMessage\0slug\";s:4:\"toma\";s:37:\"\0App\\Message\\ExportMessage\0typeExport\";s:9:\"candidats\";}} | [] | default | 2022-04-26 14:36:53 | 2022-04-26 14:36:53 | NULL |
I continue to work on it.
EDIT 2 :
Ok, so I didn't understand how asynchrones buses worked in php, because I'm from Typescript, and asynchronous process in Typescript are really different compared to this.
I needed a consumers that will listen when there is a message pushed in the bus, and consume it, and send it to the handler...
The documentation explained that, but i didn't understand :
https://symfony.com/doc/current/the-fast-track/en/18-async.html#running-workers-in-the-background
So now, I can generate my excel file asynchronously. I just have to create something to watch for it to be created, and give a link to download it.
Hope this thread can help some people who, like me, didn't quite understand the bus mecanic.

Related

Get variable defined in service from another controller

I will go straight to the point
I have these 2 simple controllers:
/**
* #Route("/controller1", name="controller1")
*/
public function controller1(UserVerification $verification)
{
$verification->setVerificationCode();
return $this->render('user_settings/settings.html.twig');
}
/**
* #Route("/controller2", name="controller2")
*/
public function controller2(UserVerification $verification)
{
$verificationCode = $verification->getVerificationCode();
return $this->render('user_settings/settings.html.twig', [
'verificationCode' => $verificationCode
]);
}
And these two methods in my UserVerification Service:
public function setVerificationCode(){
$this->verificationCode = rand(100000, 999999);
return $this;
}
public function getVerificationCode(): int
{
return $this->verificationCode;
}
My question is: Is that real to get verificationCode in controller2 which has been set in controller1? For now in that example above in controller1 when i use method getVerificationCode() it's successfully returning some random code, but of course in controller2 it's returning null. Is there a way to share service instance?
Thanks for any advice
Here is the point:
In Symfony , by default , services are shared , that mean the same instance is working for many request , if you want the contrary you can modify it in service declaration of your service like :
# config/services.yaml
services:
App\SomeNonSharedService:
shared: false
# ...
But , here , it's different , you function is returning a random value , so even you are using the same service instance , it's sure that you will not have the same result , so here you can do 2 thing :
1- save data into session in case this logic concerns user when is logged in :
2- save data into the dababase
For the two solutions , you have for exmaple to verify if the value already exist into session/DB , like for the first this , it's your service that occur the result , and in the sametime store the value samewhere , so the next request , it will receive data from where you sotre it .
here is an example if you are using Session :
# config/services.yaml
services:
App\Services\CodeService:
arguments:
- "#session"
ServiceCode :
<?php
namespace App\Services;
use Symfony\Component\HttpFoundation\Session\Session;
class CodeService
{
private $session;
public function __construct( Session $session)
{
$this->session = $session;
}
pulic function setVerificationId(){
if(isset(!$this->session->get('verificationCode'))){
$this->session->set('verificationCode', rand(100000, 999999));
}
return this->getVerificationId();
}
public function getVerificationId()
{
return $this->session->get('verificationCode');
}
}
This case is working only if you are using session , if not , do the
same thing using EntityManager to store data somewhere in database.
Hope that help you.

Mocking a service called by a controller from a WebTestCase

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

Symfony, Call service in PHP Unit

I have a unit test, where I need to call a service.
I did it that way:
<?php
namespace FM\PriceBundle\Tests\Service;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class PriceServiceTest extends KernelTestCase
{
private $container;
public function testFiulPrice()
{
self::bootKernel();
$this->container = self::$kernel->getContainer();
$productId = 1;
$id = 1;
$what = ['postal', 'departement', 'region', 'country', 'insee'];
$date = new \DateTime('2016-06-23');
$price = $this->container->get('fm.price.get')->fiulPrice($productId, $id, $what[0], $date);
$this->assertNotEmpty($price);
}
}
But I have this error:
There was 1 error:
1) FM\PriceBundle\Tests\Service\PriceServiceTest::testFiulPrice
Predis\Connection\ConnectionException: Connection refused [tcp://localhost:6379]
I guess you use redis in your app.
But I don't understand why do you want to implement test like this.
That test literally takes fixed data, an tries to get a result from external service that uses dependency (redis).
In my opinion, you need unit test for PriceService, another one for serivce registered as 'fm.price.get', and (maybe) the third one that use redis, but in this case you have to populate cache with some sample data.

RabbitMQ wait for multiple queues to finish

Ok here is an overview of what's going on:
M <-- Message with unique id of 1234
|
+-Start Queue
|
|
| <-- Exchange
/|\
/ | \
/ | \ <-- bind to multiple queues
Q1 Q2 Q3
\ | / <-- start of the problem is here
\ | /
\ | /
\|/
|
Q4 <-- Queues 1,2 and 3 must finish first before Queue 4 can start
|
C <-- Consumer
So I have an exchange that pushes to multiple queues, each queue has a task, once all tasks are completed, only then can Queue 4 start.
So message with unique id of 1234 gets sent to the exchange, the exchange routes it to all the task queues ( Q1, Q2, Q3, etc... ), when all the tasks for message id 1234 have completed, run Q4 for message id 1234.
How can I implement this?
Using Symfony2, RabbitMQBundle and RabbitMQ 3.x
Resources:
http://www.rabbitmq.com/tutorials/amqp-concepts.html
http://www.rabbitmq.com/tutorials/tutorial-six-python.html
UPDATE #1
Ok I think this is what I'm looking for:
https://github.com/videlalvaro/Thumper/tree/master/examples/parallel_processing
RPC with Parallel Processing, but how do I set the Correlation Id to be my unique id to group the messages and also identify what queue?
You need to implement this: http://www.eaipatterns.com/Aggregator.html but the RabbitMQBundle for Symfony doesn't support that so you would have to use the underlying php-amqplib.
A normal consumer callback from the bundle will get an AMQPMessage. From there you can access the channel and manually publish to whatever exchanges comes next in your "pipes and filters" implementation
In the RPC tutorial at RabbitMQ's site, there is a way to pass around a 'Correlation id' that can identify your messages to users in the queue.
I'd recommend using some sort of id with your messages into the first 3 queues and then have another process to dequeue messages from the 3 into buckets of some sort. When those buckets receive what I'm assuming is the completion of there 3 tasks, send the final message off to the 4th queue for processing.
If you are sending more than 1 work item to each queue for one user, you might have to do a little preprocessing to find out how many items a particular user placed into the queue so the process dequeuing before 4 knows how many to expect before queuing up.
I do my rabbitmq in C#, so sorry my pseudo code isn't in php style
// Client
byte[] body = new byte[size];
body[0] = uniqueUserId;
body[1] = howManyWorkItems;
body[2] = command;
// Setup your body here
Queue(body)
// Server
// Process queue 1, 2, 3
Dequeue(message)
switch(message.body[2])
{
// process however you see fit
}
processedMessages[message.body[0]]++;
if(processedMessages[message.body[0]] == message.body[1])
{
// Send to queue 4
Queue(newMessage)
}
Response to Update #1
Instead of thinking of your client as a terminal, it might be useful to think of the client as a process on a server. So if you setup an RPC client on a server like this one, then all you need to do is have the server handle the generation of a unique id of a user and send the messages to the appropriate queues:
public function call($uniqueUserId, $workItem) {
$this->response = null;
$this->corr_id = uniqid();
$msg = new AMQPMessage(
serialize(array($uniqueUserId, $workItem)),
array('correlation_id' => $this->corr_id,
'reply_to' => $this->callback_queue)
);
$this->channel->basic_publish($msg, '', 'rpc_queue');
while(!$this->response) {
$this->channel->wait();
}
// We assume that in the response we will get our id back
return deserialize($this->response);
}
$rpc = new Rpc();
// Get unique user information and work items here
// Pass even more information in here, like what queue to use or you could even loop over this to send all the work items to the queues they need.
$response = rpc->call($uniqueUserId, $workItem);
$responseBuckets[array[0]]++;
// Just like above code that sees if a bucket is full or not
I am a little unclear on what you are trying to achieve here. But I would probably alter the design somewhat so that once all messages are cleared from the queues you publish to a separate exchange which publishes to queue 4.
In addition to my RPC based answer I want to add another one which is based on EIP aggregator pattern.
The idea is next: Everything is async, no RPC or other sync things. Every task sends an even when it is done, The aggregator is subscribed to that event. It basically counts tasks and sends task4 message when the counter reaches expected number (in our case 3). I choose a filesystem as a storage for counters for the Sake of simplicity. You can use a database there.
The producer looks simpler. It just fires and forgets
<?php
use Enqueue\Client\Message;
use Enqueue\Client\ProducerInterface;
use Enqueue\Util\UUID;
use Symfony\Component\DependencyInjection\ContainerInterface;
/** #var ContainerInterface $container */
/** #var ProducerInterface $producer */
$producer = $container->get('enqueue.client.producer');
$message = new Message('the task data');
$message->setCorrelationId(UUID::generate());
$producer->sendCommand('task1', clone $message);
$producer->sendCommand('task2', clone $message);
$producer->sendCommand('task3', clone $message);
The task processor has to send an event once its job is done:
<?php
use Enqueue\Client\CommandSubscriberInterface;
use Enqueue\Client\Message;
use Enqueue\Client\ProducerInterface;
use Enqueue\Psr\PsrContext;
use Enqueue\Psr\PsrMessage;
use Enqueue\Psr\PsrProcessor;
class Task1Processor implements PsrProcessor, CommandSubscriberInterface
{
private $producer;
public function __construct(ProducerInterface $producer)
{
$this->producer = $producer;
}
public function process(PsrMessage $message, PsrContext $context)
{
// do the job
// same for other
$eventMessage = new Message('the event data');
$eventMessage->setCorrelationId($message->getCorrelationId());
$this->producer->sendEvent('task_is_done', $eventMessage);
return self::ACK;
}
public static function getSubscribedCommand()
{
return 'task1';
}
}
And the aggregator processor:
<?php
use Enqueue\Client\TopicSubscriberInterface;
use Enqueue\Psr\PsrContext;
use Enqueue\Psr\PsrMessage;
use Enqueue\Psr\PsrProcessor;
use Symfony\Component\Filesystem\LockHandler;
class AggregatorProcessor implements PsrProcessor, TopicSubscriberInterface
{
private $producer;
private $rootDir;
/**
* #param ProducerInterface $producer
* #param string $rootDir
*/
public function __construct(ProducerInterface $producer, $rootDir)
{
$this->producer = $producer;
$this->rootDir = $rootDir;
}
public function process(PsrMessage $message, PsrContext $context)
{
$expectedNumberOfTasks = 3;
if (false == $cId = $message->getCorrelationId()) {
return self::REJECT;
}
try {
$lockHandler = new LockHandler($cId, $this->rootDir.'/var/tasks');
$lockHandler->lock(true);
$currentNumberOfProcessedTasks = 0;
if (file_exists($this->rootDir.'/var/tasks/'.$cId)) {
$currentNumberOfProcessedTasks = file_get_contents($this->rootDir.'/var/tasks/'.$cId);
if ($currentNumberOfProcessedTasks +1 == $expectedNumberOfTasks) {
unlink($this->rootDir.'/var/tasks/'.$cId);
$this->producer->sendCommand('task4', 'the task data');
return self::ACK;
}
}
file_put_contents($this->rootDir.'/var/tasks/'.$cId, ++$currentNumberOfProcessedTasks);
return self::ACK;
} finally {
$lockHandler->release();
}
}
public static function getSubscribedTopics()
{
return 'task_is_done';
}
}
I can show you how you can do it with enqueue-bundle.
So install it with composer and register as any other bundle. Then configure:
// app/config/config.yml
enqueue:
transport:
default: 'amnqp://'
client: ~
This approach is based on RPC. Here's how you do it:
<?php
use Enqueue\Client\ProducerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/** #var ContainerInterface $container */
/** #var ProducerInterface $producer */
$producer = $container->get('enqueue.client.producer');
$promises = new SplObjectStorage();
$promises->attach($producer->sendCommand('task1', 'the task data', true));
$promises->attach($producer->sendCommand('task2', 'the task data', true));
$promises->attach($producer->sendCommand('task3', 'the task data', true));
while (count($promises)) {
foreach ($promises as $promise) {
if ($replyMessage = $promise->receiveNoWait()) {
// you may want to check the response here
$promises->detach($promise);
}
}
}
$producer->sendCommand('task4', 'the task data');
The consumer processor looks like this:
use Enqueue\Client\CommandSubscriberInterface;
use Enqueue\Consumption\Result;
use Enqueue\Psr\PsrContext;
use Enqueue\Psr\PsrMessage;
use Enqueue\Psr\PsrProcessor;
class Task1Processor implements PsrProcessor, CommandSubscriberInterface
{
public function process(PsrMessage $message, PsrContext $context)
{
// do task job
return Result::reply($context->createMessage('the reply data'));
}
public static function getSubscribedCommand()
{
// you can simply return 'task1'; if you do not need a custom queue, and you are fine to use what enqueue chooses.
return [
'processorName' => 'task1',
'queueName' => 'Q1',
'queueNameHardcoded' => true,
'exclusive' => true,
];
}
}
Add it to your container as a service with a tag enqueue.client.processor and run command bin/console enqueue:consume --setup-broker -vvv
Here's the plain PHP version.

Symfony 2 load different template depending on user agent properties

Is it possible (and how) to
determine if a user is using a mobile device
force symfony 2 to load different template in that case
(and fall back the default html template)
What id like to do is, to load different templates without modifying any controller.
UPDATE
It wasn't the detection part the real issue here, it's really nothing to do with symfony. It can be done (load different template) on a controller level:
public function indexAction()
{
$format = $this->isMobile() ? 'mob' : 'html';
return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
}
But can it be done globally? Like a service, or something that execute before every request, and make changes in the templating rules.
Ok, so I don't have a full solution but a little more than where to look for one :)
You can specify loaders (services) for templating item in app/config/config.yml
framework:
esi: { enabled: true }
#translator: { fallback: %locale% }
secret: %secret%
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: %kernel.debug%
form: true
csrf_protection: true
validation: { enable_annotations: true }
templating:
engines:
- twig
loaders: [moby.loader]
default_locale: %locale%
trust_proxy_headers: false
session: ~
Then define the mentioned loader service:
services:
moby.loader:
class: Acme\AppBundle\Twig\Loader\MobyFilesystemLoader
arguments: ["#templating.locator", "#service_container"]
After that define your loader service class:
namespace Acme\AppBundle\Twig\Loader;
use Symfony\Bundle\FrameworkBundle\Templating\Loader\FilesystemLoader;
use Symfony\Component\Templating\Storage\FileStorage;
class MobyFilesystemLoader extends FilesystemLoader
{
protected $container;
public function __construct($templatePathPatterns, $container)
{
parent::__construct($templatePathPatterns);
$this->container = $container;
}
public function load(\Symfony\Component\Templating\TemplateReferenceInterface $template)
{
// Here you can filter what you actually want to change from html
// to mob format
// ->get('controller') returns the name of a controller
// ->get('name') returns the name of the template
if($template->get('bundle') == 'AcmeAppBundle')
{
$request = $this->container->get('request');
$format = $this->isMobile($request) ? 'mob' : 'html';
$template->set('format', $format);
}
try {
$file = $this->locator->locate($template);
} catch (\InvalidArgumentException $e) {
return false;
}
return new FileStorage($file);
}
/**
* Implement your check to see if request is made from mobile platform
*/
private function isMobile($request)
{
return true;
}
}
As you can see this isn't the full solution, but I hope that this, at least, points you to the right direction.
EDIT: Just found out that there is a bundle with mobile detection capabilities, with custom twig engine that renders template file depending on a device that sent request
ZenstruckMobileBundle, although I never used it so... :)
Well, you can use LiipThemeBundle.
You can utilize kernel.view event listener. This event comes to action when controller returns no response, only data. You can set reponse according to user agent property. For example
In your controller,
public function indexAction()
{
$data = ... //data prepared for view
$data['template_name'] = "AcmeBlogBundle:Blog:index";
return $data;
}
And the in your kernel.view event listener,
<?php
namespace Your\Namespace;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Templating\EngineInterface;
Class ViewListener
{
/**
* #var EngineInterface
*/
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$data = $event->getControllerResult(); //result returned by the controller
$templateName = $data['template_name'];
$format = $this->isMobile() ? 'mob' : 'html'; //isMobile() method may come from a injected service
$response = $this->templating->renderResponse($templateName . "." . $format . "twig", $data);
$event->setResponse($response);
}
}
Service definition,
your_view_listener.listener:
class: FQCN\Of\Listener\Class
arguments: [#templating]
tags:
- { name: kernel.event_listener, event: kernel.view, method: onKernelView }
This is what did the trick for me in Symfony 2.0:
Override twig.loader service so we can set our custom class:
twig.loader:
class: Acme\AppBundle\TwigLoader\MobileFilesystemLoader
arguments:
locator: "#templating.locator"
parser: "#templating.name_parser"
And create our custom class, that just sets "mob" format to the templates in case the client is a mobile device:
namespace Acme\AppBundle\TwigLoader;
use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader;
class MobileFilesystemLoader extends FilesystemLoader
{
public function findTemplate($template)
{
if ($this->isMobile()) {
$template->set('format', 'mob');
}
return parent::findTemplate($template);
}
private function isMobile()
{
//do whatever to detect it
}
}
I would suggest that this is not best handled by the controller but by CSS media queries, and serving a separate stylesheet to different classes of devices based on the results of that CSS media query.
A good intro here:
http://www.adobe.com/devnet/dreamweaver/articles/introducing-media-queries.html
and I would try reading http://www.abookapart.com/products/responsive-web-design in great detail. Some thinking has been done since the book was published, but it will get you headed the right direction.
From my experiences, you can but by specifying a format in the first place - check these docs, they may be able to assist you
I think is nothing to do with symfony. Templates are for the VIEW. You may achieve this by using different CSS for the same template to get different layout (template). I am using jQuery and CSS to handle different devices. You may want to look at some source code of the UI from http://themeforest.net/; specifically this template. This is one handles different device.
Alternative: https://github.com/suncat2000/MobileDetectBundle
I found it quite good compared to https://github.com/kbond/ZenstruckMobileBundle and https://github.com/liip/LiipThemeBundle

Categories