New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MvcTranslator being used without php5-intl (HeadTitle::__toString() must not throw an exception) #4879

Closed
asgrim opened this Issue Jul 24, 2013 · 28 comments

Comments

Projects
None yet
@asgrim
Contributor

asgrim commented Jul 24, 2013

When using headTitle() view helper in ZF 2.2.1 without php5-intl extension installed, everything works fine. Upgrade to ZF 2.2.2 (still without php5-intl) and we are seeing Method Zend\View\Helper\HeadTitle::__toString() must not throw an exception.

Installing php5-intl solves this problem - but we do not have any sort of internationalisation in our app.

I believe it is change weierophinney@36b20ff#L0L148 that breaks our specific case.

I do not know what the difference in MvcTranslator to translator is, but we have not configured either of these services, but apparently MvcTranslator is a Zend\I18n\Translator\Translator instance. If we do not use translation in our application, we should not be forced to use either Zend\I18n or php5-intl as a dependency.

@asgrim

This comment has been minimized.

Show comment
Hide comment
@asgrim

asgrim Jul 24, 2013

Contributor

Oh - if ZF2 cannot operate without php5-intl then maybe ext-intl should be put into composer.json?

Contributor

asgrim commented Jul 24, 2013

Oh - if ZF2 cannot operate without php5-intl then maybe ext-intl should be put into composer.json?

@Ocramius

This comment has been minimized.

Show comment
Hide comment
@Ocramius

Ocramius Jul 24, 2013

Member

@asgrim we had that, but that basically makes entire installations unusable (intl is not everywhere). Also, i18n is kinda niche, so we rely on users being aware of the extension being missing.

Member

Ocramius commented Jul 24, 2013

@asgrim we had that, but that basically makes entire installations unusable (intl is not everywhere). Also, i18n is kinda niche, so we rely on users being aware of the extension being missing.

@asgrim

This comment has been minimized.

Show comment
Hide comment
@asgrim

asgrim Jul 24, 2013

Contributor

But if something as simple as the headTitle view helper cannot operate without it, surely that means it is a dep now. We don't have any kind of translation configured - we didn't even use the skeleton application for this, so at no point have we thought about translation (our app simply doesn't need it). However, in 2.2.2, because a service somewhere (I haven't looked where) is configuring itself (as MvcTranslator as mentioned above), now headTitle cannot operate at all without the intl extension...

I don't know much about how translation works in ZF2 so I can't really think of a solution...

Contributor

asgrim commented Jul 24, 2013

But if something as simple as the headTitle view helper cannot operate without it, surely that means it is a dep now. We don't have any kind of translation configured - we didn't even use the skeleton application for this, so at no point have we thought about translation (our app simply doesn't need it). However, in 2.2.2, because a service somewhere (I haven't looked where) is configuring itself (as MvcTranslator as mentioned above), now headTitle cannot operate at all without the intl extension...

I don't know much about how translation works in ZF2 so I can't really think of a solution...

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Jul 24, 2013

Member

Translation in the helpers should typically be disabled by default; however, I cannot remember off the top of my head if this is determined by a flag, or presence of a translator.

Are you sure that the exception being raised is due to translation? Is it possible to determine the exception and pass on the details here?

Member

weierophinney commented Jul 24, 2013

Translation in the helpers should typically be disabled by default; however, I cannot remember off the top of my head if this is determined by a flag, or presence of a translator.

Are you sure that the exception being raised is due to translation? Is it possible to determine the exception and pass on the details here?

@asgrim

This comment has been minimized.

Show comment
Hide comment
@asgrim

asgrim Jul 24, 2013

Contributor

Yeah, definitely - I put a try/catch in Zend\View\Helper\Placeholder\Container\AbstractStandalone.php to catch it and dump it - previously we had the ever so useful error "Method Zend\View\Helper\HeadTitle::__toString() must not throw an exception".

The dump of the exception can be seen in this gist.

If I understand you correctly, at least in the change in 36b20ff it checks the existence of the MvcTranslator service - something which is configured in our application, but WE haven't configured (so I'm assuming ZF2 has configured it or perhaps another component?)

Contributor

asgrim commented Jul 24, 2013

Yeah, definitely - I put a try/catch in Zend\View\Helper\Placeholder\Container\AbstractStandalone.php to catch it and dump it - previously we had the ever so useful error "Method Zend\View\Helper\HeadTitle::__toString() must not throw an exception".

The dump of the exception can be seen in this gist.

If I understand you correctly, at least in the change in 36b20ff it checks the existence of the MvcTranslator service - something which is configured in our application, but WE haven't configured (so I'm assuming ZF2 has configured it or perhaps another component?)

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Jul 24, 2013

Member

