viernes, 6 de enero de 2012

i18n Registros vacios

Optional translation form for I18n objects with Symfony and Doctrine

Scaffolding: Not just for construction workers anymore
Creative Commons License photo credit : kevindooley
There are people around here claiming that the Symfony form framework is a gift from the gods. One might say that this statement is a bit exaggerated, but it’s true that the release of the 1.3/1.4 version of the Symfony brought many improvements, and yes, the form framework can save you a lot of time.
In one of my current project, I had to developp some basic CMS-like features. In a back-office, an admin should be able to writes different kind of articles (news, interviews…). Hey That’s a job for inheritance ! But wait, there’s more. Articles can be translated in several languages. Easy, with the Doctrine I18n behavior.
One last requirement : article translations are optional. One might publish an article in english, an other in french, and a third in both languages. This is a little more tricky, so let’s see how to do this.

Some references

Before we start, here are the wonderful ressources I read to get my way out :

Let’s start with the schema

Article:
  actAs:
    Timestampable: ~
    I18n:
      fields: [ title, body ]
      actAs:
        Sluggable: { fields: [ title ], uniqueBy: [ lang, title ] }
 
  columns:
    title: { type: string(255), notnull: true }
    body: { type: clob, notnull: true }
    author: { type: string(255), notnull: false }
 
News:
  inheritance:
    extends: Article
    type: concrete
As it’s friday, I will also give you some fixtures :
News:
  n1:
    author: 'Fantomas'
    Translation:
      fr:
        title: 'OK, l''Ipad est sorti. Vous allez me foutre la paix maintentant ?'
        body: |
          Vous commencez serieusement à me gonfler avec vos articles et vos tweets sur
          une techno inutile et bardée de DRM.
 
  n2:
    author: 'Garcin Fony'
    Translation:
      fr:
        title: 'Sinon, à part ça, ça va ?'
        body: |
          Allez, pour me calmer, je vais me refaire un p'tit café
Let’s build the whole thing, and generate the admin in the same time.
php symfony doctrine:build --all --and-load
php symfony generate:app backend
php symfony doctrine:generate-admin backend News
Check the module admin we’ve just build. Click on the first «  edit  » button. Wait a minute ? The I18n fields are just missing, how are we gonna edit our articles ? If you’re not familiar with the I18n behavior, you must know that the content is split into two tables. The new table, «  article_translation  », holds all the I18n fields.
mysql> SELECT * FROM news;
+----+-------------+---------------------+---------------------+
| id | author      | created_at          | updated_at          |
+----+-------------+---------------------+---------------------+
|  1 | Fantomas    | 2010-01-29 12:14:46 | 2010-01-29 12:14:46 | 
|  2 | Garcin Fony | 2010-01-29 12:14:46 | 2010-01-29 12:14:46 | 
+----+-------------+---------------------+---------------------+
 
mysql> SELECT id, lang, title FROM news_translation;
+----+------+------------------------------------------------------------------+
| id | lang | title                                                            |
+----+------+------------------------------------------------------------------+
|  1 | fr   | OK, l'Ipad est sorti. Vous allez me foutre la paix maintentant ? | 
|  2 | fr   | Sinon, à part ça, ça va ?                                     | 
+----+------+------------------------------------------------------------------+
So, to be able to edit our translations, you have to embed the I18n forms, using the embedI18 function. Let’s do that.
// lib/form/doctrine/NewsForm.class.php
class NewsForm extends BaseNewsForm
{
  /**
   * @see ArticleForm
   */
  public function configure()
  {
    parent::configure();
    $this->embedI18n(array('fr', 'en'));
  }
}
Reload the edit page. Now you can edit your news in french and english. Pretty cool, isn’t it ?

Let’s refactor this mess

Since now, it’s been pretty easy. However, the code we’ve written is bad. It’s bad, for the following reasons :
  1. Each time we will add a new article type (e.g. interview, etc.), we will have to update it’s configure method  ;
  2. Each time we will change the available languages (to add a new one, or disable an existing one), we will have to edit every forms  ;
