Advertisement

Migration to Symfony2 continued

On December 21, 2011 Stefan Koopmanschap wrote an excellent article on this blog titled “Painless (well, less painful) migration to Symfony2.” In his article he explained the advantages of doing a gradual migration. The technical solution that he proposed to make this possible was to “…wrap your old application into your Symfony2 application.” He even provided us the tool (The IngewikkeldWrapperBundle code) to do so.

We were very much inspired by his passionate elucidation and we were fully convinced of the urge to start migrating to Symfony2 as soon as possible. However, he also provided us with a “A word of caution” about 2 things: performance and authentication/authorization. This might get some people worried, but not us: it challenged us to find a solution for those two open issues.

1. Performance

As Stefan Koopmanschap explains, in his solution you “…use two frameworks for all your legacy pages” and “…two frameworks add more overhead than one.” Since our Symfony1 application (the LeaseWeb Self Service Center) is not the fastest you’ve ever seen, some customers are even complaining about it’s speed, this got us a little worried. Losing performance was not really an option, so we had to find a solution.

Symfony 1 & 2 both use a Front Controller architecture (one file handling all requests) we were just looking for seperating traffic between the two front controllers. Stefan proposed to do so using Symfony 2 routing and make it use a fall-back route to handle your legacy URLs. We hereby propose to do it using a .htaccess rewrite. This has virtually no overhead, because every Symfony request gets rewritten by mod_rewrite anyway.

2. Authentication/authorization

He also wrote: “Another issue is sessions.” Further clarifying the problem by stating: “If your application works with authentication and authorization, you’ll now have to work with two different systems that have a different approach to authentication and authorization”. Since our application requires both authentication and authorization we had to come up with a solution here. We decided to move the authentication (login page) to Symfony2 and make Symfony1 “trust” this authentication done by “Symfony2”.

To realize this solution we had to enable Symfony1 to “see” and “understand” the Symfony2 session. First we made sure that both applications use the same name by setting the Symfony2 “framework_session_name” setting in “app/config/config.yml” to “symfony”. Then we reverse engineered the Symfony2 session storage and found that it serializes some PHP object into it. To be able to unserialize those objects we had to register an autoload function in Symfony1 using “spl_autoload_register”

Finally, instructions

To solve the performance problem we installed Symfony2 in the “sf2” directory inside the Symfony1 project (next to “apps”) and we started by changing the lines in our “web/.htaccess” file from:

# redirect to front controller
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]

And added these lines above it:

# redirect to new symfony2 front controller
RewriteCond %{REQUEST_FILENAME} !-f
# but only if URL matches one from this list:
RewriteCond %{REQUEST_URI} ^/users/login
# end of list
RewriteRule ^(.*)$ sf2/web/$1 [QSA,L]

To support the Symfony2 authentication and authorization in Symfony1 we created a “Symfony2AuthenticationFilter” class. This filter can be loaded by putting it under “lib/filter” folder in your Symfony1 project and add the following lines in “apps/ssc/config/filters.yml”:

symfony2AuthenticationFilter:
    class: Symfony2AuthenticationFilter

For configuration of the filter we added a few new application settings to “/apps/ssc/config/app.yml”:

all:
    symfony2:
        paths: ['sf2/vendor/symfony/src', 'sf2/src', 'sf2/vendor/bundles', 'sf2/vendor/doctrine-common/lib']
        attribute: '_security_main'

This path setting shows that Symfony2 is located in the “sf2” sub-directory of the Symfony1 project. The attribute reflects the name of the Symfony2 firewall. The code of the Symfony2AuthenticationFilter is this:

function symfony2_autoload ($pClassName)
{
  $sf2Paths = sfConfig::get('app_symfony2_paths');

  foreach ($sf2Paths as $path)
  {
    $path = sfConfig::get('sf_root_dir') . DIRECTORY_SEPARATOR . $path;
    $file = $path . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR ,$pClassName ) . ".php";

    if (file_exists($file))
    {
      include($file);
      break;
    }
  }
}

spl_autoload_register("symfony2_autoload");

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Role\Role;