Well, interestingly, from your backtrace, it's grabbing Zend\I18n\Translator\Translator -- which is not what the MvcTranslator service provides. MvcTranslator provides Zend\Mvc\I18n\Translator, which, while it extends the Zend\I18n version, should still show up in the stack trace if that service is being used.

This tells me that you're adding the Translator service (via the Zend\I18n\Translator\TranslatorServiceFactory) somewhere in your application, as the helper plugin manager is detecting it and attempting to inject it into your helpers.

This service (Translator) has been present in the skeleton application for over a year (i.e., since before the first stable version). We only recently (as of 2.2.0) modified the framework itself to add the factory for the MvcTranslator service by default, however. We did this after some debate, as we wanted to ensure that the MvcTranslator service is used in preference to the previously configured Translator service, due to changes we made in the Validator component to reduce dependencies.

My point, however, is that your application, @asgrim, is still referring to the old service, which is evidently configured in your application. The logic for injecting the translator into helpers only changed in that it checks first for the MvcTranslator service -- and, not finding that, falls back to the original behavior, which is what your gist indicates happened here.

The unfortunate problem: there's no way currently to unregister services in the ServiceManager; they're either there, or they are not.

Will need to think on a solution for this. In the meantime, one option you have is to extend Zend\I18n\Translator\Translator and make the various operations in it no-ops -- i.e., have translate($message) return $message, and so on. You would then register your extension under either the Translator and/or MvcTranslator services. This would work around the ext/intl requirement temporarily until we get a solution in place.

Member

weierophinney commented Jul 24, 2013

Well, interestingly, from your backtrace, it's grabbing Zend\I18n\Translator\Translator -- which is not what the MvcTranslator service provides. MvcTranslator provides Zend\Mvc\I18n\Translator, which, while it extends the Zend\I18n version, should still show up in the stack trace if that service is being used.

This tells me that you're adding the Translator service (via the Zend\I18n\Translator\TranslatorServiceFactory) somewhere in your application, as the helper plugin manager is detecting it and attempting to inject it into your helpers.

This service (Translator) has been present in the skeleton application for over a year (i.e., since before the first stable version). We only recently (as of 2.2.0) modified the framework itself to add the factory for the MvcTranslator service by default, however. We did this after some debate, as we wanted to ensure that the MvcTranslator service is used in preference to the previously configured Translator service, due to changes we made in the Validator component to reduce dependencies.

My point, however, is that your application, @asgrim, is still referring to the old service, which is evidently configured in your application. The logic for injecting the translator into helpers only changed in that it checks first for the MvcTranslator service -- and, not finding that, falls back to the original behavior, which is what your gist indicates happened here.

The unfortunate problem: there's no way currently to unregister services in the ServiceManager; they're either there, or they are not.

Will need to think on a solution for this. In the meantime, one option you have is to extend Zend\I18n\Translator\Translator and make the various operations in it no-ops -- i.e., have translate($message) return $message, and so on. You would then register your extension under either the Translator and/or MvcTranslator services. This would work around the ext/intl requirement temporarily until we get a solution in place.

@asgrim

This comment has been minimized.

Show comment
Hide comment
@asgrim

asgrim Jul 24, 2013

Contributor

I have checked the type of the returned value of $this->getTranslator() in HeadTitle->renderTitle() and it is definitely a Zend\Mvc\I18n\Translator instance. To further illustrate this, if I add the following into Zend\View\HelperPluginManager->injectTranslator():

var_dump($locator->has('MvcTranslator'));
var_dump($locator->has('translator'));
var_dump(get_class($locator->get('MvcTranslator')))

I can see:

bool(true)
bool(false)
string(24) "Zend\Mvc\I18n\Translator"

If I search for MvcTranslator in my entire application & dependencies, I can see reference in Zend\Mvc\Service\ServiceListenerFactory:

'MvcTranslator'                  => 'Zend\Mvc\Service\TranslatorServiceFactory',