Fortunately, since Symfony 1.3, the form inheritance follow the model structure. Notice that NewsForm extends BaseNewsForm, which extends ArticleForm.
Let’s refactor our code :
# config/app.yml
all:
  cultures:
    enabled:
      fr: Français
      en: English
// lib/form/doctrine/NewsForm.class.php
 
// Revert the changes we added there
class NewsForm extends BaseNewsForm
{
  /**
   * @see ArticleForm
   */
  public function configure()
  {
    parent::configure();
  }
}
 
// lib/form/doctrine/ArticleForm.class.php
class ArticleForm extends BaseArticleForm
{
  /**
   * Available languages
   *
   * @var array $languages
   **/
  protected $langages;
 
  public function configure()
  {
    $this->languages = sfConfig::get('app_cultures_enabled');
 
    $langs = array_keys($this->languages);
 
    $this->embedI18n($langs);
    foreach($this->languages as $lang => $label)
    {
      $this->widgetSchema[$lang]->setLabel($label);
    }
  }
}
Reload the edit form. Now, no matter how many articles sub-types you add, you still can configure available languages from one place.

Translation edition

Let’s try our brand new admin module by editing a news. If you used the fixtures I provided, you should have a french article, with no english translation. Try to update the french version, and click on the ‘Save’ button.
This is a miserable failure. We can’t save the form, because the english fields are required. We’re stuck, we have to fill every translation for an article before we can save it. Wouldn’t it be nice if we could make a translation form optional ? Let’s add a requirement : if every fields in a translation form are empty, it just should be ignored.
To achieve this goal, we will update the doBind method. Here’s the new ArticleForm class.
// lib/form/doctrine/ArticleForm.class.php
class ArticleForm extends BaseArticleForm
{
  /**
   * Available languages
   *
   * @var array $languages
   **/
  protected $langages;
 
  public function configure()
  {
    $this->languages = sfConfig::get('app_cultures_enabled');
 
    $langs = array_keys($this->languages);
 
    $this->embedI18n($langs);
    foreach($this->languages as $lang => $label)
    {
      $this->widgetSchema[$lang]->setLabel($label);
    }
  }
 
  /**
   * Cleans and binds values to the current form
   *
   * Ignore i18n forms when all their fields are empty
   *
   * @see sfForm::doBind
   **/
  protected function doBind(array $values)
  {
    foreach($this->languages as $lang => $label)
    {
      if($this->embeddedI18nFormIsEmpty($values[$lang]))
      {
        unset(
          $values[$lang],
          $this[$lang]
        );
      }
    }
 
    parent::doBind($values);
  }
 
  /**
   * Check if every fields, except for id and lang, are empty
   **/
  protected function embeddedI18nFormIsEmpty(array $values)
  {
    foreach($values as $key => $value)
    {
      if(in_array($key, array('id', 'lang')))
        continue;
 
      if('' !== trim($value))
      {
        return false;
      }
    }
    return true;
  }
}
In the overidden doBind method, we check, for every I18n form, if some values has been submited. If not, we just unset the corresponding fields.
Reload the edit form, update the french translation, leave the english field empty, and click on ‘Save’. Hurray, the forms is saved correctly.
However, you should keep reading, because you’re gonna have a surprise. Let’s look into the database.
mysql> SELECT id, lang, slug FROM news_translation;
+----+------+--------------------------------------------------------------+
| id | lang | slug                                                         |
+----+------+--------------------------------------------------------------+
|  1 | en   |                                                              | 
|  1 | fr   | ok-l-ipad-est-sorti-vous-allez-me-foutre-la-paix-maintentant | 
|  2 | fr   | sinon-a-part-ca-ca-va                                        | 
+----+------+--------------------------------------------------------------
Something went wrong ! Somewhere in the process, Symfony created an empty translation object for our article. Notice the void «  slug  » field ? If you try to edit another news, you will receive a duplicate content error. WTF ?
I must admit that I don’t fully understand why this happens (sometimes I think I do, and a few minutes later, I realize I don’t). However, here’s the solution to overcome this problem.
// lib/form/doctrine/ArticleForm.class.php
 
  // Add this at the beginnig of the class:
  /**
   * I18n ignored forms
   **/
  protected $I18nFormsIgnored = array();
 
  // update the doBind method:
  /**
   * Unset i18n forms values when every field is empty
   **/
  protected function doBind(array $values)
  {
    foreach($this->languages as $lang => $label)
    {
      if($this->embeddedI18nFormEmpty($values[$lang]))
      {
        $this->I18nFormsIgnored[] = $lang;
        unset(
          $values[$lang],
          $this[$lang]
        );
      }
    }
 
    parent::doBind($values);
  }
 
  // And override the doUpdateObject method:
  /**
   * Updates the values of the object with the cleaned up values.
   *
   * @param  array $values An array of values
   *
   * @see sfFormDoctrine::doUpdateObject()
   */
  protected function doUpdateObject($values)
  {
    parent::doUpdateObject($values);
 
    foreach($this->I18nFormsIgnored as $lang)
    {
      unset($this->object->Translation[$lang]);
      unset($values[$lang]);
    }
  }
Delete the useless line in the mysql database, so we can start on a clean base. Once more, reload the edit form, leave the english translation empty, save the form, et voilà ! The form is saved, the translation is updated, and no empty line is added in the table.

Adding and deleting a translation

Just to be sure, add some data in the english translation title, and try to save the form. You should get a «  body is required  » error, which is the expected behavior. Try to fill correctly the english translation, and save again. Check that the english translation is correctly added in the database. Erase all french fields, save one more time, and check that the french translation is definitly removed from the DB (no dummy entry).
Well, it seems that our form is pretty functional. One last thing : there is still a «  slug  » field in each translation form, that you might want to unset. Easy, just edit the ArticleTranslationForm.class.php file :
// lib/form/doctrine/ArticleTranslationForm.class.php
 
class ArticleTranslationForm extends BaseArticleTranslationForm
{
  public function configure()
  {
    unset($this['slug']);
  }
}
Edit a news. If you’re using Symfony 1.{3,4}.1, you’ll see that the damn slug field is still there. It’s a Symfony bug. Indeed, the translations forms don’t follow the inheritance schema. Notice that BaseNewsTranslationForm extends BaseFormDoctrine instead of ArticleNewsTranslation.
You’ll have to manually change that, however, this will be overidden each time you rebuild your forms.
// lib/form/doctrine/base/BaseNewsTranslationForm.class.php
// Replace
abstract class BaseNewsTranslationForm extends BaseFormDoctrine
 
// with
abstract class BaseNewsTranslationForm extends ArticleTranslationForm
 
// lib/form/doctrine/NewsTranslationForm.class.php
// Edit the configure method
class NewsTranslationForm extends BaseNewsTranslationForm
{
  public function configure()
  {
    parent::configure();
  }
}
Now, the slug field should disapear. At the present moment, the bug is still open.

Let’s test everything

As it’s friday, I feel generous, so as a bonus, I will give you the functional tests to cover our magical form. Enjoy.
// lib/sfBackendTestFunctional.class.php
/**
 * This class is used to run functional tests in a secured backend
 *
 * Takes care of the login action, and the fixtures loading
 */
class sfBackendTestFunctional extends sfTestFunctional
{
  public function __construct($browser, $lime = null, $testers = array())
  {
    parent::__construct($browser, $lime, $testers);
  }
 
  /**
   * Perform user authentication
   *
   * @param   array of String         $user_data
   * @return  sfGuardTestFunctional   $this
   */
  public function signin($user_data)
  {
   return $this->info(sprintf('Login as "%s"', $user_data['username']))->
     get('/login')->
     click('sign in', array('signin' => $user_data), array('_with_csrf' => true))->
 
     with('form')->begin()->
       hasErrors(false)->
     end()->
 
     with('user')->begin()->
       isAuthenticated(true)->
     end()->
 
     with('request')->begin()->
       isParameter('module', 'sfGuardAuth')->
       isParameter('action', 'signin')->
     end()->
 
     with('response')->begin()->
       isRedirected()->
       followRedirect()->
       end()
    ;
  }
  /**
   * Load project fixtures
   **/
  public function loadData()
  {
    Doctrine::loadData(sfConfig::get('sf_data_dir').'/fixtures');
    return $this;
  }
}
// test/functional/backend/newsActionsTest.php
 
