Skip to content

Instantly share code, notes, and snippets.

@mattattui
Last active October 16, 2016 12:07
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattattui/efd1c4470f2ce0097fb5 to your computer and use it in GitHub Desktop.
Save mattattui/efd1c4470f2ce0097fb5 to your computer and use it in GitHub Desktop.
Using obfuscated ids with DoctrineParamConverter

Using Tiny (ID obfuscator) with Symfony2's ParamConverters

  • Symfony's convenience methods for automatically fetching database entities from URL parameters are super-handy.
  • Obfuscated/hash IDs are a great idea, especially in APIs (where you aren't concerned with SEO, but might be concerned about sequential numeric ids or exposing database information).
  • Here's how to make them work together.

The stuff in this gist sets up a Twig filter (obfuscate) to create the obfuscated ids (for URLs), makes the obfuscator available as a service (id_obfuscator) so you can also generate obfuscated URLs in your controllers or whatever, and extends the DoctrineParamConverter to allow it to retrieve entities by their deobfuscated id.

Following Phil Sturgeon's excellent advice in Build APIs You Won't Hate, I've also added an option to allow multiple ids to be loaded at once, like /resources/id1,id2,id3,id4. It's really quite handy sometimes. Bewarned though; it won't 404 if only some of the ids are missing. Of this whole thing, this is the part that could do with the most attention, sorry! The good news is that it's totally optional, so if you want to avoid the potential shonkiness then just don't use it. In the example controller code I put separate actions to demonstrate how you use it with and without multiple-id support, but obviously don't use both, that's just silly. Pick one! If you don't like to read and just paste the code in, it'll crash. That's my petty vengeance at work :)

I've used Zack Kitzmiller's Tiny-php library here because it's simple and great, but you could use hashids instead with very little change; same difference.

Take it, use it (at your own risk), fix it, no need to credit me. I'm not sure it warrants clogging up Packagist with, but if you do decide to make it into a reusable Symfony bundle then I'd love to know about it.

If you aren't using Symfony 2.6 (or you upgraded to it)

  • sf2.6 has an app/config/services.yml file, while if you started your project with an earlier version you probably don't. You can make your own and import it to config.yml or you could just put this stuff in there directly, whatever.
  • sf2.6 starts new projects with an AppBundle, following the Symfony Best Practices, while older versions don't (and actually make it kind of an arse to set one up, since it has no vendor namespace). Just… use whatever you're already using I guess.

Suggested improvements

  • Read APIs you won't hate again to remember how to handle 404s with multiple ids
  • Support for different Tiny keys (e.g. for different entities)
  • Add tests
  • Make it a Bundle
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Resource;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class ResourceController extends Controller
{
// …
/**
* @Route("/resources/{id}", name="resource_show")
* @ParamConverter("resource", class="AppBundle:Resource", converter="obfuscated")
*/
public function showAction(Request $request, Resource $resource)
{
//…
return $this->render('AppBundle:Resource:show.html.twig', [
'resource' => $resource,
]);
}
/**
* Alternative with support for multiple ids. Don't use both, pick one -
* I've made this throw a fatal error so you don't just cut and paste :P
*
* @Route("/resources/{id}", name="resource_show")
* @ParamConverter("resources", class="AppBundle:Resource", converter="obfuscated", options={
* "repository_method":"findById",
* "multiple": true
* })
*/
public function showAction(Request $request, array $resources)
{
//…
return $this->render('AppBundle:Resource:show.html.twig', [
'resources' => $resources,
]);
}
}
{% extends '::layout.html.twig' %}
{% block body %}
//…
<a href="{{ path('resource_show', { id: resource.id | obfuscate }) }}">Link to thing</a>
// …
{% endblock %}
<?php
namespace AppBundle\Request\ParamConverter;
use Doctrine\Common\Persistence\ManagerRegistry;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter;
use Symfony\Component\HttpFoundation\Request;
use ZackKitzmiller\Tiny;
/**
* DoctrineParamConverter.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ObfuscatedDoctrineParamConverter extends DoctrineParamConverter
{
/**
* @var ManagerRegistry
*/
protected $registry;
/**
* @var ZackKitzmiller\Tiny
*/
protected $tiny;
public function __construct(ManagerRegistry $registry = null, Tiny $tiny)
{
parent::__construct($registry);
$this->tiny = $tiny;
}
protected function getIdentifier(Request $request, $options, $name)
{
$id = parent::getIdentifier($request, $options, $name);
if ($id && array_key_exists('multiple', $options) && $options['multiple']) {
$id = array_map([$this->tiny, 'from'], array_filter(preg_split('/[\s,]+/', $id)));
return $id;
}
if ($id) {
return $this->tiny->from($id);
}
return false;
}
}
<?php
namespace AppBundle\Extension;
use ZackKitzmiller\Tiny;
class Obfuscator extends \Twig_Extension
{
private $tiny;
public function __construct(Tiny $tiny)
{
$this->tiny = $tiny;
}
/**
* Returns a list of functions to add to the existing list.
*
* @return array An array of functions
*/
public function getFilters()
{
return array(
'obfuscate' => new \Twig_Filter_Method($this, 'getObfuscatedId'),
);
}
public function getObfuscatedId($id)
{
return $this->tiny->to($id);
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return 'obfuscate';
}
}
parameters:
id_obfuscator_key: "[generate this using vendor/zackkitzmiller/tiny/bin/genset]"
services:
id_obfuscator:
class: ZackKitzmiller\Tiny
arguments:
- "%id_obfuscator_key%"
obfuscated_paramconverter:
class: AppBundle\Request\ParamConverter\ObfuscatedDoctrineParamConverter
arguments:
- "@doctrine"
- "@id_obfuscator"
tags: # Priority false means it won't ever run automatically; you have to ask for it
- { name: request.param_converter, priority: false, converter: obfuscated }
obfuscator_extension:
private: true
class: AppBundle\Extension\Obfuscator
arguments:
- "@id_obfuscator"
tags:
- { name: twig.extension }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment