I'm trying to write a test for a module that requires a specific configuration change during a test run, overwriting the default configuration provided by the module's own module.config.php.
In a normal application run, configuration is merged in order and I can use config/autoload/*.local.php to supersede module's config. But during a phpunit run those files aren't loaded, and I can't find a way to modify it during testing runtime.
I tried modifying the config in my Bootstrap file, to no avail; and even in the test controller setup directly:
public function setUp()
{
$app_config = include(Bootstrap::getRootPath() . '/config/application.config.php');
$test_config = Bootstrap::getTestConfig();
$new_config = ArrayUtils::merge($app_config, $test_config);
$this->setApplicationConfig($new_config);
}
But by the time one of the factories is running I check the values for the config during a debug session, and my injected values are nowhere to be found.
Also tried hooking to EVENT_MERGE_CONFIG and modify it there:
public static function onMergeConfig(ModuleEvent $e)
{
$configListener = $e->getConfigListener();
$config = $configListener->getMergedConfig(false);
$new_config = ArrayUtils::merge($config, $this->getTestConfig(static::$zf2ModulePaths));
// Pass the changed configuration back to the listener:
$configListener->setMergedConfig($new_config);
}
What's the proper way to accomplish this?
Personally I go with a similar approach as zfcampus/zf-development-mode does. Depending on the environment, I add another config_glob_paths to the application configuration. While I use it for managing development related configuration, this can easily be adapted for testing.
The basic idea is to have two application configuration files, one general and one environment specific (expect for production).
application.config.php:
return [
'modules' => [],
'module_listener_options' => [
'config_glob_paths' => [
__DIR__ . '/autoload/{{,*.}global,{,*.}local}.php',
],
'config_cache_enabled' => false,
'module_map_cache_enabled' => false,
],
];
development.config.php:
return [
'modules' => [],
'module_listener_options' => [
'config_glob_paths' => [
'config/autoload/{,*.}{global,local}-development.php',
],
'config_cache_enabled' => false,
'module_map_cache_enabled' => false,
],
];
Merging those two, will autoload the files matching in this order:
'config_glob_paths' => [
__DIR__ . '/autoload/{{,*.}global,{,*.}local}.php',
'config/autoload/{,*.}{global,local}-development.php',
],
While the later ones (postfixed with -development) will overwrite settings of the default ones.
So in my autoload directory I have the files like:
database.global.php
database.global-development.php
Local files are still possible for each environment as well of course:
database.local.php
database.local-development.php
The load order on development for the files then would be (if existing):
database.global.php
database.local.php
database.global-development.php
database.local-development.php
On a production environment, the development.config.php file is not merged, hence the *-development.php files are not loaded.
This pattern can easily be changed to testing environments as well of course. It is also noteworth that by this approach, you can change other application settings as well (e.g. application config caching / modules to load).
Related
I am developing several packages and would like to have a single config file for all of them if they are to be published.
Inside my service provider I did this:
public function boot()
{
$this->publishes([
__DIR__ . '/config/custom.php' => config_path('custom.php'),
]);
}
public function register()
{
$this->mergeConfigFrom(
__DIR__ . '/config/custom.php', 'custom'
);
}
Config:
return [
'containers' => [
...
]
];
And surely enough, if I publish it, it creates the file with values inside. But if a file already exists, having different keys:
return [
'xxxyyy' => [
...
],
];
publishing doesn't do anything. I would expect it to look like:
return [
'xxxyyy' => [
...
],
'containers' => [
...
]
];
What am I doing wrong?
In case anyone else is baffled by this, I have tested several cases and here is my explanation (official docs).
$this->publishes part enables the package config to be published by vendor:publish command. Nothing to explain here.
$this->mergeConfigFrom part enables the package config to be merged with currently published copy. Merge in this case means merged from Laravel config() scope.
This means that going to artisan tinker and running config('key') (where key is name of your config file) will return nothing if there is no published file and you don't have mergeConfigFrom. If a file is published and has other key value pairs which are not present in your package config and you have mergeConfigFrom present, config('key') will return merged array of values from a published file together with values from your config.
If a file with a same config name exists in your root config folder, and your package internally uses the same config name but you don't have mergeConfigFrom, config('key') will return only contents of a file inside root config folder, and will ignore everything from your package as you didn't provide a way for Laravel to see this as a "global" configuration. Your package will keep internally using the package config, but from app scope you will not be able to fetch it.
I have Zend Framework 3 Application with working translator using po files.
I have configured it like this in my \config\global.php file:
'translator' => [
'locale' => 'en_US',
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => getcwd() . '/data/language/',
'pattern' => '/%s/general.mo',
],
],
],
When i change the value of the "locale" it works ok and finds the proper .po file.
I need to be able to set the locale depending on a user profile's value saved in the database.
I have checked the documentation from here http://zendframework.github.io/zend-i18n/translation/ and the tutorial from here https://docs.zendframework.com/tutorials/i18n/ but they just mention the setLocale() method with no explanation or example. There is similar thread here Zend framework 2 : How to set locale globaly? but it's for ZF2 and it doesn't provide working solution just some suggestions.
To summarize my question - how and where should i use the setLocale() method so it would be effective in the whole application and $this->translate($message) in all view files will use the new locale instead the default one used in the configuration file?
You just need to set the PHP locale. To do so, use \Locale::setDefault('en-GB');.
Have a look at SlmLocale, this specific file is where it's done.
While that was the easiest way, you could also use the setLocale function on the MvcTranslator I guess. For that, you would need to override the existing factory with your own factory, therefore decorating the original one.
If you look at the ConfigProvider file in zend-mvc-i18n, you can see that aliases and factories are used here to create the MVC translator. Then you can see how the factory in question works, it basically creates a decorate translator, as stated in the doc.
By default, the service manager always provide the same instance (shared service), just like a singleton.
What we will therefore do is override this configuration (ie. make sure your own module is after the Zend\Mvc\I18n in modules.config.php). Then, in the module configuration, we can provide our own translator.
Our translator basically consist of the translator from the documentation, on which the setLocale is called. In order to do so, we can use a delegator.
return [
'factories' => [
TranslatorInterface::class => TranslatorServiceFactory::class,
],
'delegators' => [
TranslatorInterface::class => [
\Application\Factory\TranslatorFactory::class,
],
],
];
And then the TranslatorFactory:
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
class TranslatorFactory implements DelegatorFactoryInterface
{
public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
{
$translator = call_user_func($callback);
$translator->setLocale('en-GB');
return $translator;
}
}
That would be one way to do it (you get the container in that factory, so you could get some user data probably).
Another solution is to use the event system, and only declare the locale in the event listener where you retrieve your user details.
I can't put the library under vendor/ because that directory is ignored in .gitignore. I put it under bing-ads/ and I have
<?php
namespace app\models;
include 'bing-ads\v10\bingads\ClientProxy.php';
use \Yii;
use BingAds\Proxy\ClientProxy;
to access it. It works for console commands/actions, but I have a feeling it will not work during a web call because the root directory will be web/. Where should I put this library and how can I access it via both console actions and web actions?
The Microsoft PHP library is located here.
I found a way by adding the file paths to the autoload section of composer.json. I remembered that I had to do that for some of the other libraries as well, even the ones available via Composer.
"autoload": {
"classmap": [
"vendor/googleads/googleads-php-lib/src/Google/Api/Ads/Common/Util",
"vendor/googleads/googleads-php-lib/src/Google/Api/Ads/AdWords/Util/v201605",
"bing-ads/v9/bingads/CustomerManagementClasses.php",
"bing-ads/v10/bingads/v10/CampaignManagementClasses.php",
"bing-ads/v10/bingads/v10/BulkClasses.php",
"bing-ads/v10/bingads/ClientProxy.php"
]
}
Then I ran
$ composer install
...
Generating autoload files
I'm not sure this is the best way though.
You could store them wherever you want e.g. in a folder named "BingAds".
Just add the alias as example in a common base config file /common/config/base.php which is included in your console as well as in your web application e.g. for your /web/index.php
$config = \yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../common/config/base.php'),
require(__DIR__ . '/../common/config/web.php'),
require(__DIR__ . '/../config/base.php'),
require(__DIR__ . '/../config/web.php')
);
Inside this common/config/base.php you could add your settings, extensions etc. which are valid for both the console and your web application
<?php
$config = [
...
'aliases' => [
'#BingAds' => '#app/BingAds/v10',
'#BingAds/Proxy' => '#app/BingAds/v10/bingads',
],
];
Your Clientproxy.php is stored as example in the directory /BingAds/Proxy/.
Then you don't have to include your files every time you want to use them and just write.
use BingAds\Proxy\ClientProxy;
I have unpacked your linked zip file and stored the files of the directory Bing Ads API in PHP\PHP\Bing Ads API in PHP to my application root directory BingAds whith the aliases mentioned above.
I have tested it by creating a clientProxy object in both a console and web application.
$test = new ClientProxy('test');
var_dump($test);
Both Printed out
object(BingAds\Proxy\ClientProxy)[140]
private 'authenticationToken' => null
private 'username' => null
private 'password' => null
private 'developerToken' => null
private 'wsdlUrl' => string 'test' (length=4)
private 'accountId' => null
private 'customerId' => null
private 'service' => null
private 'namespace' => null
I haven`t tested out other classes but I guess you get how it works.
Add this to your composer.json:
{
"repositories": [
{
"type": "package",
"package": {
"name": "microsoft/bing-ads",
"version": "9.0.0",
"dist": {
"url": "https://code.msdn.microsoft.com/Bing-Ads-API-Version-9-in-fb27761f/file/159208/2/Bing%20Ads%20API%20in%20PHP.zip",
"type": "zip"
},
"autoload": {
"classmap": [
"PHP/Bing Ads API in PHP/v10/bingads/"
]
}
}
}
],
"require": {
"microsoft/bing-ads": "9.0.0"
}
}
Then add composer generated autload.php file if you haven't already.
Then you can call the BingAds\Proxy\ClientProxy() without includes.
There's no better way:)
It's best to place external library to extensions directory.
And in config do
return [
'import' => [
'application.extensions.bing-ads.v10.bingads.ClientProxy',
],
...
];
in main and in console configs.
I usually just put the code in a components folder. If you're using the basic template, this folder can be under your application root; if you're using the advanced template, this folder can be under the necessary app: frontend, backend, console, or common. I recommend putting it under common for reasons I shall explain later
Under every app config, Yii 2 uses the ::setAlias method to assign different aliases to the key folders. In the basic app template, #app refers to the application root. While in the advanced template #app may refer to any of backend, frontend, or console.
If your library code is under backend, you can access it like so
namespace backend\controllers;
use yii\web\Controller;
use backend\components\MyLibCode;
class SiteController extends Controller
{
public function actionIndex()
{
$mlb = new MyLibCode();
// ...
}
}
However, it is good Yii 2 practice to have common library code in the common folder. So if this library code is to be used across apps, I suggest you put it into the common\components folder and replace backend with common in the use statement in the code above.
p.s: Justinas method also works; it's borrowed from Yii 1. But this may become cumbersome because this asks Yii to load the class when the application starts. If there is some heavy-lifting in that file, it may be detrimental to the app's performance.
Right now I'm trying to implement themming for my Yii2 based project.
How I see the thing now:
User chooses an application theme from the list on the settings
page in backend.
Using yii2-settings I'm saving all the
configuration data in DB (pretty easy).
In the application
bootstrap.php I'm creating new alias called #theme. Basically it
should lead us to a application theme base path (used in search
paths, assets manager, e.t.c.).
According to official
documentation, that's how I configured my view component:
'view' => [
'theme' => [
'basePath' => '#theme',
'baseUrl' => '#theme',
'pathMap' => [
'#app/views' => '#theme',
'#app/widgets' => '#theme/widgets',
'#app/modules' => '#theme/modules',
],
],
],
An issue I have is with p.3. According to yii2-settings documentation that's how I supposed to read the settings:
$theme = Yii::$app->settings->get('name', 'general');
Yii::setAlias('#theme', realpath(dirname(__FILE__)."/../../themes/$theme"));
But obviously, it's not working for me because of yii2-settings component didn't initialized yet when bootstrap.php is called. I've been trying to initialize it later in the init() method of my base controller, then adjust other aliases manually, but I feel that way being somewhat 'unclean', and also it still fails because of #theme alias is also used in asset file which is Yii2 starting to publish before calling the controller's init method.
So does anyone has any thoughts of how to do that 'hacking' the code as less as possible? I know I could just move configuration to some file, then read it manually before the application initialization, but it's still not the way I want to go.
Maybe there's some way to override some system component to set the alias after db component is loaded, but before view component configuration? Or Yii loads this components in a different order? Anyway. Any help would be appreciated!
You could try an Application Event in bootstrap:
\Yii::$app->on(\yii\base\Application::EVENT_BEFORE_REQUEST, function ($event) {
$theme = Yii::$app->settings->get('name', 'general');
Yii::setAlias('#theme', realpath(dirname(__FILE__)."/../../themes/$theme"));
});
OR in configuration file:
[
'on beforeRequest' => function ($event) {
// ...
},
]
From Yii 2 docs:
EVENT_BEFORE_REQUEST This event is triggered before an application
handles a request. The actual event name is beforeRequest.
When this event is triggered, the application instance has been
configured and initialized. So it is a good place to insert your
custom code via the event mechanism to intercept the request handling
process. For example, in the event handler, you may dynamically set
the yii\base\Application::$language property based on some parameters.
Here's the final solution:
config/bootstrap.php:
// Setting a temporary path for components configuration - will be changed later
Yii::setAlias('#theme', realpath(dirname(__FILE__)."/../../themes/"));
config/main.php
'components' => [
'view' => [
'theme' => [
'basePath' => '#theme',
'baseUrl' => '#theme',
'pathMap' => [
'#app/views' => '#theme',
'#app/widgets' => '#theme/widgets',
'#app/modules' => '#theme/modules',
],
],
],
],
'on beforeRequest' => function ($event) {
$theme = Yii::$app->settings->get('theme', 'general');
Yii::setAlias('#theme', realpath(dirname(__FILE__)."/../../themes/$theme"));
},
I am using Yii2 and have been reading about theming and theme inheritance; however have some questions:
Consider the following example:
'view' => [
'theme' => [
'pathMap' => [
'#app/views' => [
'#app/themes/current',
'#app/themes/default',
],
],
'baseUrl' => '#web/themes/current',
'basePath' => '#webroot/themes/current',
],
],
Now, imagine we request the theme file foo; as I understand it this will first be looked for in the following order:
#app/themes/current/foo.php
#app/themes/default/foo.php
#app/views/foo.php
Now imagine foo.php isn't found in the #app/themes/current/ theme, so it would use the file found in #app/themes/default/.
Now, considering the baseUrl and basePath settings I am a little confused how these are used in these situations.
Now, imagine foo.php references an image file inside the file, wouldn't this still attempt to find #web/themes/current/images/myImage.jpg rather than #web/themes/default/images/myImage.jpg?
In this case, basePath is worthless. Because basePath is only applied when
pathMap is empty.
basePath is not a fallback, It is a shortcut of pathMap, quick use when you only have one theme.
'pathMap' => [
'#app/views' => [
'#app/themes/current/views',
],
],
Equivalent to:
'basePath' => '#app/themes/current',
(Yii understands that folder #app/themes/current/views exist)
baseUrl: It is returned when you call $this->theme->getBaseUrl() in view file. Less worth with multi theme.
About fallback for static files. Theming fallback is not designed for this purpose.
Point your links inside file like this exmaple from docs:
$theme = $this->theme;
// returns: $theme->baseUrl . '/img/logo.gif'
$url = $theme->getUrl('img/logo.gif');
// returns: $theme->basePath . '/img/logo.gif'
$file = $theme->getPath('img/logo.gif');
It will get file from current theme directory.