This tells me (unless I'm chasing a red herring here) that this injection is happening because the MvcTranslator service simply exists - even though I don't want it to. But because it exists, I now have to use Zend\I18n and in turn the intl PHP extension in order to use my application - all because I wrote echo $this->headTitle('foo');...

As I mentioned, my application was not based on the skeleton application, so the Translator service is never configured anywhere (as can be seen in my output above). The change in 36b20ff breaks (in ZF 2.2.2) headTitle because even if the MvcTranslator service existed happily before, the Zend\View\HelperPluginManager->injectTranslator() method never checked for it - it only checked for Translator (which has never been configured in my application), so no translator at all was ever injected (i.e. using ZF 2.2.1).

Our application is still in early development, so I am happy to just install the intl ext in our environments until we can get a better fix, but for others this may not be so easy / appropriate.

Contributor

asgrim commented Jul 24, 2013

I have checked the type of the returned value of $this->getTranslator() in HeadTitle->renderTitle() and it is definitely a Zend\Mvc\I18n\Translator instance. To further illustrate this, if I add the following into Zend\View\HelperPluginManager->injectTranslator():

var_dump($locator->has('MvcTranslator'));
var_dump($locator->has('translator'));
var_dump(get_class($locator->get('MvcTranslator')))

I can see:

bool(true)
bool(false)
string(24) "Zend\Mvc\I18n\Translator"

If I search for MvcTranslator in my entire application & dependencies, I can see reference in Zend\Mvc\Service\ServiceListenerFactory:

'MvcTranslator'                  => 'Zend\Mvc\Service\TranslatorServiceFactory',

This tells me (unless I'm chasing a red herring here) that this injection is happening because the MvcTranslator service simply exists - even though I don't want it to. But because it exists, I now have to use Zend\I18n and in turn the intl PHP extension in order to use my application - all because I wrote echo $this->headTitle('foo');...

As I mentioned, my application was not based on the skeleton application, so the Translator service is never configured anywhere (as can be seen in my output above). The change in 36b20ff breaks (in ZF 2.2.2) headTitle because even if the MvcTranslator service existed happily before, the Zend\View\HelperPluginManager->injectTranslator() method never checked for it - it only checked for Translator (which has never been configured in my application), so no translator at all was ever injected (i.e. using ZF 2.2.1).

Our application is still in early development, so I am happy to just install the intl ext in our environments until we can get a better fix, but for others this may not be so easy / appropriate.

@stefanotorresi

This comment has been minimized.

Show comment
Hide comment
@stefanotorresi

stefanotorresi Jul 25, 2013

Contributor

Mumble mumble...
The Translator (now renamed MvcTranslator) service was not registered by default before 00b81b5. Why is registered by default now? Is there any component that strictly needs that?

If the default service list registered by Zend\Mvc\Service\ServiceListenerFactory cannot be reverted back for some reason, leaving aside the fact that that means that some component has a hard dependency on the translator service, it should not be a problem: the ExtensionNotLoadedException is only thrown when Zend\I18n\Translator\Translator is created without a default locale.
Now, it appears to me that the translator usage is not meant to be completely denied without intl, so a solution could be moving the extension_loaded() control block from Zend\I18n\Translator\Translator::getLocale() (which is very likely to be invoked at some point in any application with the service registered) to Zend\I18n\Translator\Translator::factory(), and providing a default locale value rather than throwing an exception when intl is not available. After all, the exception is thrown by all the classes that strictly need it: filters, validators and the like.

How does that sound?

Contributor

stefanotorresi commented Jul 25, 2013

Mumble mumble...
The Translator (now renamed MvcTranslator) service was not registered by default before 00b81b5. Why is registered by default now? Is there any component that strictly needs that?

If the default service list registered by Zend\Mvc\Service\ServiceListenerFactory cannot be reverted back for some reason, leaving aside the fact that that means that some component has a hard dependency on the translator service, it should not be a problem: the ExtensionNotLoadedException is only thrown when Zend\I18n\Translator\Translator is created without a default locale.
Now, it appears to me that the translator usage is not meant to be completely denied without intl, so a solution could be moving the extension_loaded() control block from Zend\I18n\Translator\Translator::getLocale() (which is very likely to be invoked at some point in any application with the service registered) to Zend\I18n\Translator\Translator::factory(), and providing a default locale value rather than throwing an exception when intl is not available. After all, the exception is thrown by all the classes that strictly need it: filters, validators and the like.

How does that sound?

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Jul 25, 2013

Member

@stefanotorresi We started registering the MvcTranslator service by default in 2.2.0, as there was no clean way to encourage using it over the Translator service the skeleton application was registering prior to that point; without registering it, applications that upgraded to 2.2.0+ would no longer have the ability to translate validator error messages.

The reason it's an issue now is that we also finally updated the HelperPluginManager in 2.2.2 to query for the existence of the MvcTranslator service in favor of the Translator service -- something I did forgetting that the MvcTranslator service is always registered now (regardless of whether or not it's used).

Yes, it's a mistake.

Yes, we need a solution.

But we need a solution that addresses all of the use cases. I'm looking into it.

Member

weierophinney commented Jul 25, 2013

@stefanotorresi We started registering the MvcTranslator service by default in 2.2.0, as there was no clean way to encourage using it over the Translator service the skeleton application was registering prior to that point; without registering it, applications that upgraded to 2.2.0+ would no longer have the ability to translate validator error messages.

The reason it's an issue now is that we also finally updated the HelperPluginManager in 2.2.2 to query for the existence of the MvcTranslator service in favor of the Translator service -- something I did forgetting that the MvcTranslator service is always registered now (regardless of whether or not it's used).

Yes, it's a mistake.

Yes, we need a solution.

