Yii2: How to force using fallback MessageFormatter method? - php

My website is with a hosting provider that has the MessageFormatter class available on the server (Linux, PHP 7.0.27) but it is an old ICU version (4.2.1) that doesn't support my message {number,plural,=0{# available} =1{# available} other{# available}} and gives the error:
Message pattern is invalid: Constructor failed
msgfmt_create: message formatter creation failed: U_ILLEGAL_CHARACTER
...because of the =1 and =2 notation.
I'm not able to make changes to the server so how can I force using the fallback method provided by Yii2 which works just fine?

There is this hacky way you can try.
Copy the yii\i18n\MessageFormatter code to a new file. Name it MessageFormatter.php and place somewhere in your application (but not in vendor folder).
In this new file change the format() method to:
public function format($pattern, $params, $language)
{
$this->_errorCode = 0;
$this->_errorMessage = '';
if ($params === []) {
return $pattern;
}
return $this->fallbackFormat($pattern, $params, $language);
}
Don't change anything else (including namespace).
Now let's use Yii mapping.
Find a place in your application when you can put code that will be run every time in bootstrapping phase. Good place for this is common/config/bootstrap.php if you are using "Advanced Template"-like project.
Add there this line:
Yii::$classMap['yii\i18n\MessageFormatter'] = 'path/to/your/MessageFormatter.php';
Obviously change the path to the one you've chosen. Now Yii autoloader will load this class from your file instead of the original Yii vendor folder (as mentioned in Class Autoloading section of the Guide).
In the modified file MessageFormatter method presence of intl library is never checked so fallback is used as default.
The downside of this trick is that you need to update manually your file every time original Yii file is changed (so almost every time you upgrade Yii version).
Another approach is to configure I18N component in your application to use your custom MessageFormatter where you can extend the original file and just override format() method inside without modifying class map.

Related

Yii2 $model->_attributes assignment does not work in new version

I inherited a project that was created with Yii2, ver. 2.0.4, with the task to update said project to a more current version of Yii2 (2.0.15) because of the incompatibility of the older one with PHP 7.2+.
I noticed that there is a lot of use of assigning arrays to a model:
$model->_attributes = $array;
With the new version this results in an exception
'yii\base\UnknownPropertyException' with message 'Setting unknown property: app\models\model::_attributes'
For the time being I created a workaround with the following function:
function customSetAttributes(&$model, $array) {
foreach($model->attributeLabels() as $model_key => $model_label) {
if(!isset($array[$model_key])) continue;
$model->$model_key = $array[$model_key];
}
}
Also, the getter function now has a similar issue.
What I would like to know:
Was this type of assignment never intended in the first place (and I just haven't found the previous developer's code that enables it)? I skimmed over the Yii2 changelog but didn't notice anything related.
Is there a way to "salvage" the previous behaviour so I don't have to replace each occurence with my workaround function?
ActiveRecord::$_attributes was always private and never should be used in this way. I guess that previous developer edited framework core files in vendor directory and make this property protected/public.
You may try to emulate this behavior by creating virtual attribute using getter and setter:
public function get_attributes() {
return $this->getAttributes();
}
public function set_attributes($values) {
$this->setAttributes($values, false);
}
But this will not always work and it is more like an ugly hack to make crappy code work. I strongly suggest to fix code to use setAttributes() instead of _attributes.
Also you should compare yii2 package from vendor directory with source from https://github.com/yiisoft/yii2-framework/releases/tag/2.0.4 - you may find more places where core was edited.

CodeIgniter - Loading an application package prevents loading of view and dealing with collisions