class Symfony2AuthenticationFilter extends sfFilter
{
  public function execute($filterChain)
  { // get session data
    $sessionData = null;
    $symfony2Attribute = sfConfig::get('app_symfony2_attribute');
    if (isset($_SESSION['_symfony2']['attributes'][$symfony2Attribute]))
    { $sessionData = unserialize($_SESSION['_symfony2']['attributes'][$symfony2Attribute]);
    }
    // get sf1 username
    if (!$this->getContext()->getUser()->isAuthenticated()) $sf1UserName = false;
    else $sf1UserName = $this->getContext()->getUser()->getUserName();
    // get sf2 username
    if (!$sessionData) $sf2UserName = false;
    else $sf2UserName = $sessionData->getUser()->getUserName();
    // if usernames do not match
    if ($sf1UserName!=$sf2UserName)
    { if ($sf2UserName) // if symfony2 is signed in
      { // signin to symfony1
        $this->getContext()->getUser()->setUserName($sf2UserName);
        $this->getContext()->getUser()->setAuthenticated(true);
        $this->getContext()->getUser()->clearCredentials();
      }
      else // if symfony2 is not signed in
      { // signout from symfony1
        $this->getContext()->getUser()->setUserName(false);
        $this->getContext()->getUser()->setAuthenticated(false);
        $this->getContext()->getUser()->clearCredentials();
        // redirect to current page
        $path = $this->getContext()->getRequest()->getPathInfo();
        $this->getContext()->getController()->redirect($path);
      }
    }
    // and execute next filter
    $filterChain->execute();
  }
}

Update (Jan 22, 2013): You can now find the bundle we created for this on the LeaseWeb Labs on GitHub account and Packagist:

18 Responses to “Migration to Symfony2 continued”

  • [...] von Posts, Stefan Koopmanschap über Umhüllung Ihren Code, um ihn gesprochen zu arbeiten . In diese zweite post , Maurtis van der Schee packt zwei Fragen Stefan genannt – Performance-Probleme und [...]

  • John Patrick Gerdeman:

    I’m having trouble following.

    1. By »setting the Symfony2 “framework_session_name” setting in “app/config/config.yml” to “symfony”« you mean “app/config/config.yml” should look like:

    framework:
    session:
    name: symfony

    Correct?

    2. The sfUser::clear method is not available in sfUser, sfBasicSecurityUser or sfGuardSecurityUser. Where is it defined at and what is its purpose? I assume it clears the credentials and the attribute holders, correct?

    John

  • Maurits van der Schee:

    @John: 1) correct, 2) also correct! It clears all identifying data from the user.

    I’ve updated the post, thank you for your improvements!

  • Guill:

    Hi.

    I can’t make this work properly. Not sure whether my .htaccess is correct or not. Is it possible for you to publish the final .htaccess?
    What about the one in the sf2/web folder, does it need any manipulation?

  • Guill:

    And according to your .htaccess, isn’t it that the sf2 folder should be in the web folder of sf1, and not next to the ‘apps’ folder? Else how can the redirect work?

  • Guill:

    Alright, got it, symlinks of course.
    1st part works well, but autoload and the bundles give me issues. I’ll checkout these couple of days. Thanks!

  • Maurits van der Schee (Innovation Engineer):

    @Guill: If you are more specific I might be able to help you. What are you running into exactly?

  • Guill:

    Hi Maurits, thanks for the quick response!

    First, I’m not sure whether I’m doing things correctly, but let me explain where I am now.

    I’ve been able to separate the traffic between sf1 and sf2 with the sf1 .htaccess.
    The login is sent to sf2. I can login correctly on sf2, and I then redirect to sf1. sf1 can’t login.
    The session name between sf1 and sf2 are the same, but when the user logged in and is redirected to sf1, the session on sf1 appears empty (i dumped it on sf2 and it is not empty there).

    As the bundle is new, I was not sure if I had to use it, so what I’m explaining above is without the LswRemoteTemplateBundle (I’m thinking it’s probably why it doesn’t work, haha).
    I’ve tried to use the LswRemoteTemplateBundle but I’m having 2 issues:
    1. the api_caller.curl service is not declared. I’ve installed your LswApiCallerBundle as well but still the service is not declared there, so I’m not sure what to do with that error.
    2. in the services.yml of the LswRemoteTemplateBundle, one of the argument sent to the template (the @session) throws the following error:
    ErrorException: Catchable Fatal Error: Argument 2 passed to Lsw\RemoteTemplateBundle\Templating\RemoteSessionCachedTemplate::__construct() must be an instance of Symfony\Component\HttpFoundation\Session, instance of Symfony\Component\HttpFoundation\Session\Session given, called in /Users/guill/Dropbox/Development/ums1ums2/sf2/app/cache/dev/appDevDebugProjectContainer.php on line 1555 and defined in /Users/guill/Dropbox/Development/ums1ums2/sf2/vendor/leaseweb/remote-template-bundle/Lsw/RemoteTemplateBundle/Templating/RemoteSessionCachedTemplate.php line 25
    Not sure what to do with that error either.

    I’ve been using sf1 for a while but I’m a big noob at sf2. Maybe something obvious that I’ve missed?
    Thanks for your help Maurits.

  • Maurits van der Schee (Innovation Engineer):

    Hi Guill,

    1: the “api_caller.curl” is renamed to “api_caller” in the latest version of the bundle. You can simply rename the reference to the service in LswRemoteTemplateBundle in “Resources/config/services.yml”. I’ve updated the LswRemoteTemplateBundle to reflect the change (on github).
    2: this seems to be a difference between symfony 2.1 and symfony 2.0. @session service is now of type Symfony\Component\HttpFoundation\Session\Session while it was of type Symfony\Component\HttpFoundation\Session in 2.0. I’ve updated the LswRemoteTemplateBundle to reflect the change (on github).

    I hope it works now (in symfony 2.1), but i feel free to post more problems. Regards, Maurits

  • Guill:

    Hi Maurits,

    I apologize for the late answer. Chinese New Year where I’m living, so I spent some time off in the real world!
    I’ve got the latest bundle from github and I don’t have any error message anymore, thanks for that. I still can’t share the sessions though, but this might be because of my weak knowledge of Symfony 2. I’ll investigate. Thanks again.

  • Maurits van der Schee (Innovation Engineer):

    @Guill: If there you think there is something I can help you with, please let me know.

  • Guill:

    Hi again Maurits,

    Thanks for the words. I might need a little bit more help yes. I’m not sure what I’m doing wrong, but I can login with sf2 and I get redirected to a sf1 page but my user is not logged in sf1. sf2 and sf1 share the same session name (verified with Chrome dev tools), and loading sf1 loads the Symfony2AuthenticationFilter where I dump the php $_SESSION to check, but what I get from the dump is just an empty array. Any idea where I could go wrong?

  • Guill:

    Alright just to keep up to date, this was an issue with Symfony 2.1, but not with 2.0.
    Sf 2.1 stores the session in its own folder, so I had to force it to store in the default PHP folder. Now I can exchange values between sf1 and sf2.

  • Maurits van der Schee (Innovation Engineer):

    @Guill: Great. I think you are on the right track. The session storage should indeed be set exactly the same on both projects, so both session name and storage method. You way of debugging by dumping $_SESSION is exactly the method I used to debug the problem as well.

  • Guill:

    Hi Maurits!

    Just to let you know that it is working now! It is a great solution, thanks a lot for exploring that and for your help.
    I still don’t really get the use of the bundles though, I’ve removed them from the Kernel in sf2 and everything is still working.

    Anyway, I will now start working on the ‘following’ of this solution. Our app is going to be balanced on several VMs, so I need to store the session in a database instead of the session files. Sf2 has an easy way through configuration to store the session in a mongo database, but I’ve just found that the data stored is just a big hash. So I’ll need to find a way to deconstruct this. I’ll let you know how it goes if you’re interested.

    Thanks again!

  • Maurits van der Schee (Innovation Engineer):

    @Guill: Glad it worked. One advice: consider using a memcache cluster to store your session information in, because database session storage can put more load on you database than you might expect.

  • […] You may wrap your legacy project in a brand new sf2 project, by using this bundle. This way, you’ll be able to migrate your project one piece at a time, and new functionalities may be developed with sf2 as soon as you get the wrapper to work. You may be interested by this post about migrating […]

Leave a Reply