But we need a solution that addresses all of the use cases. I'm looking into it.

@alexshelkov

This comment has been minimized.

Show comment
Hide comment
@alexshelkov

alexshelkov Aug 4, 2013

Here is simple way to disable translator in helpers, just place this code somewhere inside your module:

    public function onBootstrap(MvcEvent $e)
    {
        /** @var HelperPluginManager $helperPluginManger */
        $helperPluginManger = $e->getApplication()->getServiceManager()->get('ViewHelperManager');

        $helperPluginManger->addInitializer(function($helper) {
            if ( $helper instanceof TranslatorAwareInterface ) {
                $helper->setTranslatorEnabled(false);
            }
        });
    }

Pls when fixing this issue consider that often translation not required for application at all. So it must be easy to create ZF2 application with no translator in view helpers and etc.

alexshelkov commented Aug 4, 2013

Here is simple way to disable translator in helpers, just place this code somewhere inside your module:

    public function onBootstrap(MvcEvent $e)
    {
        /** @var HelperPluginManager $helperPluginManger */
        $helperPluginManger = $e->getApplication()->getServiceManager()->get('ViewHelperManager');

        $helperPluginManger->addInitializer(function($helper) {
            if ( $helper instanceof TranslatorAwareInterface ) {
                $helper->setTranslatorEnabled(false);
            }
        });
    }

Pls when fixing this issue consider that often translation not required for application at all. So it must be easy to create ZF2 application with no translator in view helpers and etc.

@crowebird

This comment has been minimized.

Show comment
Hide comment
@crowebird

crowebird Aug 7, 2013

Thanks for the fix alexshelkov!

crowebird commented Aug 7, 2013

Thanks for the fix alexshelkov!

@stubbetje

This comment has been minimized.

Show comment
Hide comment
@stubbetje

stubbetje Aug 12, 2013

Have been bitten by this issue as well, but not using the skeleton appliction.
I am using Zend\Mvc\Application, plain out of the box, with a very minimal config.

Thx @alexshelkov for the quick fix.

stubbetje commented Aug 12, 2013

Have been bitten by this issue as well, but not using the skeleton appliction.
I am using Zend\Mvc\Application, plain out of the box, with a very minimal config.

Thx @alexshelkov for the quick fix.

@dmakovec

This comment has been minimized.

Show comment
Hide comment
@dmakovec

dmakovec Sep 2, 2013

