Global and module config interaction - zend-framework2

Let's imagine I have global application configuration
return array(
'languages' => array(
'allowed' => array('de', 'en'),
),
);
And I have module configuration with the routes description. I need routes, based on global configuration, so I need to read global application config within the module to compose my routes according to languages->allowed values (constraints for the segment type route)
What is the best way to get global configuration values from the module configuration script? is it correct at all to manipulate data in configuration file instead of simple array return?

You should think a bit more ahead of your problem. You want to create a route structure based on your configuration. The configuration could come from everywhere: module config, local config and global config. It is therefore quite hard to base your module's config on a global one.
What you can do, is create the routes later. For example, you create in your module Foo the config like this:
'routes_foo' => array(
'bar' => array(
'type' => 'segment',
'options' => array(
'route' => ':locale/foo/bar',
'constraints' => array(
'locale' => '%LOCALE%',
),
),
),
),
And in your module class:
namespace Foo;
class Module
{
public function onBootstrap($e)
{
$app = $e->getApplication();
$sm = $app->getServiceManager();
$config = $sm->get('config');
$routes = $config['routes_foo');
$locales = $config['languages']['allowed'];
$routes = $this->replace($routes, array(
'%LOCALE%' => sprintf('(%s)', implode('|', $locales)
);
$router = $sm->get('router');
$router->routeFromArray($routes);
}
public function replace($array, $variables)
{
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$name] = $this->replace($value, $variables);
}
if (array_key_exists($value, $variables)) {
$array[$name] = $variables[$value];
}
}
return $array;
}
}
What happens is you grab the routes from your config (those are not automatically injected in the router). There you also load all languages from your global config. Then your "custom" routes have (at several places) a "magic" configuration key, which will be replaced by a regex constraint for the locales: (en|de). That parsed config is then injected into the router.

Related

Translating navigation breadcrumbs