include(dirname(__FILE__).'/../../bootstrap/functional.php');
 
$browser = new sfBackendTestFunctional(new sfBrowser());
$browser->loadData();
$browser->setTester('doctrine', 'sfTesterDoctrine');
 
$languages = sfConfig::get('app_cultures_enabled');
 
$news = Doctrine::getTable('News')->
  createQuery('n')->
  leftJoin('n.Translation t')->
  andWhere('t.lang = ?', 'fr')->
  orderBy('n.id', 'asc')->
  fetchOne();
 
$editUrl = sprintf('/news/%s/edit', $news->getId());
 
$browser->
  // Uncomment this if your backend is protected by a login
  //signin(array(
  //  'username' => 'admin',
  //  'password' => 'admin'
  //)))->
  get('/news')->
  with('response')->begin()->
    isStatusCode(200)->
  end()->
 
  info('1 - News list')->
  with('response')->begin()->
    checkElement('h1', '/News List/')->
    checkElement('body', '/Fantomas/')->
  end()->
 
  info('2 - News edit')->
  info('  2.1 - I18n forms for all languages are embedded')->
  get($editUrl)->
  with('response')->begin()->
    isStatusCode(200)->
    checkElement('input[name$="[title]"]', count($languages))->
  end()->
 
  info('  2.2 - I18n forms are validated')->
  get($editUrl)->
  click('Save', array('news' => array(
    'en' => array(
      'title' => 'toto',
      'body' => ''
    ))
    ), array('_with_csrf' => true)
  )->
  with('form')->begin()->
    hasErrors(1)->
    isError('en[body]', 'required')->
  end()->
 
  info('  2.3 - Empty forms are validated')->
  get($editUrl)->
  click('Save', array('news' => array(
    'en' => array(
      'title' => '',
      'body' => ''
    ))
    ), array('_with_csrf' => true)
  )->
  with('form')->begin()->
    hasErrors(0)->
  end()->
 
  info('  2.4 - Translations are updated')->
  get($editUrl)->
  click('Save', array('news' => array(
    'fr' => array(
      'title' => 'toto tutu tata',
      'body' => 'riri fifi loulou'
    ))
    ), array('_with_csrf' => true)
  )->
  with('form')->begin()->
    hasErrors(0)->
  end()->
 
  with('doctrine')->begin()->
    check('NewsTranslation', array(
      'id' => $news->getId(),
      'lang' => 'fr',
      'title' => 'toto tutu tata'
    ))->
    info('  2.5 - No empty translation is created')->
    check('NewsTranslation', array(
      'id' => $news->getId(),
      'lang' => 'en',
    ), false)->
  end()->
 
  info('  2.6 - New translations can be added')->
  get($editUrl)->
  click('Save', array('news' => array(
    'en' => array(
      'title' => 'toto tutu tata',
      'body' => 'riri fifi loulou'
    ))
    ), array('_with_csrf' => true)
  )->
 
  with('form')->begin()->
    hasErrors(0)->
  end()->
 
  with('doctrine')->begin()->
    check('NewsTranslation', array(
      'id' => $news->getId(),
      'lang' => 'en',
      'title' => 'toto tutu tata'
    ))->
  end()->
 
  info('  2.7 - Existing translations can be deleted')->
  get($editUrl)->
  click('Save', array('news' => array(
    'fr' => array(
      'title' => '',
      'body' => ''
    ))
    ), array('_with_csrf' => true)
  )->
  with('form')->begin()->
    hasErrors(0)->
  end()->
 
  with('doctrine')->begin()->
    check('NewsTranslation', array(
      'id' => $news->getId(),
      'lang' => 'fr',
    ), false)->
  end()
;
Here we are. Hope you find this helpful. I thought some kind of requirement would be pretty common, however, I didn’t find any directly related howto’s. If you can think of any other way to achieve this, please, let me know.