@alexshelkov's fix didn't work for me unfortunately (after an upgrade to 2.2.4). My short term workaround in layout.phtml (which is great if all you're doing is displaying pages, but forms will still throw errors):

    $this->headTitle()->setTranslatorEnabled(false);

dmakovec commented Sep 2, 2013

@alexshelkov's fix didn't work for me unfortunately (after an upgrade to 2.2.4). My short term workaround in layout.phtml (which is great if all you're doing is displaying pages, but forms will still throw errors):

    $this->headTitle()->setTranslatorEnabled(false);
@tfountain

This comment has been minimized.

Show comment
Hide comment
@tfountain

tfountain Sep 3, 2013

Contributor

Would a possible fix for this be to get the TranslatorServiceFactory to check whether the intl extension is available (or call a static method on Zend\I18n\Translator\Translator that does this)? It could then throw an exception if the application has translator configuration (indicating the app wants to use translation), or return null if it does not (edit: ok no, that doesn't work, as factories have to return an instance).

It would be nice for apps not using I18n to not have to include the Translator classes at all, but I can't really see a way of doing that without a BC break, so perhaps for ZF3.

Contributor

tfountain commented Sep 3, 2013

Would a possible fix for this be to get the TranslatorServiceFactory to check whether the intl extension is available (or call a static method on Zend\I18n\Translator\Translator that does this)? It could then throw an exception if the application has translator configuration (indicating the app wants to use translation), or return null if it does not (edit: ok no, that doesn't work, as factories have to return an instance).

It would be nice for apps not using I18n to not have to include the Translator classes at all, but I can't really see a way of doing that without a BC break, so perhaps for ZF3.

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Sep 3, 2013

Member

@tfountain That approach seems reasonable; can you prepare a PR for me that does this, please?

Member

weierophinney commented Sep 3, 2013

@tfountain That approach seems reasonable; can you prepare a PR for me that does this, please?

@androa

This comment has been minimized.

Show comment
Hide comment
@androa

androa Sep 12, 2013

Contributor

I looked into this now, as @tfountain as added in the edit, it is not possible to make the Factory return null. It really seems like there is no way to avoid the creation of the Translator. One way out could be adding knowledge to the Translator about if it's able to perform any translations (eg. if the $options array is empty, the Translator won't actually do anything).

Then making the HelperPluginManager check for this as well before injecting the Translator instance to helpers.

It would a very specific fix for helpers only and require some rather silly logic which preferably shouldn't be necessary.

Contributor

androa commented Sep 12, 2013

I looked into this now, as @tfountain as added in the edit, it is not possible to make the Factory return null. It really seems like there is no way to avoid the creation of the Translator. One way out could be adding knowledge to the Translator about if it's able to perform any translations (eg. if the $options array is empty, the Translator won't actually do anything).

Then making the HelperPluginManager check for this as well before injecting the Translator instance to helpers.

It would a very specific fix for helpers only and require some rather silly logic which preferably shouldn't be necessary.

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Sep 12, 2013

Member

@androa and @tfountain another possibility is to use the LazyServices functionality that @Ocramius introduced with 2.2.0, as that would delay creation of the MVCTranslator to the moment it is actually invoked.

Member

weierophinney commented Sep 12, 2013

@androa and @tfountain another possibility is to use the LazyServices functionality that @Ocramius introduced with 2.2.0, as that would delay creation of the MVCTranslator to the moment it is actually invoked.

@DASPRiD

This comment has been minimized.

Show comment
Hide comment
@DASPRiD

DASPRiD Sep 12, 2013

Member

Just noting, our problem is actually here:

https://github.com/zendframework/zf2/blob/master/library/Zend/View/HelperPluginManager.php#L144-L154

Since "MvcTranslator" is always defined, a translator is always injected, even when the user did not configure "translator".

Member

DASPRiD commented Sep 12, 2013

Just noting, our problem is actually here:

https://github.com/zendframework/zf2/blob/master/library/Zend/View/HelperPluginManager.php#L144-L154

Since "MvcTranslator" is always defined, a translator is always injected, even when the user did not configure "translator".

@DASPRiD

This comment has been minimized.

Show comment
Hide comment
@DASPRiD

DASPRiD Sep 12, 2013

Member

Suggested solution, dunno if this has any side effects:

    public function injectTranslator($helper)
    {
        if ($helper instanceof TranslatorAwareInterface) {
            $locator = $this->getServiceLocator();

            if (!$locator || !$locator->has('translator')) {
                return;
            }

            if ($locator && $locator->has('MvcTranslator')) {
                $helper->setTranslator($locator->get('MvcTranslator'));
            } elseif ($locator && $locator->has('translator')) {
                $helper->setTranslator($locator->get('translator'));
            }
        }
    }
Member

DASPRiD commented Sep 12, 2013

Suggested solution, dunno if this has any side effects:

    public function injectTranslator($helper)
    {
        if ($helper instanceof TranslatorAwareInterface) {
            $locator = $this->getServiceLocator();

            if (!$locator || !$locator->has('translator')) {
                return;
            }

            if ($locator && $locator->has('MvcTranslator')) {
                $helper->setTranslator($locator->get('MvcTranslator'));
            } elseif ($locator && $locator->has('translator')) {
                $helper->setTranslator($locator->get('translator'));
            }
        }
    }
@stefanotorresi

This comment has been minimized.

Show comment
Hide comment
@stefanotorresi

stefanotorresi Sep 12, 2013

Contributor

TBH I don't think that's a real solution because it just relies on the established convention that translator is an alias of MvcTranslator, set by the default configuration in the skeleton application since 2.2.
If one uses a service manager config where translator alias is not configured, it would eventually come up with the same ineloquent exception thrown by the view helper during rendering.

Now I'm probably missing something, so none of the following may be viable but anyway here are two solutions I came up with:

Do not instantiate Translator at all without intl

diff --git a/library/Zend/I18n/Translator/Translator.php b/library/Zend/I18n/Translator/Translator.php
index 5b2c156..c0f092f 100644
--- a/library/Zend/I18n/Translator/Translator.php
+++ b/library/Zend/I18n/Translator/Translator.php
@@ -105,6 +105,16 @@ class Translator
      */
     protected $eventsEnabled = false;

+    public function __construct()
+    {
+        if (!extension_loaded('intl')) {
+            throw new Exception\ExtensionNotLoadedException(sprintf(
+                '%s component requires the intl PHP extension',
+                __NAMESPACE__
+            ));
+        }
+    }
+
     /**
      * Instantiate a translator
      *
@@ -253,12 +263,6 @@ class Translator
     public function getLocale()
     {
         if ($this->locale === null) {
-            if (!extension_loaded('intl')) {
-                throw new Exception\ExtensionNotLoadedException(sprintf(
-                    '%s component requires the intl PHP extension',
-                    __NAMESPACE__
-                ));
-            }
             $this->locale = Locale::getDefault();
         }

Then the matter would be preventing the Translator from being registered as a default service when intl is not available.

Something like:

diff --git a/library/Zend/Mvc/Service/ServiceListenerFactory.php b/library/Zend/Mvc/Service/ServiceListenerFactory.php
index 469b673..eaf87d4 100644
--- a/library/Zend/Mvc/Service/ServiceListenerFactory.php
+++ b/library/Zend/Mvc/Service/ServiceListenerFactory.php
@@ -57,7 +57,6 @@ class ServiceListenerFactory implements FactoryInterface
             'HttpViewManager'                => 'Zend\Mvc\Service\HttpViewManagerFactory',
             'HydratorManager'                => 'Zend\Mvc\Service\HydratorManagerFactory',
             'InputFilterManager'             => 'Zend\Mvc\Service\InputFilterManagerFactory',
-            'MvcTranslator'                  => 'Zend\Mvc\Service\TranslatorServiceFactory',
             'PaginatorPluginManager'         => 'Zend\Mvc\Service\PaginatorPluginManagerFactory',
             'Request'                        => 'Zend\Mvc\Service\RequestFactory',
             'Response'                       => 'Zend\Mvc\Service\ResponseFactory',
@@ -117,6 +116,10 @@ class ServiceListenerFactory implements FactoryInterface
     {
         $configuration   = $serviceLocator->get('ApplicationConfig');

+        if (extension_loaded('intl')) {
+            $this->defaultServiceConfig['factories']['MvcTranslator'] = 'Zend\Mvc\Service\TranslatorServiceFactory';
+        }
+
         if ($serviceLocator->has('ServiceListenerInterface')) {
             $serviceListener = $serviceLocator->get('ServiceListenerInterface');

Now I know this is quite rough and dirty, but my point is that if a component requirement is not met, appropriate feedback should be given, rather than a generic "__toString() must not throw an exception" in a random helper.

Now AFAIK the Translator may be actually usable without intl if it had a default locale set, instead of trying to retrieve it from Locale, so another solution would be:

Check the configuration provided

diff --git a/library/Zend/I18n/Translator/Translator.php b/library/Zend/I18n/Translator/Translator.php
index 5b2c156..e6f31d2 100644
--- a/library/Zend/I18n/Translator/Translator.php
+++ b/library/Zend/I18n/Translator/Translator.php
@@ -126,13 +126,18 @@ class Translator

         $translator = new static();

-        // locales
+        // either provide a locale or rely on intl to lazily get it from Locale
         if (isset($options['locale'])) {
             $locales = (array) $options['locale'];
             $translator->setLocale(array_shift($locales));
             if (count($locales) > 0) {
                 $translator->setFallbackLocale(array_shift($locales));
             }
+        } elseif (!extension_loaded('intl')) {
+            throw new Exception\ExtensionNotLoadedException(sprintf(
+                '%s component requires either a "locale" setting or the intl PHP extension',
+                __NAMESPACE__
+            ));
         }

         // file patterns

This should make translations possible even without the extension, but I don't know if this was even intended in the first place.

Either way, a massive work woud have to be done in tests, because at a first glance a lot of tests that rely on intl do not skip when it's not available, so I wonder if it's the case to set up a test environment without the extension in Travis.

Contributor

stefanotorresi commented Sep 12, 2013

TBH I don't think that's a real solution because it just relies on the established convention that translator is an alias of MvcTranslator, set by the default configuration in the skeleton application since 2.2.
If one uses a service manager config where translator alias is not configured, it would eventually come up with the same ineloquent exception thrown by the view helper during rendering.

Now I'm probably missing something, so none of the following may be viable but anyway here are two solutions I came up with:

Do not instantiate Translator at all without intl

diff --git a/library/Zend/I18n/Translator/Translator.php b/library/Zend/I18n/Translator/Translator.php
index 5b2c156..c0f092f 100644
--- a/library/Zend/I18n/Translator/Translator.php
+++ b/library/Zend/I18n/Translator/Translator.php
@@ -105,6 +105,16 @@ class Translator
      */
     protected $eventsEnabled = false;

+    public function __construct()
+    {
+        if (!extension_loaded('intl')) {
+            throw new Exception\ExtensionNotLoadedException(sprintf(
+                '%s component requires the intl PHP extension',
+                __NAMESPACE__
+            ));
+        }
+    }
+
     /**
      * Instantiate a translator
      *
@@ -253,12 +263,6 @@ class Translator
     public function getLocale()
     {
         if ($this->locale === null) {
-            if (!extension_loaded('intl')) {
-                throw new Exception\ExtensionNotLoadedException(sprintf(
-                    '%s component requires the intl PHP extension',
-                    __NAMESPACE__
-                ));
-            }
             $this->locale = Locale::getDefault();
         }

Then the matter would be preventing the Translator from being registered as a default service when intl is not available.

Something like:

diff --git a/library/Zend/Mvc/Service/ServiceListenerFactory.php b/library/Zend/Mvc/Service/ServiceListenerFactory.php
index 469b673..eaf87d4 100644
--- a/library/Zend/Mvc/Service/ServiceListenerFactory.php
+++ b/library/Zend/Mvc/Service/ServiceListenerFactory.php
@@ -57,7 +57,6 @@ class ServiceListenerFactory implements FactoryInterface
             'HttpViewManager'                => 'Zend\Mvc\Service\HttpViewManagerFactory',
             'HydratorManager'                => 'Zend\Mvc\Service\HydratorManagerFactory',
             'InputFilterManager'             => 'Zend\Mvc\Service\InputFilterManagerFactory',
-            'MvcTranslator'                  => 'Zend\Mvc\Service\TranslatorServiceFactory',
             'PaginatorPluginManager'         => 'Zend\Mvc\Service\PaginatorPluginManagerFactory',
             'Request'                        => 'Zend\Mvc\Service\RequestFactory',
             'Response'                       => 'Zend\Mvc\Service\ResponseFactory',
@@ -117,6 +116,10 @@ class ServiceListenerFactory implements FactoryInterface
     {
         $configuration   = $serviceLocator->get('ApplicationConfig');

+        if (extension_loaded('intl')) {
+            $this->defaultServiceConfig['factories']['MvcTranslator'] = 'Zend\Mvc\Service\TranslatorServiceFactory';
+        }
+
         if ($serviceLocator->has('ServiceListenerInterface')) {
             $serviceListener = $serviceLocator->get('ServiceListenerInterface');

Now I know this is quite rough and dirty, but my point is that if a component requirement is not met, appropriate feedback should be given, rather than a generic "__toString() must not throw an exception" in a random helper.

Now AFAIK the Translator may be actually usable without intl if it had a default locale set, instead of trying to retrieve it from Locale, so another solution would be:

Check the configuration provided

diff --git a/library/Zend/I18n/Translator/Translator.php b/library/Zend/I18n/Translator/Translator.php
index 5b2c156..e6f31d2 100644
--- a/library/Zend/I18n/Translator/Translator.php
+++ b/library/Zend/I18n/Translator/Translator.php
@@ -126,13 +126,18 @@ class Translator

         $translator = new static();

-        // locales
+        // either provide a locale or rely on intl to lazily get it from Locale
         if (isset($options['locale'])) {
             $locales = (array) $options['locale'];
             $translator->setLocale(array_shift($locales));
             if (count($locales) > 0) {
                 $translator->setFallbackLocale(array_shift($locales));
             }
+        } elseif (!extension_loaded('intl')) {
+            throw new Exception\ExtensionNotLoadedException(sprintf(
+                '%s component requires either a "locale" setting or the intl PHP extension',
+                __NAMESPACE__
+            ));
         }

         // file patterns

This should make translations possible even without the extension, but I don't know if this was even intended in the first place.

Either way, a massive work woud have to be done in tests, because at a first glance a lot of tests that rely on intl do not skip when it's not available, so I wonder if it's the case to set up a test environment without the extension in Travis.

@DASPRiD

This comment has been minimized.

Show comment
Hide comment
@DASPRiD

DASPRiD Sep 12, 2013

Member

After some discussion on IRC, i'm preparing a PR with a different solution, which does not change anythng in Zend\I18n (since the problem is not there).

Member

DASPRiD commented Sep 12, 2013

After some discussion on IRC, i'm preparing a PR with a different solution, which does not change anythng in Zend\I18n (since the problem is not there).

@DASPRiD

This comment has been minimized.

Show comment
Hide comment
Member

DASPRiD commented Sep 12, 2013

@DASPRiD DASPRiD referenced this issue Sep 13, 2013

Closed

Hotfix/4879 #5108

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Oct 22, 2013

Member

Fixed with #5108 (for 2.3.0)

Member

weierophinney commented Oct 22, 2013

Fixed with #5108 (for 2.3.0)

@gkralik

This comment has been minimized.

Show comment
Hide comment
@gkralik

gkralik Feb 25, 2014

Contributor

Just stumbled upon this issue. I know this will be fixed in 2.3.0, but if anyone needs a solution for 2.2.* (and does not use the Translator), here it is:

https://gist.github.com/gkralik/9207273

Contributor

gkralik commented Feb 25, 2014

Just stumbled upon this issue. I know this will be fixed in 2.3.0, but if anyone needs a solution for 2.2.* (and does not use the Translator), here it is:

https://gist.github.com/gkralik/9207273

@bitwombat

This comment has been minimized.

Show comment
Hide comment
@bitwombat

bitwombat Nov 26, 2016

NOT FIXED.

This exact issue just bit me with versions much later than 2.3.0. I'm using ZF3, so not sure what versions of what modules are useful to report. But, exact same symptom, and solved by installing php*-intl.

Is anybody seeing this comment or do I need to open a new issue?

bitwombat commented Nov 26, 2016

NOT FIXED.

This exact issue just bit me with versions much later than 2.3.0. I'm using ZF3, so not sure what versions of what modules are useful to report. But, exact same symptom, and solved by installing php*-intl.

Is anybody seeing this comment or do I need to open a new issue?

@weierophinney

This comment has been minimized.

Show comment
Hide comment
@weierophinney

weierophinney Nov 26, 2016

Member

It is fixed, just not in the way you expect.

The issue referenced modified the MVC to define translator interfaces both in zend-i18n and zend-view. zend-mvc then provides an implementation that satisfies each. This allows using a dummy translator if the zend-i18n translator is not configured.

However, the zend-i18n translator requires the intl extension. The package only marks it as a suggestion, as there are other elements of the package that can be used without it. The point is: if you configure the zend-i18n translator, you will need to install the extension. Heed the suggestions when you install packages.

Member

weierophinney commented Nov 26, 2016

It is fixed, just not in the way you expect.

The issue referenced modified the MVC to define translator interfaces both in zend-i18n and zend-view. zend-mvc then provides an implementation that satisfies each. This allows using a dummy translator if the zend-i18n translator is not configured.

However, the zend-i18n translator requires the intl extension. The package only marks it as a suggestion, as there are other elements of the package that can be used without it. The point is: if you configure the zend-i18n translator, you will need to install the extension. Heed the suggestions when you install packages.

@bitwombat

This comment has been minimized.

Show comment
Hide comment
@bitwombat

bitwombat Nov 26, 2016

Heed the suggestions when you install packages.

Yes, learning that lesson! I've read tonnes and have added features to an existing app, but this was my first "from scratch" bring up.

Used $this->form() and got a huge long stack trace. My fault - I missed the suggestion that I needed zend-i18n. Figured that out, installed it, missed the suggestion again, and was met with a cryptic and long stack trace which I couldn't make sense of. Happened upon this thread which led me to install intl (called php-intl in Ubuntu), not ext-intl as suggested by composer. Not sure if that's correct or an error.

I'm just a newbie and doing my best to read docs and suggestions, but still missing some things obviously. Just telling my story in case it seems like a good idea for ZF to bark a little clearer about what it's unhappy about, if these two are common occurrences. Maybe the answer is 'no, just read the suggestions more carefully'.

Love the project, thanks so much for all the time and effort.

bitwombat commented Nov 26, 2016

Heed the suggestions when you install packages.

Yes, learning that lesson! I've read tonnes and have added features to an existing app, but this was my first "from scratch" bring up.

Used $this->form() and got a huge long stack trace. My fault - I missed the suggestion that I needed zend-i18n. Figured that out, installed it, missed the suggestion again, and was met with a cryptic and long stack trace which I couldn't make sense of. Happened upon this thread which led me to install intl (called php-intl in Ubuntu), not ext-intl as suggested by composer. Not sure if that's correct or an error.

I'm just a newbie and doing my best to read docs and suggestions, but still missing some things obviously. Just telling my story in case it seems like a good idea for ZF to bark a little clearer about what it's unhappy about, if these two are common occurrences. Maybe the answer is 'no, just read the suggestions more carefully'.

Love the project, thanks so much for all the time and effort.

@asgrim

This comment has been minimized.

Show comment
Hide comment
@asgrim

asgrim Nov 26, 2016

Contributor

which led me to install intl (called php-intl in Ubuntu), not ext-intl as suggested by composer

To hopefully help understand this, php-intl is the chosen package name, the extension is just called intl and Composer chose to express this requirement (or suggestion in this particular case) as ext-intl (the ext- part signifies this is a PHP extension rather than a PHP code library). Installation of PHP extensions varies depending on platform. Ubuntu provides the ability to install this particular extension by installing php-intl, this is probably to avoid naming conflicts with other i18n packages. Other methods could be YUM on RedHat based distros, and the name may or may not be different there, or using PECL (off my head, I'd imagine it'd be pecl install intl) for example (my point is not to recommend an installation method, but to highlight the distinction).

Contributor

asgrim commented Nov 26, 2016

which led me to install intl (called php-intl in Ubuntu), not ext-intl as suggested by composer

To hopefully help understand this, php-intl is the chosen package name, the extension is just called intl and Composer chose to express this requirement (or suggestion in this particular case) as ext-intl (the ext- part signifies this is a PHP extension rather than a PHP code library). Installation of PHP extensions varies depending on platform. Ubuntu provides the ability to install this particular extension by installing php-intl, this is probably to avoid naming conflicts with other i18n packages. Other methods could be YUM on RedHat based distros, and the name may or may not be different there, or using PECL (off my head, I'd imagine it'd be pecl install intl) for example (my point is not to recommend an installation method, but to highlight the distinction).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment