Cross Link Application with symfony

30 / 5 / 2009

One of the main complain about symfony is that sharing a route between applications is not an easy task, and there is not a clean way to do that.

The current solutions are:

  • loading application routing at runtime: not able to use caching
  • loading the context of the other application to get the related routing object: heavy to display a few links
  • writing a specific helper with hard coded element

Now, let's introduce another way to do cross link application in symfony.

The idea is to include applications' routes in the current application but we also need to keep the right level of separation between the different applications.

The swToolboxPlugin implements a natural way to share links between application, for instance you can call from the frontend a backend route by doing:

<?php link_to('@backend.edit_blog_post?id='.$post->getId()) ?>

Installation

  • Install the plugin from the svn (I will provide a valid package soon).
  • Edit the app.yml file and add these few lines
all:
    swToolbox:
        routes_register_cross_applications: on

        swToolboxCrossApplicationRouting:
            backend:
                enabled: on
                load: [frontend]
                host:
                    dev: local.host/frontend_dev.php
                    prod: local.host

            frontend:
                enabled: on
                load: [backend]
                host:
                    dev: backend.local.host/backend_dev.php
                    prod: backend.local.host
  • execute a symfony cc

The few config lines specify to the plugin to load the backend app's route when using the frontend's app and "vice-versa".

Usage

From one application you can do:

<?php link_to('@backend.edit_blog_post?id='.$post->getId()) ?>

No helper or modification are required in your code, except adding more links ;)

How does that work ?

The plugin register a specific routing handler, which load routes from another application. All external routes are encapsulated into a specific proxy route. The proxy route just adds more requirements to the loaded route to make sure they cannot match the routes from the current application. The specific routing handler also renames route to 'APP_NAME.ROUTE_NAME'.

That's all ;)

Limitations

  • for now this only work with named route
  • so all your routes must be defined into the routing.yml file
  • this will not work if your are using no_script_name = off (due to a fixGenerateUrl call that we cannot remove without another hack).

Extra hack !

In some cases you might not want to edit a link, such as APP_NAME.ROUTE_NAME, but you want to keep the ROUTE_NAME only. This can be done but with a little hack into the sfFrontWebController which is in charge of generating the url. Create your own sfFrontWebController and extend the genUrl method.

<?php
class yourFrontWebController extends sfFrontWebController
{
    /**
    *
    * @see sfWebController#genUrl()
    */
    public function genUrl($parameters = array(), $absolute = false)
    {

        // absolute URL or symfony URL?
        if (is_string($parameters) && preg_match('#^[a-z][a-z0-9\+.\-]*\://#i', $parameters))
        {
            return $parameters;
        }

        // relative URL?
        if (is_string($parameters) && 0 === strpos($parameters, '/'))
        {
            return $parameters;
        }

        if (is_string($parameters) && $parameters == '#')
        {
            return $parameters;
        }

        $route = '';
        $fragment = '';

        if (is_string($parameters))
        {
            // strip fragment
            if (false !== ($pos = strpos($parameters, '#')))
            {
                $fragment = substr($parameters, $pos + 1);
                $parameters = substr($parameters, 0, $pos);
            }

            list($route, $parameters) = $this->convertUrlStringToParameters($parameters);
        }
        else if (is_array($parameters))
        {
            if (isset($parameters['sf_route']))
            {
                $route = $parameters['sf_route'];
                unset($parameters['sf_route']);
            }
        }

        // ------ START OF HACK

        // Custom method to avoid the need of modifying the route name in the template
        //   this is usefull if the template is shared across multiple template
        //   the first route found will be used
        if(!$this->context->getRouting()->hasRouteName($route))
        {
            $sw_cross_link_config = sfConfig::get('app_swToolbox_swToolboxCrossApplicationRouting', array());
            $sw_cross_current_app = $this->context->getConfiguration()->getApplication();

            if(array_key_exists($sw_cross_current_app, $sw_cross_link_config))
            {
                foreach($sw_cross_link_config[$sw_cross_current_app]['load'] as $app_to_load)
                {
                    $app_route = $app_to_load.'.'.$route;
                    if($this->context->getRouting()->hasRouteName($app_route))
                    {
                        $route = $app_route;
                        break;
                    }
                }
            }
        }

        // ------- END OF HACK

        // routing to generate path
        $url = $this->context->getRouting()->generate($route, $parameters, $absolute);

        if ($fragment)
        {
            $url .= '#'.$fragment;
        }

        return $url;
    }
}
  • edit your factories.yml file and add these few lines
    all:
      controller:
        class: mgWebController
  • execute a symfony cc

Now if the routing class cannot find the route in the current application, it will look to the external routes loaded. If the route exists, boom you will get the route ;)