I've been learning CodeIgniter and was just experimenting with adding Application Packages.
In the default install I've added a package path to the third_party folder that contains a single view, and then I want it to continue loading the default welcome_message. Separately this all works fine, but together the welcome_message view file can't be found apparently. Reading on in the docs at http://ellislab.com/codeigniter/user-guide/libraries/loader.html it mentions view collisions, and talks about setting the second parameter. Okay no problem, there isn't another view named welcome_message, but I do what then mention according to the example provided, which sets it to FALSE to get the welcome_message to display, but that doesn't work.
In fact I have to set it to TRUE to get it to work, which is the exact opposite of the docs. Can someone explain this logic reversal? and regarding view naming collisions due to a lack of a description in the docs does this mean setting the second param to whichever boolean prevents collisions and allow full use of all views regardless of naming? Or does it simply throw an error instead of loading the improper view?
class Welcome extends CI_Controller {
public function index()
{
$this->load->add_package_path(APPPATH . 'third_party/foo_bar/', FALSE);
$this->load->view('foo_bar');
$this->load->view('welcome_message'); // throws err on FALSE and loads on TRUE in add_package_path() call
}
}
After $this->load->view('foo_bar');
reset the path using $this->load->remove_package_path();
When you use add_package_path CI will check that folder for all path requests. This is fine when you are working with a self contained app. When you are done with that and want to access the "regular" CI paths for views etc, you need to remove the package path first.

Symfony 2 - how to parse %parameter% in my own Yaml file loader?

I have a Yaml loader that loads additional config items for a "profile" (where one application can use different profiles, e.g. for different local editions of the same site).
My loader is very simple:
# YamlProfileLoader.php
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;
class YamlProfileLoader extends FileLoader
{
public function load($resource, $type = null)
{
$configValues = Yaml::parse($resource);
return $configValues;
}
public function supports($resource, $type = null)
{
return is_string($resource) && 'yml' === pathinfo(
$resource,
PATHINFO_EXTENSION
);
}
}
The loader is used more or less like this (simplified a bit, because there is caching too):
$loaderResolver = new LoaderResolver(array(new YamlProfileLoader($locator)));
$delegatingLoader = new DelegatingLoader($loaderResolver);
foreach ($yamlProfileFiles as $yamlProfileFile) {
$profileName = basename($yamlProfileFile, '.yml');
$profiles[$profileName] = $delegatingLoader->load($yamlProfileFile);
}
So is the Yaml file it's parsing:
# profiles/germany.yml
locale: de_DE
hostname: %profiles.germany.host_name%
At the moment, the resulting array contains literally '%profiles.germany.host_name%' for the 'hostname' array key.
So, how can I parse the % parameters to get the actual parameter values?
I've been trawling through the Symfony 2 code and docs (and this SO question and can't find where this is done within the framework itself. I could probably write my own parameter parser - get the parameters from the kernel, search for the %foo% strings and look-up/replace... but if there's a component ready to be used, I prefer to use this.
To give a bit more background, why I can't just include it into the main config.yml: I want to be able to load app/config/profiles/*.yml, where * is the profile name, and I am using my own Loader to accomplish this. If there's a way to wildcard import config files, then that might also work for me.
Note: currently using 2.4 but just about ready to upgrade to 2.5 if that helps.
I've been trawling through the Symfony 2 code and docs (and this SO question and can't find where this is done within the framework itself.
Symfony's dependency injection component uses a compiler pass to resolve parameter references during the optimisation phase.
The Compiler gets the registered compiler passes from its PassConfig instance. This class configures a few compiler passes by default, which includes the ResolveParameterPlaceHoldersPass.
During container compilation, the ResolveParameterPlaceHoldersPass uses the Container's ParameterBag to resolve strings containing %parameters%. The compiler pass then sets that resolved value back into the container.
So, how can I parse the % parameters to get the actual parameter values?
You'd need access to the container in your ProfileLoader (or wherever you see fit). Using the container, you can recursively iterate over your parsed yaml config and pass values to the container's parameter bag to be resolved via the resolveValue() method.
Seems to me like perhaps a cleaner approach would be for you to implement this in your bundle configuration. That way your config will be validated against a defined structure, which can catch configuration errors early. See the docs on bundle configuration for more information (that link is for v2.7, but hopefully will apply to your version also).
I realise this is an old question, but I have spent quite a while figuring this out for my own projects, so I'm posting the answer here for future reference.
I tried a lot of options to resolve %parameter% to parameters.yml but no luck at all. All I can think of is parsing %parameter% and fetch it from container, no innovation yet.
On the other hand I don't have enough information about your environment to see the big picture but I just come up with another idea. It can be quite handy if you declare your profiles in your parameters.yml file and load it as an array in your controller or service via container.
app/config/parameters.yml
parameters:
profiles:
germany:
locale: de_DE
host_name: http://de.example.com
uk:
locale: en_EN
host_name: http://uk.example.com
turkey:
locale: tr_TR
host_name: http://tr.example.com
You can have all your profiles as an array in your controller.
<?php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
$profiles = $this->container->getParameter('profiles');
var_dump($profiles);
return $this->render('AcmeDemoBundle:Default:index.html.twig');
}
}
With this approach
you don't have to code a custom YamlLoader
you don't have to worry about importing parameters into other yml files
you can have your profiles as an array anytime you have the $container in your hand
you don't have to load/cache profile files one by one
you don't have to find a wildcard file loading solution
If I got your question correctly, this approach can help you.

Symfony2 - something other than bundle

I'm currenly working on the project where i need something orther than bundle. Something i call "Module".
It should be different from the bundle in that when project is starting system doesn't know which "Modules" will be used
and how they will be configured.
Also i'm going to use these modules similar to bundles
$response = $this->forward('AcmeHelloModule:Hello:fancy');
OR
$response = $this->forward('Acme/Hello:Hello:fancy');
Here HelloController->fancyAction(); would be executed. And this controller described say in file /src/modules/Acme/Hello/Controller/HelloController.php
So the question is how to implement this ?
a solution would be to implement a PluginBundle that can dynamicly install, load and run your so called modules.
the PluginBundle would not contain specific plugin code at all, but the runtime environment for you modules/plugins. you may then save in the database which plugins/modules are enabled and load them dynamicly at runtime.
with this sollution it should be possible to create a dynamic plugin mechanism as in wordpress. modifying the AppKernel at runtime is not a good solution because you'd also have to clear the cache when en- disabeling bundles.
In AppKernel add the following method:
public function getBundle($name, $first = true)
{
if (substr($name, -6) == 'Module')) {
return $this->getBundle('ModuleBundle')->getModule($name, $first);
}
return parent::getBundle($name, $first);
}
and all the logic runs in ModuleBundle.
But make sure the type of response is the same as Kernel->getBundle();

redeclare variable, or effectivly reload class at runtime in php

I need to be able to "effectivly" redeclare my class, so that during runtime, whilst my PHP IRC bot is running I can reload modules as the code base changes, which requires getting the class, but PHP won't allow it to be redeclared, nor is there a way to undeclare a class.
Is there a way I can acheive this? Perhaps some other method? I've tried "runkit", but that failed.
My current code is here:
http://pastie.org/private/ptj7c0t0teh3nnzn7ehcg
So to clarify, I effecivly need to be able to reload my class, instatiate it and put into a property in my main bot class once code has changed in said module, whilst the bot is running (run-time).
A brief look at your application leads me to believe your best course of action is to use a base class as suggested in the comment above.
Something to the extent of:
class IRCModule {
private static $module_dir = ''; //put your module directory here w/o trailing slash
public static function getModule( $module ) {
//if the module directory doesn't exist, don't do anything
if( !is_dir( self::$module_dir.DIRECTORY_SEPARATOR.$module ) ) return false;
//load the module file
$fname = scandir(self::$module_dir.DIRECTORY_SEPARATOR.$module);
$fname = $fname[2]; //first 2 elements will be . and ..
require_once( self::$module_dir.DIRECTORY_SEPARATOR.$module.DIRECTORY_SEPARATOR.$fname );
$className = str_replace('.class.php',NULL,$fname);
return new $className();
}
}
You would then extend that using your modules. That would allow you to overwrite a module by simply removing it's old file /my/module/dir/moduleName/moduleNameV1.0.class.php and replacing it with a new version /my/module/dir/moduleName/moduleNameV1.1.class.php
As mentioned in the comments, this will eventually fill the memory on the server, so you should schedule a reboot of the service each time you make substantial changes, but it also allows you to load new versions on demand without stopping the service.
A more stable approach would be to take advantage of process control and spin off daemons for each connection from your parent script, or implement a cache system that stores all data on the disk/database so that you can detect a change in module version and instantly reboot the server. But the solution above should work for you for the time being :)

Categories