ZenstruckObjectRoutingBundle

Symfony2 Bundle that allows generating urls from objects (sf1 style).

Build Status Scrutinizer Code Quality Code Coverage SensioLabs Insight StyleCI Latest Stable Version License

A Symfony Bundle that enables passing objects to your router. It works by decorating the default router with a custom ObjectRouter that transforms objects into a route name and route parameters. These are passed to the default router.

For those that remember symfony 1, this bundle brings back functionality that was available in that framework.

Installation

  1. Install with composer:

     composer require zenstruck/object-routing-bundle
    
  2. Enable the bundle in the kernel:

    // app/AppKernel.php
    
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new Zenstruck\ObjectRoutingBundle\ZenstruckObjectRoutingBundle(),
        );
    }

Usage

To use this bundle, you must first have an object. Let's use a BlogPost example:

namespace Acme\Entity;

class BlogPost
{
    private $id;
    private $slug;
    private $body;

    public function getId()
    {
        return $this->id;
    }

    public function getSlug()
    {
        return $this->slug;
    }

    public function getBody()
    {
        return $this->body;
    }
}

Next, you must have some routes for that object:

# app/config/routing.yml

blog_post_show:
    pattern:  /blog/{id}-{slug}
    defaults: { _controller: AcmeBundle:BlogPost:show }

blog_post_edit:
    pattern:  /blog/{id}/edit
    defaults: { _controller: AcmeBundle:BlogPost:delete }

blog_post_delete:
    pattern:  /blog/{id}
    defaults: { _controller: AcmeBundle:BlogPost:delete }
    methods:  [DELETE]

Without this bundle

Now, suppose you want to generate a route for a blog post. The standard way of doing this is as follows:

Twig:

{# variable "post" is an instance of "BlogPost" with id=1, slug=example #}

{{ path('blog_post_show', { id: post.id, slug: post.slug }) }} {# /blog/1-example #}
{{ path('blog_post_show', { id: post.id, slug: post.slug, view: full }, true) }} {# http://example.com/blog/1-example?view=full #}
{{ path('blog_post_edit', { id: post.id }) }} {# /blog/1/edit #}
{{ path('blog_post_delete', { id: post.id }) }} {# /blog/1 #}

Symfony Controller:

namespace Acme\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class MyController extends Controller
{
    public function myAction()
    {
        $post = // instance of Acme\Entity\BlogPost with id=1, path=example

        // blog_post_show (/blog/1-example)
        $url = $this->generateUrl('blog_post_show', ['id' => $post->getId(), 'slug' => $post->getSlug()]);

        // blog_post_show with extra parameter and absolute (http://example.com/blog/1-example?view=full)
        $url = $this->generateUrl(
            'blog_post_show',
            ['id' => $post->getId(), 'slug' => $post->getSlug(), 'view' => 'full'],
            true
        );

        // blog_post_edit (/blog/1/edit)
        $url = $this->generateUrl('blog_post_edit', ['id' => $post->getId()]);

        // blog_post_delete (/blog/1)
        $url = $this->generateUrl('blog_post_delete', ['id' => $post->getId()]);
    }
}

With this bundle

In this bundle's config, setup a mapping of BlogPost to the blog post routes:

# app/config/config.yml

zenstruck_object_routing:
    class_map:
        Acme\Entity\BlogPost:
            default_route: blog_post_show
            default_parameters: [id]
            routes:
                blog_post_show: [id, path]
                blog_post_edit: ~
                blog_post_delete: ~

Generating routes for a blog post is now much simpler:

Twig:

{# variable "post" is an instance of "BlogPost" with id=1, slug=example #}

{{ path(post) }} {# /blog/1-example (blog_post_show url because it is the default route) #}
{{ path('blog_post_show', post) }} {# equivalent to above #}
{{ path(post, { view: full }, true) }} {# http://example.com/blog/1-example?view=full #}
{{ path('blog_post_show', post, { view: full }, true) }} {# equivalent to above #}
{{ path('blog_post_edit', post) }} {# /blog/1/edit #}
{{ path('blog_post_delete', post) }} {# /blog/1 #}

Symfony Controller:

namespace Acme\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class MyController extends Controller
{
    public function myAction()
    {
        $post = // instance of Acme\Entity\BlogPost with id=1, slug=example

        // blog_post_show (/blog/1-example)
        $url = $this->generateUrl($post); // blog_post_show url because it is the default route
        $url = $this->generateUrl('blog_post_show', $post); // equivalent to above

        // blog_post_show with extra parameter and absolute (http://example.com/blog/1-example?view=full)
        $url = $this->generateUrl($post, ['view' => 'full'], true);
        $url = $this->generateUrl('blog_post_show', $post, ['view' => 'full'], true); // equivalent to above

        // blog_post_edit (/blog/1/edit)
        $url = $this->generateUrl('blog_post_edit', $post);

        // blog_post_delete (/blog/1)
        $url = $this->generateUrl('blog_post_delete', $post);
    }
}

Custom Transformations

This bundle comes with a ClassMapObjectTransformer that uses the bundle's config to map object classes to routes. If you have a more complex scenario, you can add your own transformers. Simply have your custom transformer implement Zenstruck\ObjectRoutingBundle\ObjectTransformer\ObjectTransformer and register it as a service tagged with zenstruck_object_routing.object_transformer.

See Zenstruck\ObjectRoutingBundle\ObjectTransformer\ClassMapObjectTransformer for a reference.

Full Default Config

zenstruck_object_routing:
    class_map:

        # Prototype
        class:

            # Optional - The route to use when an object is passed as the 1st parameter of Router::generate()
            default_route:        null

            # Route parameter as key, object method/public property as value (can omit key if object method/property is the same)
            default_parameters:

                # Examples:
                - id
                - path

            # Route name as key, parameter array as value (can leave parameter array as null if same as default_parameters)
            routes:

                # Examples:
                blog_show:           ~
                blog_edit:
                    - id

                # Prototype
                route_name:           []

NOTE 1: This bundle's router uses the PropertyAccess component to access the object's properties/methods.

NOTE 2: When mapping multiple objects that inherit one another, be sure to order them from child to parent. For instance, if you had a BlogPost that has a parent class of Page and both are mapped, be sure to put BlogPost before Page.

Copyright (c) 2014 Kevin Bond

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN