martedì 27 aprile 2010

symfony, gestire un profilo utente con sfGuard

Ad oggi la gestione utenti di Lyra è quasi inesistente. Viene utilizzato sfDoctrineGuardPlugin per proteggere con login il backend dell'applicazione, ma esiste di fatto un solo utente, il super-amministratore, creato automaticamente con il caricamento dei dati di esempio (fixtures). Usando le funzioni di sfDoctrineGuardPlugin possono già essere aggiunti nuovi utenti e gruppi, ma non venendo gestiti permessi specifici, ogni nuovo utente può operare senza alcuna restrizione, risultando a tutti gli effetti pratici indistinguibile dal super-amministratore.

Inoltre per ogni utente possono essere salvate solo le informazioni di base gestite dalla classe sfGuardUser: nome utente e password. Questo è il primo aspetto da migliorare con la creazione di un profilo utente che ci consenta di gestire informazioni aggiuntive: nome, cognome ed indirizzo e-mail; almeno per il momento, altri campi potranno essere aggiunti in seguito se necessario.

Ho seguito questa guida pubblicata sul blog ufficiale di symfony.

Per prima cosa si crea nello schema una classe per il profilo utente

config/doctrine/schema.yml

LyraUserProfile:
  tableName: users
  options:
    collate: utf8_unicode_ci
    charset: utf8
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    user_id:
      type: integer(4)
    first_name:
      type: string(80)
    last_name:
      type: string(80)
    email:
      type: string(150)
  relations:
    User:
      class: sfGuardUser
      local: user_id
      foreign: id
      type: one
      foreignType: one
      foreignAlias: Profile
      onDelete: CASCADE

L'impostazione 'onDelete: CASCADE' non è presente nella guida: credo sia una svista perché altrimenti l'esistenza della relazione rende impossibile cancellare un utente dal backend.

Si modificano le fixtures per includere le informazioni del profilo negli utenti creati con i dati di esempio, nel mio caso il super-amministratore

data/fixtures/users.yml

sfGuardUser:
  admin:
    username:       admin
    password:       admin
    is_super_admin: true

#User profile

LyraUserProfile:
  p_admin:
    first_name: Lyra
    last_name: Administrator
    email: lyra@localhost
    User: admin

Si ricostruiscono le classi del modello e si ricaricano i dati di esempio con

./symfony doctrine:build --all --and-load

La sintassi del comando è quella da usare in symfony 1.4.

A questo punto si deve personalizzare il form per la creazione e modifica di un utente nel backend in modo da poter inserire le informazioni del profilo. La guida citata all'inizio dice di copiare in lib/form/doctrine il file sfGuardUserAdminForm.class.php che si trova nella cartella plugins/sfDoctrineGuardPlugin/lib/form/doctrine e procedere con la personalizzazione della copia. In questo modo un successivo aggiornamento del plugin non sovrascriverà le nostre modifiche. Il problema è che facendo in questo modo a me la cosa non funziona, in particolare si verificano errori nei test funzionali. Infatti durante i test l'autoload carica la classe sfGuardUserAdminForm originale (quella nella cartella del plugin) e non la copia modificata nella cartella lib/form/docrine del progetto.

Non sono stato tanto ad indagare ed ho preso una strada leggermente diversa: ho creato una classe LyraUserAdminForm derivata da sfGuardUserAdminForm

lib/form/doctrine/LyraUserAdminForm.class.php

class LyraUserAdminForm extends sfGuardUserAdminForm
{
  public function configure()
  {
    parent::configure();

    $profileForm = new LyraUserProfileForm($this->object->Profile);
    unset($profileForm['id'], $profileForm['user_id']);
    $this->embedForm('user_profile', $profileForm);
    $this->widgetSchema['user_profile']->setLabel(false);
  }
}

Il form con le informazioni del profilo viene inizializzato con l'oggetto ottenuto dalla relazione definita nello schema (Profile, foreignAlias della relazione User di LyraUserProfile) e poi incluso nel form principale con embedForm().

Bisogna poi modificare generator.yml perché venga utilizzata la nostra classe al posto di sfGuardUserAdminForm e per visualizzare in un 'pannello' il form embedded (user_profile) con le informazioni del profilo.