I'm trying to translate the breadcrumbs sitting in the module/Application/config/module.config.php configuration file like:
'navigation' => array(
'default' => array(
array(
'label' => \Application\Util\Translator::translate('Home'),
'route' => 'home',
'resource' => 'route/home',
'pages' => array(
),
),
The breadcrumb Home should display as Accueil in french.
The translator works just fine for the rest of the application. But none of the breadcrumbs are translated. The language resource file has been verified and poedit-ed again and again.
In the very same configuration file, is the translator
configuration:
'service_manager' => array(
'factories' => array(
'account_navigation' => 'Application\Navigation\Service\AccountNavigationFactory',
'navigation' => 'Zend\Navigation\Service\DefaultNavigationFactory',
'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
'Application\Collector\RouteCollector' => 'Application\Service\RouteCollectorServiceFactory',
),
),
'translator' => array(
'locale' => 'fr_FR', // langue par défaut
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
'translation_files' => array(
array(
'type' => 'phpArray',
'filename' => __DIR__ . '/../../../vendor/zendframework/zendframework/resources/languages/fr/Zend_Validate.php',
'locale' => 'fr_FR'
),
array(
'type' => 'gettext',
'filename' => __DIR__ . '/../../../vendor/zf-commons/zfc-user/src/ZfcUser/language/fr_FR.mo',
'locale' => 'fr_FR'
),
array(
'type' => 'phpArray',
'filename' => __DIR__ . '/../language/fr_FR.php',
'locale' => 'fr_FR'
)
)
),
My application bootstrap looks like:
class Module
{
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$sm = $e->getParam('application')->getServiceManager();
// Add ACL information to the Navigation view helper
$authorize = $sm->get('BjyAuthorize\Service\Authorize');
$acl = $authorize->getAcl();
$role = $authorize->getIdentity();
\Zend\View\Helper\Navigation::setDefaultAcl($acl);
\Zend\View\Helper\Navigation::setDefaultRole($role);
$id = $sm->get('zfcuser_auth_service')->getIdentity();
if (! is_null($id)) {
$em = $sm->get('Doctrine\ORM\EntityManager');
$usr = $em->find('Application\Entity\User', $id->getId());
$blameableListener = new \Gedmo\Blameable\BlameableListener();
$blameableListener->setUserValue($usr);
$em->getEventManager()->addEventSubscriber($blameableListener);
} else {
// redirection
if (isset($_SERVER['SERVER_NAME']) && preg_match('/monxxxxx/', $_SERVER['SERVER_NAME'])) {
$strategy = new RedirectionStrategy();
$strategy->setRedirectRoute('driver/login');
$eventManager->attach($strategy);
}
}
if ($sm->has('MvcTranslator')) {
\Zend\Validator\AbstractValidator::setDefaultTranslator($sm->get('MvcTranslator'));
}
Locale::setDefault('fr_FR');
// custom layout
$e->getApplication()->getEventManager()->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController', 'dispatch', function($e) {
$route = $e->getRouteMatch();
$controller = $e->getTarget();
// change layout for login
if($route->getParam('controller') == 'zfcuser' && $route->getParam('action') == 'login'){
$controller->layout('layout/login');
}
}, 100);
// Log the exceptions
$application = $e->getApplication();
$sm = $application->getServiceManager();
$sharedManager = $application->getEventManager()->getSharedManager();
$sharedManager->attach('Zend\Mvc\Application', 'dispatch.error',
function($e) use ($sm) {
if ($e->getParam('exception') && strpos($e->getParam('exception'), 'are not authorized') === false) {
$sm->get('Zend\Log\Logger')->crit($e->getParam('exception'));
$toEmail = "xxxxx#xxxxx.com";
$toName = "Service IT";
$subject = "Exception error";
$body = $e->getParam('exception');
\Application\Util\Common::sendMail($toEmail, $toName, $subject, $body);
}
}
);
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__
)
)
);
}
public function init($moduleManager)
{
$moduleManager->loadModule('ZfcUser');
}
}
I'm on ZF2 2.2.5.
Might be a silly question, but do you have the PHP gettext module (verified via your phpinfo())? If yes, you could much easier use _('Translatable string') for strings needing translation. You'd have a .mo and .po file in a language folder (next to src, public, etc.).
(Asking the above because you seem to be putting a lot of effort into this, even though your code shows you use gettext as a translator, which should allow you to already translate static strings, such as home in a config file)
As an example the picture below, it is the basic setup for one of my own ZF2 vendor modules:
Next, you'd need Poedit (Free version should be plenty.).
NOTE: Make sure to always use Poedit to edit the .mo/.po files.
Use Poedit to open the files in the language folder, no need to "translate" strings that are the same string as the language they should be, unless you're using shortcuts for strings (ie. "index" string should display "Show overview", but please don't :p)
Within Poedit you have a few options to add additional strings that it searches for in your files and you can also add additional file extensions. I would suggest you have it look for these strings:
$this->translate()
_()
and these file extensions:
.php
.phtml
That way you've covered the basics. Of course your setup may be different, so modify as needed.
Lastly, you need to register the usage of gettext PHP extensions in you module config. You need to do this for every module, as you need to give a language folder path, as that is where your module 'should' be translated (though feel free to make it global and have a huge translation file if you must, not recommended).
Add the following bit to your module.config.php file:
'translator' => [
'locale' => 'en_US', // This line needs to be in the "root" config, feel free to override with/per module settings and/or user settings
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
],
],
],
With all of the above, translating your breadcrumbs should be done like so:
'navigation' => array(
'default' => array(
array(
'label' => _('Home'),
'route' => 'home',
'resource' => 'route/home',
'pages' => array(
),
),
It seems that namespace Zend\View\Helper\Navigation\Breadcrumbs; is not instanciated correctly in your service manager. Therefore, the translator will not work because it also provided by the service manager as a dependancy
What you should do is to verify if Zend\View\Helper\Navigation\Breadcrumbs::line#113 is actually parsed when you load your breadcrumbs. At least this one.
You can also check the method htmlify of AbstractHelper to test if the translator is enabled.

Basic use of TranslationServiceProvider in Silex

I'm trying to use the Silex TranslationServiceProvider in the most straighforward way i.e.
<?php
// web/index.php
require_once __DIR__.'/../vendor/autoload.php';
$app = new Silex\Application();
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
'locale' => 'fr',
'locale_fallbacks' => array('en')
));
$app['translator.domains'] = array(
'messages' => array(
'en' => array('message_1' => 'Hello!'),
'fr' => array('message_1' => 'Bonjour')
));
echo $app['translator']->trans('message_1');
// I get 'Hello!' (why ?)
It seems that the 'locale' => 'fr' line when initializing the TranslationServiceProvider is not taken into account and that the only parameter that counts is locale_fallbacks (when I change locale_fallbacks to 'fr', the message is displayed in french)
Is there something very simple I am missing here ?
Thanks in advance
Edit
When I use the the setLocale function, it still doesn't work and seems to override the locale_fallbacks:
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
'locale_fallbacks' => array('en')
));
$app['translator']->setLocale('fr');
echo $app['translator']->getLocale(); // returns 'fr' as expected
$app['translator.domains'] = array(
'messages' => array(
'en' => array('message_1' => 'Hello!'),
'fr' => array('message_1' => 'Bonjour')
));
echo $app['translator']->trans('message_1');
// now returns 'message_1' (??)
What's wrong with the way I use the provider ?
You must set the locale, otherwise the fallback is used:
$app['translator']->setLocale('fr');
I'm setting the locale in a $app->before() handler:
$app->before(function(Request $request) use ($app) {
// default language
$locale = 'en';
// quick and dirty ... try to detect the favorised language - to be improved!
if (!is_null($request->server->get('HTTP_ACCEPT_LANGUAGE'))) {
$langs = array();
// break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
$request->server->get('HTTP_ACCEPT_LANGUAGE'), $lang_parse);
if (count($lang_parse[1]) > 0) {
foreach ($lang_parse[1] as $lang) {
if (false === (strpos($lang, '-'))) {
// only the country sign like 'de'
$locale = strtolower($lang);
} else {
// perhaps something like 'de-DE'
$locale = strtolower(substr($lang, 0, strpos($lang, '-')));
}
break;
}
}
$app['translator']->setLocale($locale);
$app['monolog']->addDebug('Set locale to '.$locale);
}
});
Two remarks on Ralf’s answer:
Almost all that is done in the before() middleware in unnecessary, as the Request class provides a convenience method for determining the “best” language based on the “Accept-Language” header. So basically, nothing more than this is required:
$app->before(
function (Request $request) use ($app) {
$app['translator']->setLocale($request->getPreferredLanguage(['en', 'fr']));
}
);
Silex uses the “magic” variable “{_locale}” in a route’s definition to set the locale accordingly. This means you do neither need to declare the locale when instantiating TranslationServiceProvider, nor call setLocale(), but simply declare the route like this:
$app->get('/{_locale}/path', ...);
Now, Twig and $app['translator'] (inside the Closure or controller method) will automatically be set to the correct locale.

