views:

99

answers:

2

In order to not show the ID's of the members of my social network in the URL, I have created this route:

perfil_miembro:
  url: /miembros/:nombre_apellidos
  class: sfDoctrineRoute
  options: { model: Usuario, type: object}
  param: { module: miembros, action: show}

And added this line in the show action:

$this->usuario = $this->getRoute()->getObject();

It works OK: when I click on their names the corresponding profile is shown, and the URL is this type:

frontend_dev.php/miembros/Maria+de+Miguel+Alvarado

Now I would like to slug the names in the URL so I have changed the route this way:

perfil_miembro:
  url: /miembros/:nombre_apellidos_slug
  class: sfDoctrineRoute
  options: { model: Usuario, type: object}
  param: { module: miembros, action: show}

And I have created these methods:

public function getNombreApellidosSlug()
{

     return Tirengarfio::slugify($this->getNombreApellidos());
}

class Tirengarfio
{
  static public function slugify($text)
  {

    // replace all non letters or digits by -
    $text = preg_replace('/\W+/', '-', $text);

    // trim and lowercase
    $text = strtolower(trim($text, '-'));

    return $text;
  }
}

Now when I click on the name of a member, this URL is shown:

frontend_dev.php/miembros/maria-de-miguel-alvarado

But it always shows the profile of the first member that I have in the fixtures file.

How can I make it work?

Ubuntu 8.04 - Symfony 1.3.

+1  A: 

The slug field needs to be a real column, not a virtual one that you created.
When the url is accessed, doctrine is looking for a single object matching the fields found in the url - in your case, that means no fields, that's why you see the very first record in your table. The query logger should show something like select * from tablename limit 1;.

A note regarding your url: are you sure there will not be multiple people with the same name? If such a collision occures, noone will be able to see the 2nd, 3rd, etc. person's page. I'd include the ID in the url, in the form of /miembros/:id/:slug, that way it stays human readable and surely won't collide.

UPDATE
In the first comment, @Raise suggest using a salted hash of the ID instead of the ID itself. That is better than my original idea of including the ID.
The sfDoctrineGuardPlugin generates a new salt for every user, stores it, and it is used to set/verify the password. You'll need a new fields in your users table for the hash (the salt need not be stored, the ID won't change), and your url will look like /miembros/:hash/:slug.

Maerlyn
+1 for suggesting to 'uniquify' the URL to avoid collisions, although I think some hash of the id and a salt would be better - avoiding exposing the id to the website.Does Doctrine really build a query like that, and return the first row in the DB? That's worrying if true.
Raise
@Raise the hash is a good idea. Regarding the query building, I am a propel user, but according to the symptoms, that's the only way I can imagine. I agree a count may be in order to see how may records match the query, but it's not a must.
Maerlyn
Thanks Maerlyn, you wrote "I'd include the ID in the url,", but that is not a good practice as you can see here (search permalinks) http://www.symfony-project.org/gentle-introduction/1_4/en/09-Links-and-the-Routing-System
I also edited it later to include @Raise's better advice.
Maerlyn
@Raise (hope you read it) both the propel and doctrine routes call `$variables = $this->getRealVariables()`, so they can simply ignore url variables that do not map to DB columns. See line 46 of sfPropelRoute and line 68 of sfDoctrineRoute (version 1.4 of course)
Maerlyn
@Maerlyn i don't fully understand this part: You'll need a new fields in your users table for the hash (the salt need not be stored, the ID won't change),. Can you help me a bit more?
To be able to filter for the hash, you need it to be a field in your table (just like the slug). To be safe, it should also be salted, but you do not need to store the salt, as you won't need to regenerate the hash, just use it. When a new user registers, you generate a salt (a random number will do, or the `time()`), use that to hash the ID, then store the hash in the users table. It it clear now?
Maerlyn
A: 

You can use these options of the routing: options: { model: Usuario, type: object, method: getObjectBySlug }, but you need a getObjectBySlug() method that retrieves your object given a slug. Now you have getNombreApellidosSlug() that makes just the opposite. The problem is that usually, there is no way to know if the slug "maria-martinez" corresponds to user "Maria Martinez", "María Martínez", "MaRiA MaRtInEz" or "María MartíÑez", so that is a problem. You can solve it by having a "slug" column.

My advice is to use the Sluggable behaviour of Doctrine, that takes care of the slug column.

I'm using it on this pet project to do exactly that. Using it is pretty straightforwards:

First, you activate it in the schema:

actAs:
  Timestampable: ~
  Sluggable:
    fields: [name]
    indexName: name_slug
    canUpdate: true
    unique: true

And it will create and mantain a column named "slug".

Then, use it in the routing. In my case is:

list_permalink:
  url: /:slug
  class: sfDoctrineRoute
  options: { model: SkinnyList, type: object, method: getObjectBySlug }
  param: { module: list, action: show }
  requirements: { sf_method: get }

You'll need a method getObjectBySlug in lib/model/doctrine/yourmodelTable.class.php:

  public function getObjectBySlug($options = array())
  {
    if (!isset($options['slug']))
    {
      throw new InvalidArgumentException('The slug is required in the options');
    }
    $q = $this->createQuery('td')->where('td.slug = ?', $options['slug']) ;

    return $q->fetchOne();
  }

In the action, you can retrieve the object by doing:

$this->list = $this->getRoute()->getObject();
nacmartin