miércoles, 24 de agosto de 2011

Call to a member function remove() on a non-object


Si estas usando el cache system de Symfony, debes tener un problema similar a  Call to a member function remove() on a non-object, eso siginifica que getViewCacheManager retorna null.

Seguro has estado mas de 2 dias googleando y llegaste aqui !

El problema es que cuando estas trabajando en el entorno .dev el cache no esta activo, por eso no funciona sfContext::getViewCacheManager()

Solo debes activarlo en settings.yml

   dev:
     .settings:
       error_reporting:                                                         
       web_debug:              true
       cache:                  true
Eso es todo!

Fuente: http://www.pbtg.com/blogs/jim/2009/07/15/symfony-cache-gotcha

Variables en Symfony

[sf_symfony_lib_dir] 
    [sf_root_dir] 
    [sf_apps_dir] 
    [sf_lib_dir] 
    [sf_log_dir] 
    [sf_data_dir] 
    [sf_config_dir]
    [sf_test_dir]
    [sf_plugins_dir]
    [sf_web_dir]
    [sf_upload_dir]
    [sf_cache_dir]
    [sf_app_base_cache_dir]
    [sf_app_cache_dir]
    [sf_template_cache_dir]
    [sf_i18n_cache_dir]
    [sf_config_cache_dir]
    [sf_test_cache_dir]
    [sf_module_cache_dir]
    [sf_app]
    [sf_environment]
    [sf_debug]
    [sf_app_dir]
    [sf_app_config_dir]
    [sf_app_lib_dir]
    [sf_app_module_dir]
    [sf_app_template_dir]
    [sf_app_i18n_dir]
    [sf_error_404_module]
    [sf_error_404_action]
    [sf_login_module]
    [sf_login_action]
    [sf_secure_module]
    [sf_secure_action]
    [sf_module_disabled_module]
    [sf_module_disabled_action]
    [sf_use_database]
    [sf_i18n]
    [sf_compressed]
    [sf_check_lock]
    [sf_csrf_secret]
    [sf_escaping_strategy]
    [sf_escaping_method]
    [sf_no_script_name]
    [sf_cache] 
    [sf_etag]
    [sf_web_debug]
    [sf_error_reporting]
    [sf_file_link_format]
    [sf_admin_web_dir]
    [sf_web_debug_web_dir]
    [sf_standard_helpers]
    [sf_enabled_modules]
    [sf_charset]
    [sf_logging_enabled]
    [sf_default_culture]
 
print_r(sfConfig::getAll());

Valores por defecto en formularios symfony

Para poner un valor por defecto al cargar un formulario, nada tan sencillo como poner en el actions:
$this->form->setDefault(’nombrecampo’, ‘valorcampo’);

Fuente: http://www.informaticosinmemoria.com/2009/10/valores-por-defecto-en-formularios-symfony/

Para ver el error en un al grabar un formulario


if ($form->isValid()){
     $form->save();
}else {
     echo $form->getErrorSchema()->getMessage();
}

¿ Que módulo o acción se está ejecutando?


Para saber el modulo que se está ejecutando:
$this->getModuleName()

Y para saber la acción:
$this->getActionName()


Fuente: http://www.informaticosinmemoria.com/2009/11/%c2%bf-que-modulo-o-accion-se-esta-ejecutando/

Mejorando el selector de culturas de Symfony

Extraído de Practical Symfony – Día 19: Internacionalización y Localización:
Pero la mayor parte del tiempo, tu sitio web no estará disponible en los 136 principales idiomas. El método getPreferredCulture() devuelve el mejor lenguaje mediante la comparación de los idiomas preferidos del usuario y los idiomas de tu sitio web:
// in an action
$language = $request->getPreferredCulture(array('en', 'fr'));
Sí, pero si quieres hilar fino en tus aplicaciones pronto descubrirás que se queda corto con el método getPreferredCulture():
/**
   * Returns the preferred culture for the current request.
   *
   * @  array  $cultures  An array of ordered cultures available
   *
   * @ string The preferred culture
   */
  public function getPreferredCulture(array $cultures = null)
  {
    $preferredCultures = $this->getLanguages();
 
    if (null === $cultures)
    {
      return isset($preferredCultures[0]) ? $preferredCultures[0] : null;
    }
 
    if (!$preferredCultures)
    {
      return $cultures[0];
    }
 
    $preferredCultures = array_values(array_intersect($preferredCultures, $cultures));
 
    return isset($preferredCultures[0]) ? $preferredCultures[0] : $cultures[0];
  }