ZF2 - Add pages to navigation in the controller

In my project I have a navigation, which is created from an array in a config.php file using the default factory. I want to add subpages to the current pages in the controller.
class IndexController extends AbstractActionController {
public function newpageAction() {
$navigation = $this->getServiceLocator()->get('navigation');
$currentPage = $navigation->findById('index');
$options = array(
'id' => 'newpage',
'label' => 'New Page',
'route' => 'my-route',
'controller' => 'index',
'action' => 'newpage',
'active' => true,
);
$newpage = new \Zend\Navigation\Page\Mvc($options);
$currentPage->addPage($newpage);
}
}
The page is added successfully but then I try to create the url for the page in the breadcrumbs view using the getHref() method of the page:
<?php foreach($this->pages as $page) {?>
<li>
<?php echo $page->getLabel();?>
</li>
<?php }?>
But I get the following error for the newly added pages:
Additional information:
Zend\Navigation\Exception\DomainException
File:
\vendor\zendframework\zendframework\library\Zend\Navigation\Page\Mvc.php:198
Message:
Zend\Navigation\Page\Mvc::getHref cannot execute as no Zend\Mvc\Router\RouteStackInterface instance is composed
I guess the problem is in the way I create and add the pages to the navigation. Is there another way to do that or how I fix this error?
I want to add the pages after the 3th level in the controller instead of in the config file because there are params in the urls of the pages and the labels are dynamic.
Any suggestions for accomplishing this task in any other way are welcome.
You could add the default router.
\Zend\Navigation\Page\Mvc::setDefaultRouter ($this->getServiceLocator ()->get ('router'));
The error is due to the MVC page having unmet dependencies (the router). It is the factory's job to inject these components (depending on a URI or MVC type).
To make sure each MVC page has router injected create a new factory that in turn uses another already provided factory Zend\Navigation\Service\ConstructedNavigationFactory to create your own navigation container and return it's pages. In your example this will be just one page.
EDIT
If you have to add the navigation pages in the controller, where you do not know the page config prior to the newpageAction(); You could extend the class to allow config to be set within the controller.
For example
public function MyCustomNavFactory extends ConstructedNavigationFactory
{
// make the config optional
public function __construct($config = array())
{
$this->config = $config;
}
// Allow config to be set outside the class
public function setConfig($config)
{
$this->config = $config;
}
}
Module.php
// Module
public function getServiceConfig() {
return array(
'invokables' => array(
// Create the factory as an invokable (as there are no __construct args)
'MyCustomNavFactory' => 'App\Navigation\Service\MyCustomNavFactory'
),
);
}
The controller call would then just be simply just use
// Controller
public function newpageAction()
{
$serviceManager = $this->getServiceLocator();
$navigation = $serviceManager->get('MyCustomNavFactory');
$options = array(
'id' => 'newpage',
'label' => 'New Page',
'route' => 'my-route',
'controller' => 'index',
'action' => 'newpage',
'active' => true,
);
$navigation->setConfig($options);
$pages = $navigation->getPages($serviceManager);
}
The answer of #AlexP is correct.
But there error into Controller Action As when he call custom factory using ServiceLocator will get Object of type AbstractContainer Object Because ServiceLocator will call createService method into your custom factory (MyCustomNavFactory) which extends AbstractNavigationFactory So the next line will call setConfig method into AbstractContainer Object not into your custom factory (MyCustomNavFactory).
The correct Way to Set Breadcrumb configuration from Controller Action is:
// Controller
public function newpageAction()
{
$serviceManager = $this->getServiceLocator();
$navigationFactory = new MyCustomNavFactory();
$options = array(
'id' => 'newpage',
'label' => 'New Page',
'route' => 'my-route',
'controller' => 'index',
'action' => 'newpage',
'active' => true,
);
$navigationFactory->setConfig($options);
$pages = $navigationFactory->getPages($serviceManager);
}
OR
Remove setConfig method form custom factory and set configuration using it's Constructor
// custom Factory
public function MyCustomNavFactory extends ConstructedNavigationFactory
{
// make the config optional
public function __construct($config = array())
{
parent::__construct($config);
}
}
Then Controller will be:
// Controller
public function newpageAction()
{
$serviceManager = $this->getServiceLocator();
$options = array(
array(
'id' => 'newpage',
'label' => 'New Page',
'route' => 'my-route',
'controller' => 'index',
'action' => 'newpage',
'active' => true,
)
);
$navigationFactory = new MyCustomNavFactory($options);
$pages = $navigationFactory->getPages($serviceManager);
}

