Vamos a hacer uso del atributo DATA de HTML5 para crear dos selects anidados. En algunas ocasiones necesitamos saber que tipo de dato hemos seleccionado en el primer desplegable en lugar del valor en sí. Podría darse el caso de un primer select con razas de animales y el segundo (que cambiará según lo que hemos seleccionado en el primero) con comida para diferentes tipos de animal.
Creo que es más claro con un ejemplo:
1 2 3 4 5 6 | <select id="animales"> <option value="1" data-type="perro">Caniche</option> <option value="2" data-type="gato">Siamés</option> <option value="3" data-type="gato">Persa</option> <option value="3" data-type="perro">Pastor alemán</option> </select> |
En el segundo despegable mostraremos los diferentes tipos de comida según si hemos seleccionado un perro o un gato. En este caso nos da igual la raza, solo queremos saber que tipo de animal es.
Como se puede ver, he añadido el atributo data-type a cada option del select con el tipo de animal que es. Este valor personalizado lo podemos obtener fácilmente con jQuery tal que así:
1 2 3 4 5 6 7 8 | $(function(){ $('animales').change(function(){ var seleccionado = $(this).find('option:selected'); // 'type' es lo que va a continuación del guión en data-type var animal = seleccionado.data('type'); ... }); }); |
También podemos acceder a este valor con Javascript básico:
1 2 3 | var animales = document.getElementById('animales'); var seleccionado = animales.options[animales.selectedIndex]; var animal = seleccionado.getAttribute('data-type'); |
Ahora ya podremos generar el segundo desplegable con los diferentes tipos de comida según el tipo de animal que hemos seleccionado.
Más información sobre los atributos DATA de HTML5: HTML5 Custom Data Attributes (data-*)
Etiquetas: anidado, atributos data, desplegable, html5, javascript, jquery, select
En las entidades de Symfony2 es sencillo relacionar un campo con otra tabla con una relación ManyToOne y que este campo solo pueda tomar un valor que esté disponible en la segunda entity. Si creamos un formulario para poder introducir nuevos registros en la base de datos, necesitamos un campo del tipo select para seleccionar uno de los datos de la segunda tabla y relacionarlos.
Deberemos modificar el formulario y añadirle un par de opciones más. En este ejemplo, vamos a relacionar un usuario con su país.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | namespace Acme\MiBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class PerfilType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder->add('pais', 'entity', array( 'class' => 'AcmeMiBundle:Pais' 'label' => 'Pais', ) ); } } |
Que no se nos olvide añadir el método __toString() a la entity Pais:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | namespace Acme\MiBundle\Entity; use Doctrine\ORM\Mapping as ORM; class Pais { [...] public function __toString() { return $this->getNombre(); } } |
Y así de sencillo, ahora en nuestra plantilla aparecerá automáticamente el campo de tipo select a la hora de seleccionar el país del usuario.
Etiquetas: AbstractType, entity, formularios, select, Symfony2
Trabajando con arrays en javascript, una función bastante interesante con la que contamos es con indexOf() para obtener la posición de la primera ocurrencia del valor especificado en un array.
Casualmente, esta función no está disponible para arrays en Internet Explorer 8 y anteriores pero si que está disponible para utilizar con strings.
Con este sencillo “hack” podremos contar con esta función cuando manejemos arrays.
1 2 3 4 5 6 7 8 9 10 | if(!Array.indexOf){ Array.prototype.indexOf = function(obj){ for(var i=0; i<this.length; i++){ if(this[i]==obj){ return i; } } return -1; } } |
He creado dos bundles, uno llamado UserBundle y otro ProfileBundle. El bundle UserBundle es totalmente independiente y reutilizable en cualquier proyecto (contando con funcionalidades tales como registro, login, logout, recordar contraseña, confirmar usuario mediante email, etc.) con la única pega de que solamente se guardan el email y la contraseña del usuario. Hay proyectos que solo requieren estos datos y no merece la pena “ensuciarlo” con más.
El bundle ProfileBundle subsana esta limitación y otorga al usuario un perfil con su nombre, apellidos, etc. El registro se hace desde el UserBundle y, una vez logueado en la aplicación, completas tu perfil de usuario mediante el ProfileBundle.
Al terminar de completar tu perfil y pulsar el botón guardar, el usuario logueado se recarga con los nuevos datos introducidos. Al hacer esto, me aparecía el siguiente error:
You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.
Y en otras ocasiones también se mostraba este otro error:
Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken::serialize() must return a string or NULL
La solución es sencilla y solo hay que fijarse en el mensaje de error. Si el usuario se va a recargar una vez ya está logueado, la entidad Usuario debe implementar la clase Serializable y definir sus métodos serialize() y unserialize().
En diferentes blogs que hablan sobre Symfony2, en los métodos serialize y unserialize hacen uso del método getUsername() lo cual puede ser un error. Lo muestro con un ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class User implements UserInterface, \Serializable { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $email * * @ORM\Column(name="email", type="string", length=255, unique=true) * @Assert\Email() */ private $email; [...] function getUsername() { return $this->getEmail(); } public function serialize() { return serialize($this->getUsername()); } public function unserialize($data) { $this->email = unserialize($data); } } |
En el ejemplo anterior vemos que la propiedad $email no es la clave primaria y, aunque hayamos marcado que es única, nos seguirá apareciendo el error anterior ya que Doctrine necesita que el refresh del usuario se haga mediante su identificador único (en algún caso podría llegar a ser el email, pero no en este ejemplo).
Solamente tendremos que cambiar el código anterior por este:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class User implements UserInterface, \Serializable { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string $email * * @ORM\Column(name="email", type="string", length=255, unique=true) * @Assert\Email() */ private $email; [...] function getUsername() { return $this->getEmail(); } public function serialize() { return serialize($this->getId()); } public function unserialize($data) { $this->id = unserialize($data); } } |
El código anterior se puede optimizar y se podrían añadir más propiedades a los métodos serialize y unserialize (se podrían utilizar en conjunto las propiedades id, email, etc.).
Etiquetas: bundle, doctrine2, EntityUserProvider, error, serializable, serialize, Symfony2, unserialize, UserInterface
He estado desarrollando mi propio bundle para el registro de usuarios en Symfony2 y me he encontrado con el problema de querer añadir campos al formulario de registro que no he definido en el modelo. Uno de estos campos es un checkbox para aceptar los términos de uso de la aplicación. No me interesa guardar este campo en la base de datos por lo que no está definido en el modelo.
Así es como lo he hecho:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class RegisterFormType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder ->add('email', 'email') ->add('password', 'repeated', array( 'type' => 'password', 'invalid_message' => 'Las dos contraseñas deben coincidir', 'options' => array('label' => 'Contraseña'), 'required' => false)) ->add("accept_tos", "checkbox", array( "property_path" => false, ) ); $builder ->addValidator(new CallbackValidator(function(FormInterface $form){ if (!$form["accept_tos"]->getData()) { $form->addError(new FormError('Debes aceptar los términos de uso')); } }) ); } } |
Como vemos, añadimos un campo checkbox con nombre “accept_tos” y le indicamos la opción “property_path” a false, por lo que no validará este campo con el modelo. Eso sí, debemos añadir un validador para este campo para asegurarnos de que el usuario acepta las condiciones de uso.
Fuente: http://www.richsage.co.uk/2011/07/20/adding-non-entity-fields-to-your-symfony2-forms/
Etiquetas: addValidator, buildForm, bundle, CallbackValidator, formularios, property_path, Symfony2
Con motivo del concurso #BigBangApps convocado por la web Ideas4All en que proponían desarrollar una aplicación utilizando su API, me puse a programar una aplicación para Android de la web. La API de Ideas4All devuelve los datos en XML (hubiese preferido en JSON pero de momento no lo permite) por lo que el “gran secreto” de la app es leer correctamente este tipo de archivos.
Hay varias formas de leer archivos XML, siendo una de ellas mediante SAX (Simple API for XML). En este post voy a explicar los pasos para leer/parsear correctamente un archivo XML disponible en Internet.
El fichero de ejemplo que queremos leer es el listado de categorías:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?xml version="1.0" encoding="UTF-8"?> <categories type="array"> <category> <id type="integer">6</id> <name>Vida/Salud</name> </category> <category> <id type="integer">3</id> <name>Sostenibilidad</name> </category> ... ... </categories> |
Lo primero que necesitamos es una clase que utilizaremos para guardar los datos de cada categoría. El objetivo es devolver una lista de objetos del tipo categoría, donde se encontraran todas las categorías leídas del XML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class ParsedCategoryDataSet { private String id = null; private String name = null; public String getId() { return id; } public void setId(String extractedString) { this.id = extractedString; } public String getName() { return name; } public void setName(String extractedString) { this.name = extractedString; } public String toString(){ return "name = " + this.name; } } |
Como podemos observar, solamente se trata de una clase con setters y getters para almacenar los datos de cada categoría.
Ahora pasamos a crear la clase con la que parsearemos el archivo XML mediante SAX en Android.
El modelo SAX lee secuencialmente el fichero XML y ejecuta varios métodos (que podemos controlar) por cada elemento leído. Estos métodos son:
- startDocument(): comienza a leer el XML.
- endDocument(): ha terminado de leer XML.
- startElement(): ha encontrado el comienzo de una etiqueta XML.
- endElement(): ha encontrado el cierre de una etiqueta XML.
- characters(): texto que ha encontrado entre el comienzo y el cierre de una etiqueta
Estos métodos se encuentran en la clase org.xml.sax.helpers.DefaultHandler, de la que heredaremos nuestro propio parseador de categorías:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import java.util.Vector; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class CategoryHandler extends DefaultHandler{ public CategoryHandler() { super(); this.myParsedCategoryDataSet = new Vector<ParsedCategoryDataSet>(); } // Variables de control para saber cuando estamos en el interior de cada etiqueta @SuppressWarnings("unused") private boolean in_category = false; private boolean in_id = false; private boolean in_name = false; // En esta variable guardamos el texto encontrado entre las etiquetas StringBuilder builder; // Aquí guardamos cada objeto categoria private ParsedCategoryDataSet DataSet; // Vector donde se guardaran todas las categorías encontradas private Vector<ParsedCategoryDataSet> myParsedCategoryDataSet; public Vector<ParsedCategoryDataSet> getParsedCategoryDataSets() { return this.myParsedCategoryDataSet; } public Vector<ParsedCategoryDataSet> getParsedData() { return this.myParsedCategoryDataSet; } @Override public void startDocument() throws SAXException { // Comenzamos a leer el fichero xml, creamos el vector donde se guardarán las categorías this.myParsedCategoryDataSet = new Vector<ParsedCategoryDataSet>(); } @Override public void endDocument() throws SAXException { // Ha terminado de leer el fichero, en este paso no hacemos nada } @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if (localName.equals("category")) { // Ha encontrado la etiqueta principal de cada elemento "category" // Creamos un nuevo objeto categoría donde iremos guardando los datos this.in_category = true; DataSet = new ParsedCategoryDataSet(); }else if (localName.equals("id")) { // Estamos dentro de la etiqueta "id", creamos el StringBuilder que utilizaremos // en el método characters para guardar el contenido this.in_id = true; builder = new StringBuilder(); }else if (localName.equals("name")) { // Estamos dentro de la etiqueta "name", creamos el StringBuilder que utilizaremos // en el método characters para guardar el contenido this.in_name = true; builder = new StringBuilder(); } } @Override public void endElement(String namespaceURI, String localName, String qName) throws SAXException { if (localName.equals("category")) { // Hemos llegado al final de la etiqueta principal de cada elemento "category" // Añadimos al vector el elemento leído this.in_category = false; myParsedCategoryDataSet.add(DataSet); }else if (localName.equals("id")) { // Ha encontrado la etiqueta de cierre de "id" this.in_id = false; }else if (localName.equals("name")) { // Ha encontrado la etiqueta de cierre de "name" this.in_name = false; } } @Override public void characters(char ch[], int start, int length) { // Si estamos dentro de la etiqueta "id" if(this.in_id){ if (builder!=null) { for (int i=start; i<start+length; i++) { // Añadimos al StringBuilder (definido al encontrar el comienzo de la etiqueta "id") // lo que haya entre las etiquetas de inicio y fin builder.append(ch[i]); } } // Lo asignamos al "id" del objeto categoría (DataSet) DataSet.setId(builder.toString()); } // Si estamos dentro de la etiqueta "id" if(this.in_name){ if (builder!=null) { for (int i=start; i<start+length; i++) { // Añadimos al StringBuilder (definido al encontrar el comienzo de la etiqueta "name") // lo que haya entre las etiquetas de inicio y fin builder.append(ch[i]); } } // Lo asignamos al "name" del objeto categoría (DataSet) DataSet.setName(builder.toString()); } } } |
El código está comentado y creo que se explica bastante bien. Me consta que algunos programadores leen el contenido que se encuentra entre las etiquetas de inicio y cierre, cuando llegan a esta última etiqueta en lugar de en el método characters. Yo me he encontrado con muchos problemas haciéndolo así ya que se corre el riesgo de no conseguir leer todos los caracteres que realmente hay. La propia documentación del método lo explica:
The Parser will call this method to report each chunk of character data. SAX parsers may return all contiguous character data in a single chunk, or they may split it into several chunks; however, all of the characters in any single event must come from the same external entity so that the Locator provides useful information.
Ya tenemos casi todo lo necesario para leer el fichero XML de la API. Solamente nos queda implementar el método que parseará el archivo utilizando nuestra clase.
Esto solo es el extracto del método que lee el XML, encontraréis el código completo de la activity aquí, donde también se hace uso de la combinación de ProgressDialog y Thread para mostrar un mensaje de “cargando” mientras se leen los datos (este tema da para otro post
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | ... ... private Vector<ParsedCategoryDataSet> categories; private String categories_url = "http://url_de_la_api/categories.xml"; ... ... public void loadCategories() { try { // Url del archivo XML URL url = new URL(categories_url); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); // Utilizamos nuestro propio parseador (CategoryHandler) CategoryHandler myExampleHandler = new CategoryHandler(); xr.setContentHandler(myExampleHandler); InputSource is = new InputSource(url.openStream()); // Le indicamos la codificación para evitar errores is.setEncoding("UTF-8"); xr.parse(is); // Asignamos al vector categories los datos parseados categories = myExampleHandler.getParsedData(); } catch (Exception e) { // Ha ocurrido algún error Log.e("Ideas4All", "Error", e); } } |
Y eso es todo, no olvidéis indicar en el AndroidManifest.xml solicitar permisos de acceso a Internet.
<uses-permission android:name="android.permission.INTERNET"></uses-permission>Podéis encontrar la aplicación completa en github:
Código fuente de la aplicación de Ideas4All para Android
Etiquetas: api, github, ideas4all, leer, parsear, progressdialog, sax, thread, xml
Un detalle de Sublime Text 2 bastante incómodo cuando estás programando en PHP, es que cuando hacemos doble click en una variable para seleccionarla, solamente selecciona el nombre de la variable excluyendo el símbolo dolar ($).
La solución es muy sencilla, solo debemos ir a:
Preferences / File Settings - Defaults
Allí encontraremos la línea:
"word_separators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?",Solamente debemos quitar el símbolo $ de la línea anterior, dejándolo así:
"word_separators": "./\\()\"'-:,.;<>~!@#%^&*|+=[]{}`~?",Si ahora cerramos y volvemos a abrir el editor, ya se seleccionará la variable completa.
Truco encontrado aquí:
http://www.sublimetext.com/forum/viewtopic.php?f=3&t=2704
Etiquetas: php, sublime text 2, variables
Una de las cosas más habituales cuando desarrollamos una aplicación para Android, es poder “enviar” datos (variables, objetos, etc.) desde una Activity a otra. En términos generales, una Activity es una “pantalla” de nuestra aplicación.
Para realizar esta tarea, Android pone a nuestra disposición los llamados “Intent“. En general, utilizaremos un Intent cuando queramos movernos de una actividad a otra, permitiéndonos a su vez pasar datos desde la Activity en la que estamos hacia la nueva.
1 2 3 4 5 6 | // Pasaremos de la actividad actual a OtraActivity Intent intent = new Intent(this, OtraActivity.class); intent.putExtra("variable_integer", objeto.getId()); intent.putExtra("variable_string", objeto.getNombre()); intent.putExtra("objeto_float", objeto.getPrecio()); startActivity(intent); |
- En la segunda línea creamos un Intent, al que se le pasa como parámetros la actividad actual (this) y la actividad a la que queremos pasar (en este caso OtraActivity.class).
- En las tres líneas siguientes definimos tres variables de tres tipos diferentes. En el primer caso, estaremos creando una variable llamada “variable_integer” con el contenido de objeto.getId(). Esta variable se “envía” a OtraActivity y podremos recuperar su contenido allí para utilizarlo.
- En la última línea, iniciamos la nueva actividad pasando como parámetro el Intent que hemos creado.
El código que viene a continuación es el que se ejecutará en la actividad OtraActivity. Se muestran dos formas de recuperar las variables, se puede utilizar la que más convenga en cada ocasión.
1 2 3 4 5 6 7 8 9 10 11 12 | // Estamos en OtraActivity int recuperamos_variable_integer = getIntent().getIntExtra("variable_integer"); String recuperamos_variable_string = getIntent().getStringExtra("variable_string"); float recuperamos_variable_float = getIntent().getFloatExtra("objeto_float"); // O también de esta otra forma // Estamos en OtraActivity Bundle datos = this.getIntent().getExtras(); int recuperamos_variable_integer = datos.getInt("variable_integer"); String recuperamos_variable_string = datos.getString("variable_string"); float recuperamos_variable_float = datos.getFloat("objeto_float"); |
Es importante que sepamos de que tipo son las variables que enviamos en el Intent, ya que deberemos asignar esos valores al tipo de variable correcto.
Etiquetas: activity, Android, bundle, datos, getExtras, getIntent, intent, variables
Sublime Text 2 es un editor de texto que se está haciendo muy popular debido a su facilidad de uso y su entorno minimalista. Sublime Text 2 hace que solo te concentres en el código que estás escribiendo.
Si nos descargamos el programa desde su propia web, descubrimos que al descomprimirlo se trata de una carpeta donde se encuentra todo lo necesario para ejecutar el programa. Cada vez que queramos abrirlo, debemos entrar en esta carpeta y ejecutar el archivo de la aplicación (también podemos crear un acceso directo en el escritorio). En esta carpeta no encontraremos ni siquiera el icono de Sublime Text 2.
¿Cómo solucionarlo?
- Primero nos descargamos el icono del programa para que el lanzador quede “bonito”
- Creamos un enlace de la aplicación a la carpeta /usr/bin/:
sudo ln -s /home/jonseg/sublime-text-2/sublime_text /usr/bin/sublime_text- Copiamos el icono que nos hemos descargado antes al directorio donde se encuentran los iconos del sistema:
sudo cp sublime.png /usr/share/icons/
- Creamos el archivo sublime.desktop con el siguiente contenido:
[Desktop Entry] Version=2.0 Type=Application Name=Sublime Text 2 Comment=Editor Sublime Text 2 TryExec=sublime_text Exec=sublime_text %F Icon=/usr/share/icons/sublime.png MimeType=text/plain;
Y lo copiamos al directorio /usr/share/applications/:
sudo cp sublime.desktop /usr/share/applications/
Y listo. Si pulsamos la tecla de Windows de nuestro teclado (en Unity), el resultado debe ser algo parecido a esto:
Etiquetas: Desarrollo, editor, ide, programación, sublime text, sublime text 2
Netbeans es mi IDE de desarrollo favorito cuando estoy programando en PHP. Podemos mejorar nuestra productividad utilizando Netbeans haciendo uso de sus numerosos atajos de teclado que nos facilitará el trabajo. Aunque sé que hay bastantes más (y por supuesto para otros lenguajes), voy a intentar enumerar los más útiles que conozco. Si vosotros conocéis alguno más, no dudéis en escribir un comentario y lo añadiré al post.
Vamos a ver a continuación unos atajos para escribir código mucho más rápido con Netbeans. Tan solo tendremos que escribir la primera palabra que se indica y pulsar la tecla del tabulador.
foreach ($array as $value) { }
foreach ($array as $key => $value) { }
if (true) { }
switch ($categoriesLlistat) { case $value: break; default: break; }
while (true) { }
while ($row = mysql_fetch_array($query)) { }
while ($row = mysql_fetch_object($query)) { }
while ($row = mysql_fetch_row($query)) { }
También se pueden generar atajos de código definidos por nosotros mismos. Ese es un tema que da para otro post
Etiquetas: atajos, netbeans, php, phpdoc, productividad, programación, teclado