Thursday, December 11, 2014

Symfony site localization based on domain name

Problem: I want to localize my site based on the domain name.

Symfony strongly suggests that you use paths (like '/en' or '/fr') after the domain name to determine what the locale should be. This is ideal for a site with only one domain name, but for a site that has a different one for each localization, it's unnecessary. You should be able to determine the language based on the domain.

Solution: Use an event listener.

With an event listener, you can catch the request, parse the domain name, and set the locale appropriately. For this blog's purposes, let's say that a site has www.endomain.com for its English site and www.frdomain for its French site.

Create this folder/file in your bundle: EventListener/LocaleListener.php. Inside, put this:

namespace My\CustomBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;


class LocaleListener
{
  public function setLocale(GetResponseEvent $event)
  {
    if (strstr(strtolower($_SERVER['HTTP_HOST']), strtolower('frdomain')))
    {
      $request = $event->getRequest();
      $request->setLocale('fr');
    }

  }
}

If the HTTP_HOST contains the string 'frdomain', then set the locale to 'fr'.

Now register the listener in your bundle's services.yml file (it should be in Resources/config/). Inside, put this:

services:
    my_custom.language.kernel_request_listener:
        class: My\CustomBundle\EventListener\LocaleListener
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: setLocale }

Now every time a request is sent to Symfony, this listener runs first and sets the locale to 'fr' if it detects 'frdomain' in HTTP_HOST. Otherwise, it keeps the default locale (in this case, it's en).

That should work! Happy coding!

Monday, January 13, 2014

Symfony, internalization/localization, and 404 pages

Background

Given 1) the Symfony framework and 2) the need for localization, one shouldn't be surprised that there's a package that takes care of that already (somewhat): JMSI18nRoutingBundle. It works so that in your config.yml, you can specify the default locale, the locales that your site works with, and what the domain names are for each locale. I guess I'm not using this bundle correctly, though, because although my config.yml file looks like this:

jms_i18n_routing:
  default_locale: %locale%
  locales: [en, fr]
  strategy: custom
  hosts:
    en: www.englishVersion.com
    fr: www.frenchVersion.com
  redirect_to_host: true

typing in the French locale into the address bar redirects to the English site. (And if you happen to have an inkling of what I'm doing wrong, do let me know!)

This necessitates the extra steps of 1) creating a separate /fr path that serves up French content and 2) configuring .htaccess to redirect www.frenchVersion.com to that path.

The Problem

Given that 1) www.frenchVersion.com originally serves up English content and 2) needs coaxing in .htaccess to redirect, I hit the problem of English 404 pages appearing under the French domain. Ie., www.frenchVersion.com/path-does-not-exist will serve up the English 404 message.

The Solutions

The halfway solution - detect the language in the template

In my custom 404 page (in project_root\app\Resources\TwigBundle\views\Exception\error404.html.twig), I included an if-statement that checked the domain name and then set the variable lang appropriately.

{% set lang = ('frenchVersion' in app.request.getHost()) ? 'fr' : 'en' %}

{{ 'projname.error404.copy' | trans({}, "messages", lang) }}

This solution only worked halfway, though. While the 404 message was in French, the surrounding layout.html.twig was still in English.

Full solution - create a listener for any exceptions thrown

With the guidance of this Stackoverflow answer, I was able to check for the domain name before anything was rendered when an exception is thrown. This is a two-file (or two-part) solution. First, you create a LanguageListener in projectroot\src\GenericName\SpecificNameBundle\EventListener. Code it like:


namespace GenericName\SpecificNameBundle\EventListener;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;


class LanguageListener
{
  public function setLocale(GetResponseEvent $event)
  {
    if (strstr($_SERVER['HTTP_HOST'], 'frenchVersion'))
    {
      $request = $event->getRequest();
      $request->setLocale('fr');
    }
  }
}

And then in projectroot\src\GenericName\SpecificNameBundle\Resources\config\services.yml:

services:
  # ...
  genericname.language.kernel_request_listener:
    class: GenericName\SpecificNameBundle\EventListener\LanguageListener
    tags:
      - { name: kernel.event_listener, event: kernel.exception, method: setLocale }

And there you go! Even your layout should be rendered using the correct locale. Go try it out! Happy coding!

Friday, November 8, 2013

Using Symfony's routing.yml file for different host names

I like extensions, but when they crap out on you, you're kind of a sitting duck. So rather than rely on them, I figured I might as well learn to handle multiple domain names the extension-free way. Did you know this is now available in Symfony? How to match a route based on the host.
I needed a language toggle for my website, so that when a user is on the English site, the link to the French is available, and vice versa. Between routing.yml, parameters.yml, and config.yml, I was able to create a decent (but still "wordy") toggle for the pages I needed. (And as I type this, I realize that the correct way might be annotations instead. I'll look into that in a bit.)
Step 1: parameters.yml (in app/config/)
This is the only file that will contain the domain names in English and French. Ergo, all other config files will pull from this file. Specify keys like “domain_locale_en” and “domain_locale_fr” to hold the domain names of your English and French sites.
domain_locale_en:    www.programmingnotestoself.com
domain_locale_fr:    www.notesdeprogrammationalauto.com 
Step 2: config.yml (in app/config/)
This is the file where you’ll make the domain names available to the Twig (template) files. Modify the twig section.
twig:
    # ...
        globals:  
            domain_en:    %domain_locale_en%
            domain_fr:    %domain_locale_fr%
If you ever need to print out the domain in a link, for example, you can just do this:
< a href="%domain_en%">English blog< /a>
(If you try to include "http://" in parameters.yml, you’re going to get a surprise. At least, I did… I ended up in the default Symfony welcome page.)
Step 3: routing.yml (in app/config/)
This is the part that I’m sure can use a little more polishing, so if you have suggestions, please let me know. But, anyway, given that I now have two domain names for the root, I can code routing.yml like this:
en_root:
  path:     /
  host:     %domain_locale_en%
  defaults: { _controller: BlogPostsBundle:Default:index }

fr_root:
  path:     /
  host:     %domain_locale_fr%
  defaults: { _controller: BlogPostsBundle:Default:index }
So, yes, whether it detects www.programmingnotestoself.com or www.notesdeprogrammationalauto.com, it’ll go to the same controller. Ugly, no?
The not-so-fun part
Because of this hack, all the other pages and routes in my site need to be registered twice in the routing file. For example, www.programmingnotestoself.com/help and www.notesdeprogrammationalauto.com/aide lead to the same page, but Symfony needs to know that. This is how they look like in my routing.yml file:
_help:
    pattern:   /help
    host:      %domain_locale_en%
    defaults:  { _controller: BlogPostsBundle:Default:help }

_aide:
    pattern:   /aide
    host:      %domain_locale_fr%
    defaults:  { _controller: BlogPostsBundle:Default:help }
Notice host:? This restricts the path to that particular domain name. So www.programmingnotestoself.com/aide is totally legit, but try www.programmingnotestoself.com/aide and it’ll fail. But if you take out host:, you can totally do a crossover (the latter link). As for what locale will be served up, it’ll be whatever the domain name’s locale is.
The cute part
I didn’t know you could force Twig to translate a string to a particular locale!
Assume that the controller for this page passed on variable $lang as either 'fr' or 'en' and $lang_name as 'English' or 'Français':
{% if lang == 'en' %}
    < a href="http://{{domain_fr}}/{{ 'help'|trans(locale='fr')}}>{{lang_name}}< /a>
{% else %}
    < a href="http://{{domain_en}}/{{ 'help'|trans(locale='en')}}>{{lang_name}}< /a>
{% endif %}
If you’re on the English site, force 'help' to be translated to the French string, and do similar if you’re on the French site.
Happy coding!

Thursday, October 3, 2013

My new best friend

error_reporting(E_ALL);
ini_set('display_errors', '1');
Like most programmers, I do var_dumps and echo's... But when all else fails--this.

Tuesday, September 17, 2013

Symfony 2 symblog tutorial errors

It seems that the Symfony 2 tutorial ("Symblog") is a little out of date. While going through it, I stumbled on some unexpected behaviour. There were exceptions getting thrown when the tutorial made no mention of them. This could be due to several behaviours being deprecated since 2.1; I was using 2.3.4.

So without further ado...

The errors and their fixes as you work through the tutorial

1) When creating a controller method for the Contact page, $form->bind($this->bindRequest($request) resulted in:

FatalErrorException: Error: Call to undefined method Symfony\Component\Form\Form::bindRequest() in /path/to/root/src/Blogger/BlogBundle/Controller/PageController.php line 31

I poked around at code that I already knew worked and found this alternative:
$form->bind($this->getRequest());

2) While editing src/Blogger/BlogBundle/Entity/EnquiryType.php, adding this line $metadata->addPropertyConstraint('body', new MaxLength(50)); in static function loadValidatorMetadata(ClassMetadata $metadata) resulted in an exception:

