https://d226lax1qjow5r.cloudfront.net/blog/blogposts/phone-numbers-in-rails-with-normalizes-and-generated-columns/phone-numbers_rails.png

Numbers de teléfono en Rails con columnas normalizadas y generadas

Publicado el January 11, 2024

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 1 para EE.UU. o 44 para el Reino Unido)

  • Para el + o el código de acceso internacional (por ejemplo 00) que debe omitirse

  • Para que se omita el código de acceso a la troncal (p. ej. 0) se omita

  • Para 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:

Screenshot of a rendered Rails 'users/new' view template showing an input form with First Name, Last Name, Email, Phone Country Code, and Phone Number input fieldsRendered 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-67

Aquí 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#delete mé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 rango 0-9 (es decir, todos los caracteres no numéricos).

  • El método String#delete_prefix método. Al igual que el método delete este 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étodo delete del 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:

  1. 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 un 0 a 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). 0 no 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 utilizan 1 como código de acceso. También hay otros casos atípicos: algunos países utilizan 8Hungría utiliza 06México 01y Mongolia utiliza 01 o 02.

    Algunos casos extremos aún más delicados son Italia (junto con San Marino y Ciudad del Vaticano), que utilizó utilizaba 0 como prefijo de acceso troncal, pero ahora incluye que 0 como parte del número para tanto interno y marcación internacional, y Grecia que utiliza 2 para los teléfonos fijos y 6 para los números móviles. Para hacer frente a todos estos casos extremos, un enfoque más robusto para nuestra normalizes en producción podría ser utilizar condicionalmente diferentes en función del código de país introducido.

  2. El objetivo de normalizes es 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 User clase

  • Tenemos código en nuestra clase User que define en parte international_phone_numbercuando lo ideal sería mantener este tipo de lógica en nuestras ActiveRecord migraciones.

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 :virtual columna, en este caso un :string

  • :as. Especifica cómo debe generarse el valor de la columna. En este caso, concatenamos los valores phone_country_code y phone utilizando el operador || de PostgreSQL.

  • :stored. Se trata de un valor booleano que determina si el valor de la columna :virtual columna 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:

https://a.storyblok.com/f/270183/373x376/e8d3211236/karl-lingiah.png
Karl LingiahDefensor del desarrollador Ruby

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.