In my project (BtoB project), I have a global application with a lot of modules in it.
Each module provides common functionnalities, for all of my clients.
I have also in the root directory a clients folder, in it, I have all clients specificities, in their folder.
Thoses folders, aren't modules. So they are not loaded with Zf2. I usually load those specificities with abstractFactories.
This architecture follow is what I have currently :
- clients
- clientOne
- Invoice
- Cart
- Orders
- clientTwo
- Invoice
- Orders
- clientThree
- Reporting
- module
- Application
- CartModule
- InvoiceModule
- OrdersModule
- Reporting
My clients wants to have some custom views, sometimes, they ask us to provide those views. But my application give a common view for all of them. I have to modify this architecture to load a client view if it exist, or load the common view.
To handle this case I Imagine to have into each clients folder this :
- client
- clientOne
- Invoice
- Cart
- View
- cartView.phtml
- Orders
EDIT :
After some good answers (#AlexP & #Wilt), I tried to implements this solution :
So I have a ClientStrategy; it's factory is Like This :
<?php
namespace Application\View\Strategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Application\View\Resolver\TemplateMapResolver;
use Zend\View\Resolver;
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$map = $serviceLocator->get('config')['view_manager']['template_map'];
$resolver = new Resolver\AggregateResolver();
$map = new TemplateMapResolver($map, $this->clientMap($session->offsetGet('cod_entprim')));
$resolver
->attach($map)
->attach(new Resolver\RelativeFallbackResolver($map));
$viewRenderer->setResolver($resolver);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #return array
*/
public function clientMap($codprim)
{
$clients = array(
21500 => 'clientOne',
32000 => 'clientTwo',
// ..
);
return (isset($clients[$codprim])) ? $clients[$codprim]: false;
}
}
My clientMap method allow me to load my client folder, and views it may have in it like this :
class ClientOne
{
/**
* get The main Code
* #return integer
*/
public function getCodEntPrim()
{
return 21500;
}
/**
* Load all customs views
* #return array
*/
public function customViews()
{
return array(
'addDotations' => __DIR__ . '/Dotations/view/dotations/dotations/add-dotations.phtml',
);
}
/**
* GetName
* #return string
*/
public function getName()
{
return get_class();
}
}
So when it comes to my TemplateMapResolver to do his job I do this :
<?php
namespace Application\View\Resolver;
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving view.
*
* #param string $clientName
*/
protected $clientName;
/**
* Merge nos vues avec celle clients avant de repeupler l'arrayMap global
* #param array $map [description]
*/
public function __construct(array $map, $client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->mergeMap($map);
}
parent::__construct($map);
}
/**
* Merge les map normales avec les map clients, pas propre ?
* #param array $map
* #return array
*/
public function mergeMap($map)
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
$clientMap = $class->customViews();
return array_replace_recursive($map, $clientMap);
}
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
return parent::get($name);
}
/**
* Gets the Client name to use when retrieving view.
*
* #return string
*/
public function getClientName()
{
return $this->clientName;
}
/**
* Sets the Client name to use when retrieving view.
*
* #param mixed $clientName the client name
*
* #return self
*/
public function setClientName($clientName)
{
$this->clientName = $clientName;
return $this;
}
}
I tried a lot of things, this works but somes issues cames up :
My template_path_stack not works anymore, so a lot of my views are broken.
I think this is a complete mess, to do this, that way.
Hard to maintain.
I understand a bit better, how it works, but i'm still unable to implements it the good way.
If you really want to do that (I am not so sure if it is the best way) then you can extend the TemplateMapResolver with your custom logic and set it in your Renderer instance.
Make your custom class:
<?php
Application\View\Resolver
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving template.
*
* #param string $clientName
*/
protected $clientName;
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
if ($this->has($clientName . '_' . $name)) {
return $this->map[$clientName . '_' . $name];
}
if (!$this->has($name)) {
return false;
}
return $this->map[$name];
}
}
And now something like:
$resolver = new TemplateMapResolver();
$resolver->setClientName($clientName);
// Get the renderer instance
$renderer->setResolver($resolver);
You might still have to take care of setting the map in the resolver. Maybe you can just get it from the old resolver? I am not sure... That is for you to find out. This is just to get you on the correct way.
So if you set cart_view as a template it will first try to get client_name_cart_view if not found it sets cart_view.
UPDATE
If you want to take this to the next level, then what you can do is make a custom view model for example ClientViewModel that extends the normal ViewModel class.
The constructor for this ClientViewModel takes both a client and a template name:
new ClientViewModel($client, $template, $variables, $options);
$variables and $options are optional and can be passed to the parent::__construct (constructor of the normal ViewModel)
The next step would be to create a Application\View\ClientStrategy.
This strategy is connected on render event and in this strategy you add a ViewRenderer instance with your custom TemplateMapResolver set. During rendering you can get your client from your ViewModel and find the correct template in your TemplateMapResolver using this client.
More details can be found online, there are examples. Check for example here.
The advantage will be that other views with ViewModel or JsonModel will be rendered as normally, only your ClientViewModel gets a special treatment. Thus you are not breaking your applications default logic.
Requirements
Multiple possible views per client
Default view fallback if client specific view not found
Create a new service, say TemplateProviderService which has a simple interface.
interface ViewTemplateProviderInterface
{
public function hasTemplate($name);
public function getTemplates();
public function setTemplates($templates);
public function getTemplate($name);
public function setTemplate($name, $template);
public function removeTemplate($name);
public function removeTemplates();
}
Inject and hard code the template name in controller classes.
// Some controller class
public function fooAction()
{
$view = new ViewModel();
$view->setTemplate($this->templateProvider->get('some_view_name'));
return $view;
}
Now you can create client specific factories that inject custom template script config into your template provider. All you would then need to do is decide which template provider service you want to inject into your controller.
class ViewTemplateProviderFactory
{
public function __invoke($sm, $name, $rname)
{
$config = $sm->get('config');
if (! isset($config['view_template_providers'][$rname])) {
throw new ServiceNotCreatedException(sprintf('No view template provider config for \'%s\'.', $rname));
}
return new ViewTemplateProvider($config['view_template_providers'][$rname]);
}
}
The key here is ALL view scripts, for all clients, are registered under the 'view_manager' key as normal however the name of the template in the controller never changes.
Edit
You could just use one factory and pull from config (see changes above).
return [
'view_template_providers' => [
'ClientOneTemplateProvider' => [
'some_view_name' => 'name_of_script_1'
],
'ClientTwoTemplateProvider' => [
'some_view_name' => 'name_of_script_2'
],
'ClientThreeTemplateProvider' => [
'some_view_name' => 'name_of_script_3',
],
],
'service_manager' => [
'factories' => [
'ClientOneTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientTwoTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientThreeTemplateProvider' => 'ViewTemplateProviderFactory',
],
],
'view_manager' => [
'template_map' => [
'name_of_script_1' => __DIR__ . 'file/path/to/script',
'name_of_script_2' => __DIR__ . 'file/path/to/script',
'name_of_script_3' => __DIR__ . 'file/path/to/script',
],
],
];
It seems I solved my problem, but i'm not sure it's the good way to do it. So if someone can do better, I let the bounty runs for a better solution, if exists.
Here is what I've done :
/**
* Factory permettant d'établir que les vues client soient chargé si elle existent, avant les vues par défaut.
*/
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$clientList = $serviceLocator->get('Config')['customers_list'];
$clientName = $this->clientMap($session->offsetGet('cod_entprim'), $clientList);
$clientMap = new TemplateMapResolver($clientName);
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #param integer $codprim
* #param array $clientList
* #return array
*/
public function clientMap($codprim, $clientList)
{
return (isset($clientList[$codprim])) ? $clientList[$codprim]: false;
}
}
You can see that my custom TemplateMapResolver needs a clientName, this is for loading custom views. But the most important thing is : I don't create a new Resolver, I just add my Resolver to the list by this line :
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
The second argument means, that this resolver is top priority (Default priority is 1)
My TemplateMapResolver is pretty much simple, the most important thing is this :
public function __construct($client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->getMap();
} else {
$map = array();
}
parent::__construct($map);
}
/**
* Return all custom views for one client
* #param array $map
* #return array
*/
public function getMap()
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
return $class->customViews();
}
My solution, force me to create then a class in my clients folder with the same name of the folder so, if my clientName is TrumanShow i will have an architecture like :
- [clients]
-- [TrumanShow]
--- TrumanShow.php
--- [Cart]
---- [view]
----- [cart]
------ [index]
------- cart-view.phtml
--- [Invoice]
--- [Reporting]
And in this file I will have this function that declare all my custom views :
/**
* Ici nous mettons nos custom views afin de les charger dans le template Map
* #return array
*/
public function customViews()
{
return array(
'cartView' => __DIR__ . '/Cart/view/cart/index/cart-view.phtml',
);
}
So it's possible to do this without break template_path_stack or my others routes. Now I have to call setTemplate method in my Controller, like this :
// code ...
public function cartAction() {
$view->setTemplate('cartView');
return $view;
}
And ZendFramework will check first if a custom view exists in my clients folder, or load the common view if no view is found.
Thanks to #Wilt and #AlexP for their contribution and help.
Don't overcomplicate things. Just set the ViewModel's template before you render it.
$vm = new ViewModel();
$vm->setTemplate( $user_service->getTemplate( $this->getRequest() ) );
return $vm;
Pretty clean if you inject your user into this fictitious user service, and use it to ascertain which template to inject.
The concern of the $user_service should be completely disparate from the concern for your Controller action.
Related
Im building project on Laravel 7.3 with multiple Jobs that run at the same time.
I need to make each Job write logs to different daily rotated file. The name of the log file should be based on model, that Job is processing.
The issue is I cant find smart solution.
What I have tried:
1) creating multiple channels in config/logging.php.
That works as expected but at the moment there are about 50 different Jobs and amount keeps growing. Method is ugly and hardly maintained.
2) setting up Config(['logging.channels.CUSTOMCHANNEL.path' => storage_path('logs/platform/'.$this->platform->name.'.log')]);.
Messing with Config variable was bad idea because of many Jobs running one time. As a result messages from one job often were written in another Job log.
3) using Log::useDailyFiles()
Seems like this stops working since laravel 5.5 or 5.6. Just getting error Call to undefined method Monolog\Logger::useDailyFiles(). Any thoughts how to make with work in laravel 7?
4) using tap parameter for channel in config/logging.php.
Example in laravel docs
No ideas how to pass model name into CustomizeFormatter to setup file name.
Im almost sure there is smart solution and Im just missing something.
Any suggests? Thanks!
You could inherit the log manager to allow a dynamic configuration
<?php
namespace App\Log;
use Illuminate\Support\Str;
use Illuminate\Log\LogManager as BaseLogManager;
class LogManager extends BaseLogManager
{
/**
* Get the log connection configuration.
*
* #param string $name
* #return array
*/
protected function configurationFor($name)
{
if (!Str::contains($name, ':')) {
return parent::configurationFor($name);
}
[$baseName, $model] = explode(':', $name, 2);
$baseConfig = parent::configurationFor($baseName);
$baseConfig['path'] = ...; //your logic
return $baseConfig;
}
}
Likewise about Laravel's log service provider except this one can be totally replaced
<?php
namespace App\Log;
use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app->singleton('log', function ($app) {
return new LogManager($app);
});
}
}
EDIT: I've just seen that Laravel's log service provider is missing from config/app.php, this is because it's "hard-loaded" by the application. You still can replace it by inheriting the application itself
<?php
namespace App\Foundation;
use App\Log\LogServiceProvider;
use Illuminate\Events\EventServiceProvider;
use Illuminate\Routing\RoutingServiceProvider;
use Illuminate\Foundation\Application as BaseApplication;
class Application extends BaseApplication
{
/**
* Register all of the base service providers.
*
* #return void
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
}
And finally in bootstrap/app.php, replace Illuminate\Foundation\Application with App\Foundation\Application
For example, if you try this
app('log')->channel('single:users')->debug('test');
Laravel will use the single channel's config and write to users.log if your resolution logic is
$baseConfig['path'] = $model + '.log';
I got a solution that I've been using since Laravel 4 that works, although it doesn't follow 'Laravel' way of doing things.
class UserTrackLogger
{
/**
* #var $full_path string
*/
protected $full_path;
/**
* #var $tenant string
*/
protected $tenant;
/**
* #var $user User
*/
protected $user;
/**
* #var $request Request
*/
protected $request;
public static function log(string $message, Request $request, User $user, array $data = []): void
{
/** #noinspection PhpVariableNamingConventionInspection */
$userTrack = new static($request, $user);
$userTrack->write($message, $data);
}
protected function __construct(Request $request, User $user)
{
$this->request = $request;
$this->user = $user;
$this->tenant = app()->make('tenant')->tenant__name;
$path = storage_path() . "/logs/{$this->tenant}/users";
$filename = $this->user->username_with_name;
$this->full_path = Formatter::formatPath("{$path}/{$filename}.log");
self::makeFolder($this->full_path);
}
protected function write(string $message, array $data = []): void
{
$formatter = $this->getFormat();
$record = [
'message' => $message,
'context' => $data,
'extra' => [],
'datetime' => date(Utility::DATETIME_FORMAT_DEFAULT),
'level_name' => 'TRACK',
'channel' => '',
];
file_put_contents($this->full_path, $formatter->format($record), FILE_APPEND);
}
protected function getFormat(): FormatterInterface
{
$ip = $this->request->getClientIp();
$method = strtoupper($this->request->method());
$format = "[%datetime%][{$this->tenant}][{$this->user->username}][{$this->user->name}]: $ip $method %message% %context%\n";
return new LineFormatter($format, null, true);
}
protected static function makeFolder(string $full_path): bool
{
$path = dirname($full_path);
if ( !is_dir($path) ) {
return mkdir($path, 0755, true);
}
return false;
}
}
And when I want to log something, I do UserTrackLogger::log($request->fullUrl(), $request, $user, $data);
What I would suggest is creating a logger similar to this but extends RotatingFileHandler.
Let's say I have a configuration like this (snippet from official guide):
$config = [
// ...
'container' => [
'definitions' => [
'yii\widgets\LinkPager' => ['maxButtonCount' => 5],
],
],
// ...
];
I create a class named FancyLinkPager:
class FancyLinkPager extends \yii\widgets\LinkPager
{
// ...
}
When I create an object of class FancyLinkPager like so (please ignore the $pagination object, it's here for correctness sake):
$pagination = \Yii::createObject(Pagination::class);
$linkPager = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 10 as LinkPager's default
My problem is that I wished $fancyLinkPager->maxButtonCount to be 5 as configured. I know I can add another line in the configuration or adjust it to specify my custom class, but it's not solution for me because:
I want to keep the code DRY
This is an oversimplified example of my needs - in real world you don't expect to have multiple LinkPager's child classes, but it is highly possible for other objects
My question is: is there any framework-supported way of achieving this? The solutions I came up with are:
Hack a custom __construct in FancyLinkPager (or another intermediate class or trait) so that it would look into App's configuration and call Yii::configure on the instance, but I don't find a good way to do it in generic way
Inject a dependency into FancyLinkPager with "setup" object, like LinkPagerSettings and configure that class in container section of my configuration, but it would make some trouble to work with vanilla LinkPager instances as well
Maybe the only real solution would be to create my own implementation of yii\di\Container that allows for inheriting configuration from parent classes but before I dive into this I would like to know if I haven't overlooked something.
Finally I came up with my own implementation of DI container introducing new configurable property:
public $inheritableDefinitions = [];
Full class code:
<?php
namespace app\di;
/**
* #inheritdoc
*/
class Container extends \yii\di\Container
{
/**
* #var array inheritable object configurations indexed by class name
*/
public $inheritableDefinitions = [];
/**
* #inheritdoc
*
* #param string $class
* #param array $params
* #param array $config
*
* #return object the newly created instance of the specified class
*/
protected function build($class, $params, $config) {
$config = $this->mergeInheritedConfiguration($class, $config);
return parent::build($class, $params, $config);
}
/**
* Merges configuration arrays of parent classes into configuration of newly created instance of the specified class.
* Properties defined in child class (via configuration or property declarations) will not get overridden.
*
* #param string $class
* #param array $config
*
* #return array
*/
protected function mergeInheritedConfiguration($class, $config) {
if (empty($this->inheritableDefinitions)) {
return $config;
}
$inheritedConfig = [];
/** #var \ReflectionClass $reflection */
list($reflection) = $this->getDependencies($class);
foreach ($this->inheritableDefinitions as $parentClass => $parentConfig) {
if ($class === $parentClass) {
$inheritedConfig = array_merge($inheritedConfig, $parentConfig);
} else if (is_subclass_of($class, $parentClass)) {
/** #var \ReflectionClass $parentReflection */
list($parentReflection) = $this->getDependencies($parentClass);
// The "#" is necessary because of possible (and wanted) array to string conversions
$notInheritableProperties = #array_diff_assoc($reflection->getDefaultProperties(),
$parentReflection->getDefaultProperties());
// We don't want to override properties defined specifically in child class
$parentConfig = array_diff_key($parentConfig, $notInheritableProperties);
$inheritedConfig = array_merge($inheritedConfig, $parentConfig);
}
}
return array_merge($inheritedConfig, $config);
}
}
This is how it can be used to accomplish customization of LinkPager described in the question:
'container' => [
'inheritableDefinitions' => [
'yii\widgets\LinkPager' => ['maxButtonCount' => 5],
],
],
Now if I create a class FancyLinkPager that extends yii\widgets\LinkPager the DI container will merge the default configuration:
$pagination = \Yii::createObject(Pagination::class);
$linkPager = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 5 as configured - hurrah!
I have also taken into account qiangxue's comment about explicit setting of default property values in class definition, so if we declare the FancyLinkPager class as such:
class FancyLinkPager extends LinkPager
{
public $maxButtonCount = 18;
}
the property setting will be respected:
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 18 as declared
To swap default DI container in your application you have to explicitly set Yii::$container somewhere in an entry script:
Yii::$container = new \app\di\Container();
In ZF1 I used to declare variables in the application.ini
brandname = "Example"
weburl = "http://www.example.com/"
assetsurl = "http://assets.example.com/"
And in the Bootstrap I did this so i could access them in the view
define('BRANDNAME', $this->getApplication()->getOption("brandname"));
define('WEBURL', $this->getApplication()->getOption("weburl"));
define('ASSETSURL', $this->getApplication()->getOption("assetsurl"));
Whats the ZF2 way to do this, I know that i can create an array in the local.php config file like:
return array(
'example' => array(
'brandname' => 'Example',
'weburl' => 'http://www.example.com/',
'asseturl' => 'http://assets.example.com/',
),
);
When I want to access that variable in the controller I can do
$config = $this->getServiceLocator()->get('Config');
$config['example']['brandname']);
So far so good... but how do i access this variable in the view?
I don't want to create a view variable for it in every controller. And when i try the above in a view phtml file i get an error.
Zend\View\HelperPluginManager::get was unable to fetch or create an instance for getServiceLocator
Any ideas?
You could create a sinmple view helper to act as a proxy for your config, (totally un tested).
Module.php
public function getViewHelperConfig()
{
return array(
'factories' => array(
'configItem' => function ($helperPluginManager) {
$serviceLocator = $helperPluginManager->getServiceLocator();
$viewHelper = new View\Helper\ConfigItem();
$viewHelper->setServiceLocator($serviceLocator);
return $viewHelper;
}
),
);
}
ConfigItem.php
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceManager;
/**
* Returns total value (with tax)
*
*/
class ConfigItem extends AbstractHelper
{
/**
* Service Locator
* #var ServiceManager
*/
protected $serviceLocator;
/**
* __invoke
*
* #access public
* #param string
* #return String
*/
public function __invoke($value)
{
$config = $this->serviceLocator->get('config');
if(isset($config[$value])) {
return $config[$value];
}
return NULL;
// we could return a default value, or throw exception etc here
}
/**
* Setter for $serviceLocator
* #param ServiceManager $serviceLocator
*/
public function setServiceLocator(ServiceManager $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
}
You could then do something like this in your view, assuming you have them set in your config of course :)
echo $this->configItem('config_key');
echo $this->configItem('web_url');
I would personally tend to just pass the values through to the view every time though, keeping the view a dumb as possible.
I answered this before on a different post.
/* Inside your action controller method */
// Passing Var Data to Your Layout
$this->layout()->setVariable('stack', 'overflow');
// Passing Var Data to Your Template
$viewModel = new ViewModel(array( 'stack' => 'overflow' ));
/* In Either layout.phtml or {Your Template File}.phtml */
echo $this->stack; // Will print overview
That's it... No need to mess with view helpers, event manager, service manager, or anything else.
Enjoy!
I'm using Symfony 2.3 and I can't find a good solution to add a security on two tables.
I have an user and this user can get an application, he can access to his application with this path: /application/{id}
So I'd like to secure this page if the user is link to the application. I do something to check in my controller but this not very clean:
/**
*
* #param int $idApplication
* #return UserApplication
*/
public function testUserApplication($idApplication){
//get the application
$applicationRepository = $this->getDoctrine()->getRepository('PhoneApplicationBundle:Application');
$application = $applicationRepository->find($idApplication);
if($application==null){
return null;
}
$userApplicationRepository = $this->getDoctrine()->getRepository('PhoneApplicationBundle:UserApplication');
$userApplication = $userApplicationRepository->findOneBy(array(
'user' => $this->getUser(),
'application' => $application
));
return $userApplication;
}
I don't know if I can do this using the security.
I try an other solution creating a service which check this
class Test
{
/** #var \Doctrine\ORM\EntityManager */
private $doctrine;
/**
* Constructor
*
* #param Doctrine $doctrine
*/
public function __construct(Doctrine $doctrine)
{
$this->doctrine = $doctrine;
}
public function userApplication($idApplication){
//get the application
$applicationRepository = $this->doctrine->getRepository('PhoneApplicationBundle:Application');
$application = $applicationRepository->find($idApplication);
if($application==null){
return null;
}
$userApplicationRepository = $this->doctrine->getRepository('PhoneApplicationBundle:UserApplication');
$userApplication = $userApplicationRepository->findOneBy(array(
'user' => $this->getUser(),
'application' => $application
));
return $userApplication;
}
}
service.yml:
parameters:
phone_application.test_user_application.class: Phone\ApplicationBundle\Service\Test
services:
phone_application.test_user_application:
class: %phone_application.test_user_application.class%
arguments: [#doctrine]
But I don't realy understand how to use this in a controller
Thanks for help.
First
If you want to use a service in your controller, do
$serv = $this->get('nameoftheservice);
//then
$serv->yourFunctionOfTheService();
In your case :
$serv = $this->get('phone_application.test_user_application');
//then
$serv->userApplication($id);
Second
Another way to do it :
If the $user object is fully available in your controller just do :
//get the app
$application = $this->getDoctrine()->getRepository('PhoneApplicationBundle:Application')->find($idApplication);
//check if this user owns this app, considering you have sets the right doctrine annotation for relation in your entity file
if($user->getApplications()->contains($application)) {
//do your stuff
}
else
throw new \Exception('No right there ! ');
I would like to use the Twig template system to template my e-mails. The locale of the e-mail should be based on a user setting, not from the session or request locale. How can I force the locale when rendering a Twig template?
The manual does mention how to force the locale for the Translator. But i'd like pass this locale to the render() method, in order to have the translations inside the twig template to be rendered in this locale.
This is different from using into in the template, because I think this forces a translation inside the template in a specific locale.
So, taking the example from Symfony, I'm looking for something like this:
public function indexAction($name)
{
$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),
'nl_NL' // <-- This would be nice!
)
)
;
$this->get('mailer')->send($message);
return $this->render(...);
}
You can pass the locale through as an argument when you use the trans filter (see diff: https://github.com/symfony/symfony/commit/3ea31a02630412b1c732ee1647a0724378f67665).
So you could pass another user_locale variable in your render method in your controller (or pass through the whole user object instead of passing name and user_locale separately, or use app.user in your template if the user will be logged in, etc... (depending on your application obviously)), then in your email template you can have something like this:
{{ 'greeting' | trans({}, "messages", user_locale) }} {{ name | title }}
{# rest of email template with more translation strings #}
Then in your translation file for that locale (assuming you're using yaml) just have something like this and the translations will work nicely for you on the fly:
# messages.fr.yml
greeting: 'Bonjour'
Get a hold of the Translator component and change its locale before rendering the template. This solution does not require passing an extra value to the parameters' array of the render() method and painfully refactoring all your Twig files.
public function indexAction($name)
{
$translator = $this->get('translator');
// Save the current session locale
// before overwriting it. Suppose its 'en_US'
$sessionLocale = $translator->getLocale();
$translator->setLocale('nl_NL');
$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);
// Otherwise subsequent templates would also
// be rendered in Dutch instead of English
$translator->setLocale($sessionLocale);
return $this->render(...);
}
A common approach to user mailing is storing the user's locale in the User entity and passing it directly to the translator, such as in this code snippet:
$translator->setLocale($recipientUser->getLocale());
Here is a solution (it works well, except sub-templates (Twig: render(controller('AppBundle:Invoice/Index:productTotalPartial')))
<?php
namespace Main\BoBundle\Service;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Class LocaleSwitcher
*
* #package Main\BoBundle\Service
*/
class LocaleSwitcher
{
/**
* #var TranslatorInterface
*/
private $translator;
/**
* #var string
*/
private $previousLocale;
/**
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* Change the locale
*
* #param string $locale
*/
public function setLocale($locale)
{
$this->previousLocale = $this->translator->getLocale();
$this->translator->setLocale($locale);
$this->setPhpDefaultLocale($locale);
}
/**
* Revert the locale
*/
public function revertLocale()
{
if ($this->previousLocale === null) {
return;
}
$this->translator->setLocale($this->previousLocale);
$this->setPhpDefaultLocale($this->previousLocale);
$this->previousLocale = null;
}
/**
* Use temporary locale in closure function
*
* #param string $locale
* #param \Closure $c
*/
public function temporaryLocale($locale, \Closure $c)
{
$this->setLocale($locale);
$c();
$this->revertLocale();
}
/**
* Sets the default PHP locale.
* Copied from Symfony/Component/HttpFoundation/Request.php
*
* #param string $locale
*/
private function setPhpDefaultLocale($locale)
{
// if either the class Locale doesn't exist, or an exception is thrown when
// setting the default locale, the intl module is not installed, and
// the call can be ignored:
try {
if (class_exists('Locale', false)) {
/** #noinspection PhpUndefinedClassInspection */
\Locale::setDefault($locale);
}
} catch (\Exception $e) {
}
}
}
Example:
if ($this->getLocale()) {
$this->localeSwitcher->setLocale($this->getLocale());
}
$html = $this->templating->render($templatePath);
if ($this->getLocale()) {
$this->localeSwitcher->revertLocale();
}
u can do this: send a paramater (e.g. locale) to template
public function indexAction($name)
{
$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, 'locale' => 'nl_NL'),
)
)
;
$this->get('mailer')->send($message);
return $this->render(...);
}