
Compartir:
Karl es un defensor de los desarrolladores para Vonage, centrado en el mantenimiento de nuestros SDK de servidor Ruby y la mejora de la experiencia de los desarrolladores para nuestra comunidad. Le encanta aprender, hacer cosas, compartir conocimientos y, en general, todo lo relacionado con la tecnología web.
Numbers de teléfono en Rails con columnas normalizadas y generadas
Tiempo de lectura: 11 minutos
Si estás creando una aplicación que utiliza las API de Vonage Communicationses probable que en algún momento tengas que trabajar con números de teléfono de alguna manera. Ya sea que tu aplicación esté realizando llamadas telefónicas automatizadas de texto a voz a través de la Voice APIo el envío de mensajes SMS o WhatsApp mediante la Messages APIo implementando la autenticación de dos factores con la API Verify APItendrá que especificar un número de teléfono de destino para estas interacciones.
Numbers de téléphone internationaux
Como era de esperar, los puntos finales de la API requieren que los números de teléfono tengan un formato específico y coherente. El formato de número formato de número utilizado por las API de Vonage sigue el estándar internacional E.164y espera:
Un prefijo internacional (por ejemplo
1para EE.UU. o44para el Reino Unido)Para el
+o el código de acceso internacional (por ejemplo00) que debe omitirsePara que se omita el código de acceso a la troncal (p. ej.
0) se omitaPara que el propio número excluya cualquier carácter especial (por ejemplo
()o-)
Por ejemplo, un número estadounidense tendría el formato 14155550101y un número del Reino Unido tendría el formato 447700900123.
Un escenario típico cuando se crean aplicaciones web es que la fuente de los datos de los números de teléfono sea la entrada del usuario capturada como parte de un flujo de trabajo de registro. Asegurarse de que los datos introducidos están en el formato correcto para su posterior uso con la API externa puede plantear algunos problemas. Sin embargo, si estás creando una aplicación Ruby on Rails, un par de funciones añadidas recientemente pueden ayudarte a afrontar estos retos.
Ejemplo de flujo de trabajo
Antes de entrar en detalle, pensemos un poco más en el flujo de trabajo de registro. En una aplicación Ruby on Rails, probablemente definamos un modelo User y un modelo UsersControllerjunto con las plantillas de vista necesarias. Nuestra plantilla users/new una vez renderizada, podría tener este aspecto:
Rendered Rails 'users/new' view template screenshot
Obsérvese que en este ejemplo el código de país del teléfono y el número de teléfono son campos de formulario independientes, por lo que en una implementación estándar de Rails se persistirán en la base de datos como atributos independientes de nuestro modelo. Por ejemplo, podríamos esperar que nuestro modelo UsersController contenga código del tipo
class UsersController < ApplicationController
def create
@user = User.new(user_params)
# rest of create action code
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :phone_country_code, :phone)
end
end
Nosotros podríamos combinar ambos datos en una sola entrada de nuestro formulario, pero eso haría recaer más responsabilidad en el usuario a la hora de introducir los datos correctamente y podría dificultar la limpieza de los datos en el back-end. Tener el código de país del teléfono como su propio campo de formulario nos permite restringir los datos a un conjunto específico de valores, por ejemplo con un menú de selección. Hay varias formas de implementar este campo, que no voy a detallar aquí, pero asegúrese de consultar la gema countries gema y country_select gema.
En una aplicación Rails, lo más probable es que utilicemos ayudantes de formulario Rails en nuestras plantillas de vista, y en el caso del campo Phone Number será probablemente el ayudante telephone_field helper (o su alias, phone_field). En la vista renderizada, este ayudante crea un elemento HTML <input> elemento con un atributo type con el valor tel. Aparte del valor de type (que puede ser utilizado por los navegadores de teléfonos móviles para determinar que debe mostrarse un teclado numérico), este campo es funcionalmente equivalente a un campo de tipo text (de hecho, en los navegadores que no admiten telse convertirá en una entrada de tipo text estándar).
A diferencia de otros tipos de entrada (p. ej. email o url), los navegadores no soportan ningún tipo de validación automática en este tipo de campo. Por supuesto, podemos proporcionar sugerencias de formato al usuario junto con el campo de entrada, y quizás podríamos utilizar el atributo pattern atributo en el elemento para restringir la entrada o utilizar algún tipo de solución basada en JavaScript. Sin embargo, como desarrolladores responsables, no deberíamos completamente Como desarrolladores responsables, no deberíamos confiar completamente en el usuario final o en la validación del front-end: siempre es prudente limpiar la entrada del usuario antes de transferirla a una base de datos. En el contexto de la introducción de números de teléfono, y sobre todo cuando se trata de dar soporte a una base de clientes global, esto significa estar preparado para una gran variedad de posibles formatos de números. A continuación se muestran algunos ejemplos de formatos de números de teléfono utilizados en todo el mundo:
77 12 3000
012345-60000
(01234) 500000
06-10000000
0123-400-000
01234/500000-67Aquí es donde Ruby on Rails normalizes método puede resultar muy útil.
Normaliza
El método normalizes se añadió en Rails 7.1. El método se incluye en ActiveRecord::Base a través del módulo Normalization y por tanto está disponible para todos los modelos Rails que hereden de la clase abstracta ApplicationRecord clase abstracta. En su uso, el método requiere una lista de args de uno o más :names (que equivalen a los atributos que se desea normalizar) y un argumento de palabra clave :with cuyo valor es una lambda de Ruby cuyo argumento representa el atributo a normalizar.
A continuación se muestra cómo podría ser el uso de normalize si se aplica a nuestro escenario de números de teléfono:
class User < ApplicationRecord
normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
En el ejemplo anterior :phone es una cadena, así que dentro de la lambda estamos usando dos métodos de la clase String para transformar el valor de entrada:
El método
String#deletemétodo. Este método devuelve una copia del objeto de cadena al que se llamó, con todos los caracteres indicados por el argumento selector eliminados. Aquí, mediante el uso del signo de intercalación^el selector indica que todos los caracteres no en el rango0-9(es decir, todos los caracteres no numéricos).El método
String#delete_prefixmétodo. Al igual que el métododeleteeste método devuelve una copia del objeto de cadena al que se llamó, pero en este caso sólo elimina los caracteres al inicio de la cadena que coinciden con la cadena pasada como argumento. Aquí eliminará el carácter'0'(si está presente) de la cadena devuelta por la invocación del métododeletedel método.
Con la normalizes añadida a nuestra clase User podemos probar rápidamente nuestra implementación en la consola de Rails utilizando el método normalize_value_for método:
$ rails console
Loading development environment (Rails 7.1.0)
3.1.0 :001 > User.normalize_value_for(:phone, "07900-00-00-00")
=> "7900000000"
3.1.0 :002 > User.normalize_value_for(:phone, "07900 000 000")
=> "7900000000"
3.1.0 :002 > User.normalize_value_for(:phone, "(07900) 000/000")
=> "7900000000"
Era posible realizar este tipo de transformación de datos antes de Rails 7.1 y la incorporación del método normalizes pero la implementación habría sido un poco más complicada.
Una opción sería utilizar un Llamada de retorno de Active Record como por ejemplo before_save:
class User < ApplicationRecord
before_save :sanitize_phone
private
def sanitize_phone
phone&.delete("^0-9")&.delete_prefix("0")
end
end
Otra opción sería anular el método setter:
class User < ApplicationRecord
def phone=(value)
super(value&.delete("^0-9")&.delete_prefix("0"))
end
end
Sin embargo, el método normalizes es más sencillo y limpio de implementar, especialmente si necesita transformar varios atributos de la misma forma:
class User < ApplicationRecord
normalizes :home_phone, :work_phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
Con los otros dos enfoques tendríamos que anular varios setters o definir varios métodos de ayuda.
Obsérvese también el uso del operador operador de navegación segura & en esas otras implementaciones. Esto le dice a Ruby que se salte una llamada a un método si el invocador es nily su uso aquí evita que se produzca un error NoMethodError en una situación en la que el atributo :phone era nil. Debido a la forma en que funciona el operador de navegación segura, tenemos que acordarnos de incluirlo para cada llamada al método en la cadena. Con normalizesobtenemos este comportamiento de forma gratuita por defecto. Es decir, si :phone es nilla lambda no se ejecutará en absoluto.
Advertencias
Un par de advertencias sobre nuestro ejemplo normalizes implementación:
No cubre cada situaciones o casos extremos que pueden darse en producción. Por ejemplo, la parte
delete_prefix("0")está ahí porque muchos países anteponen un0a sus números de teléfono como código de acceso cuando se marca dentro del país (lo que se conoce como acceso troncal), pero no es necesario cuando se marca internacionalmente (en ese caso se utiliza un código de salida internacional delante del código de acceso).0no es necesario cuando se marca internacionalmente (en cuyo caso se utiliza un código de salida internacional delante del código internacional del país al que se llama).Por desgracia, no todos los países siguen el mismo planteamiento. En primer lugar, no todos los países exigen un código de acceso troncal. De los que sí lo exigen, la mayoría utiliza
0pero también hay un número significativo de países (EE.UU. incluido) que utilizan1como código de acceso. También hay otros casos atípicos: algunos países utilizan8Hungría utiliza06México01y Mongolia utiliza01o02.Algunos casos extremos aún más delicados son Italia (junto con San Marino y Ciudad del Vaticano), que utilizó utilizaba
0como prefijo de acceso troncal, pero ahora incluye que0como parte del número para tanto interno y marcación internacional, y Grecia que utiliza2para los teléfonos fijos y6para los números móviles. Para hacer frente a todos estos casos extremos, un enfoque más robusto para nuestranormalizesen producción podría ser utilizar condicionalmente diferentes en función del código de país introducido.El objetivo de
normalizeses garantizar que los datos se ajustan a un formato específico antes de transferirlos a una base de datos. Lo que no no es comprobar si el número es válido o fiable. Para ello, es posible que desee ver la integración de la Number Insight API de Vonage como parte de tu flujo de trabajo de inscripción (aunque eso está fuera del alcance de este artículo).
A efectos de este artículo, vamos a suponer que estamos satisfechos con nuestros datos. Ahora que tenemos nuestros datos, ¿cómo podemos utilizar los datos? Esto nos lleva a la segunda funcionalidad de Ruby on Rails que quiero tratar: Columnas generadas.
Columnas generadas
Tenemos almacenados los datos de nuestros números de teléfono, pero :phone_country_code y :phone son atributos separados de nuestro modelo User modelo. La migración de nuestro modelo User modelo podría ser algo como esto:
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
t.string :phone_country_code
t.string :phone
t.timestamps
end
end
end
Al crear o actualizar un Userestablecemos y accedemos a esos atributos por separado:
user = User.new(phone_country_code: "44", phone: "7900000001")
user.phone_country_code # => "44"
user.phone # => "7900000001"
En usar esos datos con las API de Vonage Communications, por ejemplo para enviar un SMS mediante Messages APInecesitamos combinar el código de país y el número de teléfono:
message = Vonage::Messaging::Message.sms(message: "Hello from Vonage!")
Vonage.messaging.send(
to: "447900000001",
from: "Vonage",
**message
)(Nota: El ejemplo anterior utiliza el Vonage Ruby SDK a través del Vonage Rails Initializer)
Una opción sería combinar los datos en el punto de uso:
user = User.find_by(id: 1)
message = Vonage::Messaging::Message.sms(message: "Hello from Vonage!")
Vonage.messaging.send(
to: "#{user.phone_country_code}#{user.phone}",
from: "Vonage",
**message
)Una desventaja de este enfoque es que podríamos utilizar estos datos combinados en varios lugares de nuestra base de código, y tendríamos que repetir este patrón con cada uso de los datos. Si queremos mantener nuestro código DRY, un enfoque mejor sería proporcionar una manera de acceder a los datos ya combinados, por ejemplo como international_phone_number.
user = User.find_by(id: 1)
message = Vonage::Messaging::Message.sms(message: "Hello from Vonage!")
Vonage.messaging.send(
to: user.international_phone_number,
from: "Vonage",
**message
)Una forma de hacerlo sería definir un método en nuestro User que combine ambos valores, por ejemplo
class User < ApplicationRecord
# rest of User code
def international_phone_number
phone_country_code + phone
end
end
En lugar de definir métodos getter adicionales en nuestra clase User sería más limpio si pudiéramos consultar los datos directamente desde nuestra base de datos.
Antes de Rails 7.0, una forma de conseguirlo era definir una columna international_phone_number en nuestra tabla users y utilizar un callback before_save que crea un atributo adicional en el objeto y calcula su valor antes de transferirlo a la base de datos:
class User < ApplicationRecord
before_save :set_international_phone_number
private
def set_international_phone_number
self[:international_phone_number] = phone_country_code + phone
end
end
De este modo, podremos consultar esa columna siempre que sea necesario:
user = User.create!(phone_country_code: "44", phone: "7900000001")
user.international_phone_number => "447900000001"
Aparte de parecer un poco "chapucero", este planteamiento tiene otros inconvenientes:
Añade código repetitivo a nuestra
UserclaseTenemos código en nuestra clase
Userque define en parteinternational_phone_numbercuando lo ideal sería mantener este tipo de lógica en nuestrasActiveRecordmigraciones.
Desde Rails 7.0 existe una solución más limpia a este problema: las columnas generadas. Una columna generada es una especie de columna virtual cuyo valor se deriva del valor de otras columnas. A diferencia del ejemplo anterior, toda la lógica de las columnas generadas se encuentra en la base de datos.
Este concepto no es nuevo. MySQL tiene Columnas Virtuales desde MySQL 5.7, y muchos otros RDBMS tienen funcionalidades equivalentes. Sin embargo, si estamos creando una aplicación Rails, lo más probable es que utilicemos PostgreSQL en la capa de persistencia de datos. PostgreSQL ha añadido Columnas generadas en PostgreSQL 12.0y en la versión Rails 7.0.0 actualizó el adaptador PostgreSQL de Rails para darles soporte.
Veamos cómo podemos utilizar las columnas generadas como parte de nuestra implementación.
Para añadir una columna international_phone_number columna generada a nuestra tabla users existente, tendremos que crear una migración para definirla:
class AddInternationalPhoneNumberToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :international_phone_number, :virtual, type: :string,
as: "phone_country_code || phone", stored: true
end
end
Los tres primeros argumentos posicionales del método add_column método son los argumentos estándar requeridos para ese método concreto en una migración Rails. Tenemos un table_name de :users (que es la tabla que queremos actualizar), un argumento column_name de :international_phone_numbery un argumento type de :virtual. A esto le sigue un options con tres claves específicas para definir la columna :virtual columna:
:type. Es el tipo de datos de la columna:virtualcolumna, en este caso un:string:as. Especifica cómo debe generarse el valor de la columna. En este caso, concatenamos los valoresphone_country_codeyphoneutilizando el operador||de PostgreSQL.:stored. Se trata de un valor booleano que determina si el valor de la columna:virtualcolumna debe calcularse cuando se escriben en las columnas utilizadas para generarlos (true), o cuando la propia columna virtual se consulta (false).
Una advertencia sobre el uso de columnas generadas es que las columnas de las que se deriva el valor de la columna generada deben estar definidas en la misma tabla que la columna generada. misma tabla que la propia columna generada.
Conclusión
Como hemos visto, el uso de datos de números de teléfono en nuestras aplicaciones puede plantear problemas, especialmente cuando se requieren formatos internacionales específicos para esos datos. Sin embargo, como desarrolladores de Ruby on Rails tenemos a nuestra disposición muchas funcionalidades útiles, como el método normalizes y las columnas generadas, que pueden hacer nuestro código más limpio y nuestra vida más fácil.
Quizás uses una de estas funciones en tu próxima aplicación Rails para desarrollar algo increíble con las API de Vonage Communications. Si es así, puedes inscribirte para obtener una cuenta gratuita de Vonage Developer Account para comenzar a desarrollar de inmediato. Si tienes alguna pregunta o simplemente quieres conversar, no dudes en unirte a nosotros en Twitter o en Vonage Developer Slack. Feliz programación y ¡hasta la próxima!
Compartir:
Karl es un defensor de los desarrolladores para Vonage, centrado en el mantenimiento de nuestros SDK de servidor Ruby y la mejora de la experiencia de los desarrolladores para nuestra comunidad. Le encanta aprender, hacer cosas, compartir conocimientos y, en general, todo lo relacionado con la tecnología web.