FatalErrorException: Error: Class 'Symfony\Component\Validator\Constraints\MaxLength' not found in /path/to/root/src/Blogger/BlogBundle/Entity/Enquiry.php line xx

I read elsewhere that MaxLength and MinLength are deprecated since Symfony 2.1, so you're better off declaring the above line as
$metadata->addPropertyConstraint('subject', new Length(array('max' => 50)));

Just make sure to add this to the top of the class: use Symfony\Component\Validator\Constraints\Length;

3)When told to add the following lines to src/Blogger/BlogBundle/Resources/config/config.yml

parameters: blogger_blog.comments.latest_comment_limit: 10
You will instead get an error along the lines of blogger_blog.comments.latest_comment_limit must be defined. Now, the solution is to either do this tutorial (http://symfony.com/doc/current/cookbook/bundles/extension.html), or do the shortcut, which I did (because lazy).

4) While trying to enable Assetic for BloggerBlogBundle, you are instructed to put it in app/config/config.yml. However, it doesn't take effect. Instead all sidebar.css-ing disappeared. This is because you should have put BloggerBlogBundle in app/config/config_dev.yml.

5) Yui Compressor jar file not found - Make sure that 1) you've put the jar file in app/Resources/java and that 2) the name of that jar file is the same as in config_dev.yml.

If there will be more, I will update this post. Happy coding!

Saturday, July 20, 2013

Symfony 1.4 error: Class 'BaseFormDoctrine' not found in (projectpath)/BaseAuthorForm.class.php

Blogging because I thought this error was funny!

Currently, I'm playing around with Symfony 1.4 (for particular reasons). I've gotten to the part where I can now generate modules so that CRUD forms will be auto-created for me. When I executed this command:

php symfony doctrine:generate-module --with-show --non-verbose-templates frontend author Author

I got this error:



PHP Fatal error:  Class 'BaseFormDoctrine' not found in (pathto project)/lib/plugins/sfDoctrinePlugin/test/functional/fixtures/lib/form/doctrine/base/BaseAuthorForm.class.php on line 14.




I searched around because I'm completely new to Symfony and am under a deadline. (Spending the weekend on this. Sigh...) Heard that there's a file called config_autoload.yml.php in cache/frontend/dev/config that maps out what file the key "BaseFormDoctrine" leads to. As it turned out, it led to lib/plugins/sfDoctrinePlugin/data/generator/sfDoctrineForm/default/template/sfDoctrineFormBaseTemplate.php.

So I looked at it. And guess what! The opening php tag in the file didn't begin with "<?php". It began with "[?php".

Changed that square bracket to "<" and I was back on the road.

Monday, June 17, 2013

Ruby on Rails (on Windows) - "ruby_check_sizeof_voidp is negative"

Today I installed Ruby on Rails on my Windows machine, but had to jump through a few hoops to get it done. The biggest stumbling block was that whenever I got down to executing "rails new myapp", I got a long error that started with

C:/Ruby193/bin/ruby.exe extconf.rb
creating Makefile

make
generating generator-i386-mingw32.def
compiling generator.c
In file included from c:/Ruby193/include/ruby-1.9.1/ruby.h:32:0,
                 from ../fbuffer/fbuffer.h:5,
                 from generator.c:1:
c:/Ruby193/include/ruby-1.9.1/ruby/ruby.h:109:14: error: size of array 'ruby_check_sizeof_voidp' is negative
In file included from c:/Ruby193/include/ruby-1.9.1/ruby.h:32:0,
                 from ../fbuffer/fbuffer.h:5,
                 from generator.c:1:


and ended with

make: *** [generator.o] Error 1

and some version of the message "make sure the json gem was installed correctly" and "log errors will be stored at C:\Ruby193\lib\ruby\gems\1.9.1\gems\json-1.8.0\ext\json\ext\generator\gem_make.out".

So I tried "gem install json -v '1.8.0'" (sometimes with "--platform=ruby") but I still received errors. Then I actually looked at gem_make.out and sow the following line: "c:/Ruby193/include/ruby-1.9.1/ruby/ruby.h:109:14: error: size of array 'ruby_check_sizeof_voidp' is negative".

Cue this StackOverflow post, and I realized that I did not install the correct Devkit. I so wanted to work with the latest release of everything that I ignored the "Which Development Kit?" section of this Ruby Installer for Windows page.

I reinstalled Devkit (following the instructions here) and actually created my new web app. Now, I have Ruby 1.9.3 working with Rails 3.2.13 and the tdm-32-4.5.2 version of DevKit installed.