ZF2: Controller's Forward plugin doesn't work. How to make it work?

I need to forward the ajax request to the other Action method of current controller. I use the Forward plugin but it doesn't work. There is an example in the manual about how to use the Forward Plugin:
$foo = $this->forward()->dispatch('foo', array('action' => 'process'));
return array(
'somekey' => $somevalue,
'foo' => $foo,
);
My code:
// From Ajax on the page. I apply to the indexAction of FooController,
// I use RegEx route
xhr.open('get', '/fooindex', true);
// My Controller
namespace Foo\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
// I extend the AbstractActionController, the manual says it's important for the Forward Plugin to work
class FooController extends AbstractActionController {
// This is the action I send my request from Ajax
public function indexAction() {
// if the request if Ajax request I forward the run to the nextAction method
if ($this->getRequest()->isXmlHttpRequest()) {
// I do as manual says
$rs = $this->forward()->dispatch('FooController', array('action' => 'next'));
}
}
public function nextAction() {
// And I just want to stop here to see that the Forward Plugin works
// But control doesn't reach here
exit('nextAction');
}
}
The error I get in the Console is:
GET http://test.localhost/fooindex 500 (Internal Server Error)
If I do not use Forward everything works fine, the request comes to the indexAction just fine. Only Forward throws an error.
From the manual, about The Forward Plugin:
For the Forward plugin to work, the controller calling it must be
ServiceLocatorAware; otherwise, the plugin will be unable to retrieve
a configured and injected instance of the requested controller.
From the manual, about Available Controllers:
Implementing each of the above interfaces is a lesson in redundancy;
you won’t often want to do it. As such, we’ve developed two abstract,
base controllers you can extend to get started.
AbstractActionController implements each of the following interfaces:
Zend\Stdlib\DispatchableInterface
Zend\Mvc\InjectApplicationEventInterface
Zend\ServiceManager\ServiceLocatorAwareInterface
Zend\EventManager\EventManagerAwareInterface
So my FooController extends AbstractActionController, which implements ServiceLocatorAwareInterface, so the Forward has to work, but it doesn't. What did I miss? How to make it work?
You should remember that the dispatch plugin gets the controller to dispatch to from the service manager by name. You should therefore use the correct name and not just the classname.
Look in your configuration for the controllers.invokables array. That should contain which name of the service maps to what FQCN.
It might be you name IS FooController, then forget what I just said
You should use fully qualified name when calling the controller, so 'FooController' should be namespaced as well.
Also, you should add the controller in the list of the invokables in the module config files, for example:
return array(
'controllers' => array(
'invokables' => array(
'FooController' => 'Namespace/Controller/FooController'
...
),
)
)
try this:
class FooController extends AbstractActionController {
public function indexAction() {
return $this->forward()->dispatch('Bar\Controller\Bar',
array(
'action' => 'process',
'somekey' => $somevalue,
));
}
}
here invokable is: 'Bar\Controller\Bar' => 'Bar\Controller\Bar'
try this:
class FooController extends AbstractActionController {
public function indexAction() {
return $this->forward()->dispatch('Foo',
array(
'action' => 'process',
'somekey' => $somevalue,
));
}
}
Your module.config.php file is like this:
'controllers' => array(
'invokables' => array(
'Foo' => 'Foo\Controller\FooController', // <----- Module Controller
),
),
'router' => array(
'routes' => array(
'foo' => array(
'type' => 'segment',
'options' => array(
'route' => '/foo[/:action][/:id]', // <---- url format module/action/id
'constraints' => array(
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
'id' => '[0-9]+',
),
'defaults' => array(
'controller' => 'Foo', // <--- Defined as the module controller
'action' => 'index', // <---- Default action
),
),
),
),
),