O lo que viene a ser lo mismo: Devuélveme el primero que exista, el primero de los definidos en el navegador o nada en absoluto.
Aunque resulte raro, en la mayoría de los casos este nivel de complejidad es el suficiente ya que Symfony arreglará cualquier imprecisión que pueda darse si este mecanismo falla. No hay que olvidar que en el fichero settings.yml debemos definir una cultura por defecto para nuestra aplicación.
Pero, ¿qué pasa si un usuario configura su navegador para español de México e inglés (en ese orden de prioridad) y mi aplicación admite español (genérico) e inglés? El algoritmo anterior nos devolverá como preferido el inglés erróneamente. Según la configuración del navegador, se debería presentar un texto en español (aunque no sea en su variedad mexicana) y si esto no fuera posible, una traducción en inglés.
Es decir, los códigos de 5 carácteres de culturas como es_MX especifican una variedad del español pero no excluyen la cultura es a secas.
Por lo tanto, propongo el siguiente método para realizar una selección más precisa y configurable:
/**
   * Makes the best choice of culture. It can check the domain of preferred
   * languages in order to choose a more generic language if it's available.
   *
   * Example:
   * 
   * preferred cultures: es_ES, en
   * available cultures: es, en, fr
   * 
   * In this example this method will return 'es' over 'en' as the best choice
   * if $checkCultureDomains flag is enabled.
   *
   * If no preferred culture is available this method will return the
   * $defaultCulture.
   * 
   * @ array $availableCultures Array of valid available cultures
   * @ array $preferredCultures Array of preferred cultures. Order DOES matter (lower index => higher priority)
   * @ string $defaultCulture A default culture if none of the preferred cultures is available
   * @ bool $checkCultureDomains Flag to enable/disable language domain check
   * @ string best choice culture
   */
  public static function bestChoiceCulture($availableCultures, $preferredCultures, $defaultCulture, $checkCultureDomains = true) {
    foreach ($preferredCultures as $culture) {
      if (in_array($culture, $availableCultures)) {
        return $culture;
      }
      if ($checkCultureDomains) {
        $domain = substr($culture, 0, 2);
        if ($domain != $culture && in_array($domain, $availableCultures)) {
          return $domain;
        }
      }
    }
    return $defaultCulture;
  }
El único aspecto sobre el que dudo en este método es sobre la corrección de pasarle un valor por defecto a la función para ser devuelto si el algoritmo no encuentra una cultura apropiada. Qué se yo, prefiero dejarlo así por darle coherencia y atomicidad al método cuando lo use por ahí. Si molesta, se puede quitar y reemplazarlo por un return null o incluso una excepción. Al gusto del consumidor.
Happy coding!

viernes, 1 de julio de 2011

No puedo iniciar sesion como Cliente de Terminal Server

Seguro a muchos le han dado el mismo problema...

  1. Debemos añadir a los usuarios a "usuarios de escritorio remoto", 
  2. Abrir las politicas del sistema (gpedit.msc) 
  3. Ir a la ruta:
    1. Configuración del Equipo >
    2. Configuración de Windows >
    3. Configuración de Seguridad >
    4. Directivas Locales >
    5. Asignación de Derechos de Usuarios
  4. Y en la entrada "Permitir inicio de sesión a través de servicios de terminal server" 
  5. Añadí tanto al grupo de usuarios de escritorio remoto como a los que me interesan
  6. Aceptar y Aplicar todas las veces que sean necesarias para cerrar todo.
Una solución muy útil y un poco rebuscada para habilitar el acceso por Terminal Server.

Fuente: http://foro.ethek.com/viewtopic.php?f=7&t=9844#p54527