apps/backend/modules/sfGuardUser/config/generator.yml
...
     form:
        class: LyraUserAdminForm
        display:
          "NONE": [username, password, password_again]
          PANEL_PROFILE: [user_profile]
          PANEL_PERMISSIONS: [is_active, is_super_admin, groups_list, permissions_list]
...

Dopo aver modificato questo file ho dovuto anche rimuovere (in realtà ne ho commentato il contenuto) il file generator.yml originale installato dal plugin in plugins/sfDoctrineGuardPlugin/modules/sfGuardUser/config/. Diversamente è impossibile personalizzare alcune opzioni di configurazione. Mi sembra di ricordare qualche post nel forum di symfony dove veniva fatto notare questo problema. La cosa seccante è la necessità di ripetere l'operazione ad ogni aggiornamento del plugin, ma soluzioni alternative non ne ho trovate.

Alla fine ho creato i file di lingua per la traduzione dell'interfaccia. Tutto il lavoro fatto è visibile nel log delle modifiche della revisione 52.

8 commenti:

Anonimo ha detto...

il problrema sui test funzionali ce l'avevamo anche noi cavolo...salvata mezza giornata :) grazie

Massimo ha detto...

Di nulla. Non sono riuscito capire il motivo del problema, una volta aggirato mi resta comunque il dubbio di sbagliare qualcosa.

L. ha detto...

Ciao,
ti seguo da un po' ed ho preso interessanti spunti per qualche progettino.

Ti faccio una domanda: hai idea di come fare se si ha la necessità di utilizzare più tabelle per i profili? Es: utente e redattore con campi assolutamente differenti.

Massimo ha detto...

Magari potresti mantenere le informazioni comuni a tutti gli utenti (nome, e-mail) in un profilo base e poi creare modelli con le informazioni aggiuntive legati al gruppo utente.

Oppure utilizzare l'ereditarietà 'column aggregation' di Doctrine con un campo tipo utente nel profilo principale. Bisognerebbe provare, il fatto che con la 'column aggregation' verrebbe creata sul db una tabella unica per tutti i profili non mi sembra l'ideale in un caso come questo.

L. ha detto...

la seconda opzione l'avevo presa in considerazione e scartata considerando la disomogeneità dei campi.

Inoltre mi piacerebbe che funzionasse in modo tale da avere una tabella profilo utente legata a ciascun gruppo o permesso. Posso generare in questo caso prima l'anagrafica e dopo l'account di accesso passando l'id del cliente e l'id del gruppo/permesso sulla base della tipologia dell'utente. Mentre per recuperare le informazioni del profilo posso riscrivere il metodo getProfile().

Come ti sembra?

Massimo ha detto...

Sì, considera che in un esempio semplice come il mio, getProfile() funziona semplicemente grazie alla relazione definita nello schema, tu dovresti scrivere un metodo nel modello e ritornare l'oggetto corrispondente al profilo dell'utente determinato in base al gruppo/permesso.

L. ha detto...

Ciao,
oggi ho lavorato un po' sul suggerimento che mi avevi dato la settimana passata.

In pratica ho aggiunto dei metodi alla classe sfGuardUser in modo da individuare la tipologia dell'utente e da recuperare il profilo corrispondente individuandolo tra differenti tabelle.

Pero' per migliorare l'interfaccia mi sono imbattuto in un problema: non riesco ad utilizzare nell'action il setDefault di un campo con l'embedform che hai sviluppato nel tuo post. Ho anche aperto un topic sul forum ufficiale (http://forum.symfony-project.org/index.php/t/27906/). Mi sai aiutare?

p.s. scusami se ti stresso.

Massimo ha detto...

Mai provato, ma questo forse funziona:

$this->form->getEmbeddedForm('nome_embedded')->setDefault(...)

nome_embedded è il nome che hai usato quando hai fatto l'embedding nel configure() del form principale.

Se no passa il valore del parametro anagrafica_id come option al primo form e nel configure() lo recuperi e lo imposti sul form embedded.

Posta un commento

Nota. Solo i membri di questo blog possono postare un commento.