Where to put custom settings in Zend Framework 2?

I have some custom application specific settings, I want to put in a configuration file. Where would I put these? I considered /config/autoload/global.php and/or local.php. But I'm not sure which key(s) I should use in the config array to be sure not to override any system settings.
I was thinking of something like this (e.g. in global.php):
return array(
'settings' => array(
'settingA' => 'foo',
'settingB' => 'bar',
),
);
Is that an agreeable way? If so, how can I access the settings e.g. from within a controller?
Tips are highly appreciated.
In case you need to create custom config file for specific module, you can create additional config file in module/CustomModule/config folder, something like this:
module.config.php
module.customconfig.php
This is content of your module.customconfig.php file:
return array(
'settings' => array(
'settingA' => 'foo',
'settingB' => 'bar',
),
);
Then you need to change getConfig() method in CustomModule/module.php file:
public function getConfig() {
$config = array();
$configFiles = array(
include __DIR__ . '/config/module.config.php',
include __DIR__ . '/config/module.customconfig.php',
);
foreach ($configFiles as $file) {
$config = \Zend\Stdlib\ArrayUtils::merge($config, $file);
}
return $config;
}
Then you can use custom settings in controller:
$config = $this->getServiceLocator()->get('config');
$settings = $config["settings"];
it is work for me and hope it help you.
You use your module.config.php
return array(
'foo' => array(
'bar' => 'baz'
)
//all default ZF Stuff
);
Inside your *Controller.php you'd call your settings via
$config = $this->getServiceLocator()->get('config');
$config['foo'];
It's as simple as that :)
You can use any option from the following.
Option 1
Create one file called config/autoload/custom.global.php. In custom.global.php
return array(
'settings' => array(
'settingA' => 'foo',
'settingB' => 'bar'
)
)
And in controller,
$config = $this->getServiceLocator()->get('Config');
echo $config['settings']['settingA'];
Option 2
In config\autoload\global.php or config\autoload\local.php
return array(
// Predefined settings if any
'customsetting' => array(
'settings' => array(
'settingA' => 'foo',
'settingB' => 'bar'
)
)
)
And in controller,
$config = $this->getServiceLocator()->get('Config');
echo $config['customsetting']['settings']['settingA'];
Option 3
In module.config.php
return array(
'settings' => array(
'settingA' => 'foo',
'settingB' => 'bar'
)
)
And in controller,
$config = $this->getServiceLocator()->get('Config');
echo $config['settings']['settingA'];
If you look in config/application.config.php it says:
'config_glob_paths' => array(
'config/autoload/{,*.}{global,local}.php',
),
So ZF2 by default will autoload configuration files from config/autoload/ - so for example you could have myapplication.global.php it would get picked up and added into the configuration.
Evan.pro wrote a blog post that touches on this: https://web.archive.org/web/20140531023328/http://blog.evan.pro/environment-specific-configuration-in-zend-framework-2

Resources