
Compartilhar:
Karl é um Developer Advocate da Vonage, com foco na manutenção de nossos SDKs de servidor em Ruby e na melhoria da experiência dos desenvolvedores da nossa comunidade. Ele adora aprender, criar coisas, compartilhar conhecimento e tudo o que esteja relacionado à tecnologia da web em geral.
Números de telefone no Rails com normalização e colunas geradas
Tempo de leitura: 11 minutos
Se você estiver desenvolvendo um aplicativo que utilize as APIs de comunicação da Vonage, em algum momento você provavelmente precisará trabalhar com números de telefone de alguma forma. Seja seu aplicativo realizando chamadas automáticas com conversão de texto em fala por meio da Voice API, enviando mensagens de SMS ou pelo WhatsApp usando a Messages API, ou implementando a autenticação de dois fatores com a Verify API, você precisará especificar um número de telefone de destino para essas interações.
Números de telefone internacionais
Como era de se esperar, os endpoints da API exigem que os números de telefone estejam em um formato específico e consistente. O formato de número usado pelas APIs da Vonage segue o padrão internacional E.164e exige que:
Um código de discagem internacional (por exemplo,
1para os EUA ou44para o Reino Unido)Para o código de acesso local
+ou código de acesso internacional (por exemplo,00) seja omitidoPara o código de acesso ao porta-malas (por exemplo,
0) seja omitidoPara que o próprio número não contenha caracteres especiais (por exemplo,
()ou-)
Por exemplo, um número dos EUA teria o formato 14155550101, e um número do Reino Unido teria o formato 447700900123.
Um cenário comum ao desenvolver aplicativos web é que a fonte dos dados do seu número de telefone seja uma informação fornecida pelo usuário, capturada como parte do fluxo de trabalho de cadastro. Garantir que os dados inseridos estejam no formato correto para uso posterior com a API externa pode apresentar alguns desafios. No entanto, se você estiver desenvolvendo uma aplicação em Ruby on Rails, alguns recursos adicionados recentemente podem ajudar bastante a lidar com esses desafios.
Exemplo de fluxo de trabalho
Antes de entrarmos em detalhes sobre quais são esses recursos, vamos refletir um pouco mais sobre esse fluxo de trabalho de cadastro. Em um aplicativo Ruby on Rails, provavelmente definiremos um User modelo e uma UsersController, juntamente com os modelos de visualização necessários. Nosso users/new modelo, quando renderizado, pode ficar mais ou menos assim:
Rendered Rails 'users/new' view template screenshot
Observe que, neste exemplo, “Código de país do telefone” e “Número de telefone” são campos de formulário separados e, portanto, em uma implementação padrão do Rails, serão armazenados no banco de dados como atributos distintos do nosso modelo. Por exemplo, poderíamos esperar que nosso UsersController contivesse um código semelhante ao seguinte:
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
Nós poderíamos combinar os dois dados em um único campo de entrada no nosso formulário, mas isso aumentaria a responsabilidade do usuário em inserir os dados corretamente e, possivelmente, tornaria mais difícil a limpeza dos dados no back-end. Ter o “Código de país do telefone” como um campo de formulário separado nos permite restringir os dados a um conjunto específico de valores, por exemplo, com um menu suspenso. Existem várias maneiras de implementarmos esse campo, que não vou detalhar aqui, mas não deixe de conferir o countries gem e country_select gem.
Em uma aplicação Rails, provavelmente usaremos os helpers de formulário do Rails em nossos modelos de visualização e, no caso do próprio campo “Número de telefone”, esse provavelmente será o telephone_field auxiliar (ou seu alias, phone_field). Na visualização renderizada, esse helper cria um elemento HTML <input> elemento com um type atributo com o valor de tel. Além do valor de type (que pode ser usado por navegadores de celulares para determinar que um teclado numérico deve ser exibido), esse campo é funcionalmente equivalente a um text campo de tipo (na verdade, em navegadores que não suportam tel, ele será substituído por um text ).
Ao contrário de alguns outros tipos de entrada (por exemplo, email ou url), os navegadores não oferecem suporte a nenhum tipo de validação automática para esse tipo de campo. É claro que podemos fornecer dicas de formatação ao usuário ao lado do campo de entrada e, talvez, usar o pattern atributo no elemento para restringir a entrada ou utilizar algum tipo de solução baseada em JavaScript. No entanto, como desenvolvedores responsáveis, não devemos confiar totalmente contar com o usuário final ou com a validação do front-end — é sempre prudente limpar a entrada do usuário antes de armazená-la em um banco de dados. No contexto da entrada de números de telefone, e particularmente ao atender a uma base global de clientes, isso significa estar preparado para uma grande variedade de possíveis formatos de números que podem ser inseridos. Abaixo estão apenas alguns exemplos de diferentes formatos de números de telefone usados ao redor do mundo:
77 12 3000
012345-60000
(01234) 500000
06-10000000
0123-400-000
01234/500000-67É aqui que entra o Ruby on Rails normalizes pode pode ser muito útil.
Normaliza
O normalizes método foi adicionado no Rails 7.1. O método está incluído ActiveRecord::Base através do Normalization módulo e, portanto, está disponível para todos os nossos modelos do Rails que herdam da ApplicationRecord classe abstrata. Ao ser utilizado, o método requer uma lista de argumentos com um ou mais :names (que correspondem aos atributos que você deseja normalizar) e um :with argumento-chave, cujo valor é uma função lambda em Ruby, sendo que o argumento da lambda representa o atributo a ser normalizado.
Veja a seguir como seria a aplicação de normalize pode parecer se aplicado ao nosso cenário de número de telefone:
class User < ApplicationRecord
normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
No exemplo acima, :phone é uma string; portanto, dentro da função lambda, estamos usando dois métodos da classe String para transformar o valor de entrada:
O
String#deletemétodo. Esse método retorna uma cópia do objeto string no qual foi chamado, com todos os caracteres indicados pelo argumento seletor removidos. Aqui, por meio do uso do símbolo de inserção^, o seletor indica que todos os caracteres que não estejam estão no intervalo0-9devem ser removidos (ou seja, todos os caracteres não numéricos).O
String#delete_prefixmétodo. Semelhante aodelete, esse método retorna uma cópia do objeto string no qual foi chamado, mas, neste caso, remove apenas os caracteres no início da string que correspondam à string passada como argumento. Aqui, ele removerá o caractere'0'(se estiver presente) da string retornada peladeletechamada do método.
Com a normalizes invocação adicionada à nossa User classe, podemos testar rapidamente nossa implementação no Console do Rails, usando o 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 possível realizar esse tipo de transformação de dados antes ao Rails 7.1, e com a adição do normalizes , mas a implementação teria sido um pouco mais complexa.
Uma opção seria usar um callback do Active Record como before_save:
class User < ApplicationRecord
before_save :sanitize_phone
private
def sanitize_phone
phone&.delete("^0-9")&.delete_prefix("0")
end
end
Outra opção seria sobrescrever o método setter:
class User < ApplicationRecord
def phone=(value)
super(value&.delete("^0-9")&.delete_prefix("0"))
end
end
A normalizes abordagem, porém, é mais simples e mais clara de implementar, especialmente se você precisar transformar vários atributos da mesma maneira:
class User < ApplicationRecord
normalizes :home_phone, :work_phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
Com as outras duas abordagens, precisaríamos sobrescrever vários setters ou definir vários métodos auxiliares.
Observe também o uso do operador de navegação segura & nessas outras implementações. Isso instrui o Ruby a ignorar uma chamada de método se o chamador for nil, e seu uso aqui evita que uma NoMethodError seja lançada em uma situação em que o :phone atributo fosse nil. Devido à forma como o operador de navegação segura funciona, precisamos nos lembrar de incluí-lo para cada chamada de método na cadeia. Com normalizes, obtemos esse comportamento automaticamente por padrão. Ou seja, se :phone for nil, a lambda não será executada de forma alguma.
Advertências
Algumas observações sobre nosso exemplo normalizes de implementação:
Isso não abrange todas as situação ou caso extremo que você possa encontrar em produção. Por exemplo, a
delete_prefix("0")parte está lá porque muitos países acrescentam um0aos números de telefone como código de acesso ao discar internamente dentro do país (conhecido como acesso à linha principal), mas isso0não é necessário ao fazer chamadas internacionais (quando, em vez disso, usa-se um código de saída internacional antes do código do país de destino).Infelizmente, nem todos os países adotam a mesma abordagem. Em primeiro lugar, nem todos os países exigem um código de acesso à rede troncal. Entre aqueles que o fazem, a maioria usa
0, mas há também um número significativo de países (incluindo os EUA) que utilizam1como código de acesso à rede externa. Há também alguns outros casos atípicos: alguns países usam8, a Hungria usa06, o México usa01, e a Mongólia usa01ou02.Alguns casos-limite ainda mais complexos são a Itália (junto com San Marino e a Cidade do Vaticano), que utilizavam a usar
0como prefixo de acesso à rede principal, mas agora inclui isso0como parte do número para ambos interna e internacionais, e a Grécia, que utiliza2para telefones fixos e6para números de celular. Para lidar com todos esses casos extremos, uma abordagem mais robusta para nossanormalizesimplementação em produção poderia ser usar condicionalmente diferentes lambdas com base no código do país que foi inserido.O objetivo
normalizesé garantir que seus dados estejam em conformidade com um formato específico antes de serem gravados em um banco de dados. O que ele não faz é verificar se o número é válido e/ou confiável. Para isso, você pode considerar integrar a API Number Insight API como parte do seu fluxo de trabalho de cadastro (embora isso esteja além do escopo deste artigo).
Para os fins deste artigo, porém, vamos supor que estamos satisfeitos com nossos dados neste momento. Agora que já temos nossos dados, como podemos usá-los ? Isso nos leva ao segundo recurso do Ruby on Rails que quero abordar: as colunas geradas.
Colunas geradas
Temos os dados dos nossos números de telefone armazenados, mas :phone_country_code e :phone são atributos separados do nosso User modelo. A migração para o nosso User modelo pode ser algo assim:
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
Ao criar ou atualizar um User, definimos e acessamos esses atributos separadamente:
user = User.new(phone_country_code: "44", phone: "7900000001")
user.phone_country_code # => "44"
user.phone # => "7900000001"
Quando usar esses dados com as APIs de comunicação da Vonage, por exemplo, para enviar um SMS usando a Messages API, precisamos combinar o código do país e o número de telefone:
message = Vonage::Messaging::Message.sms(message: "Hello from Vonage!")
Vonage.messaging.send(
to: "447900000001",
from: "Vonage",
**message
)(Observação: O exemplo acima utiliza o SDK do Vonage para Ruby por meio do Inicializador Vonage para Rails)
Uma opção seria combinar os dados no momento do 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
)Uma desvantagem dessa abordagem é que poderíamos estar usando esses dados combinados em vários pontos do nosso código e precisaríamos repetir esse padrão com cada uso dos dados. Se quisermos manter nosso código DRY, uma abordagem melhor seria fornecer uma maneira de acessar os dados já combinados, por exemplo, 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
)Uma maneira de fazer isso seria definir um método em nosso User modelo que combine os dois valores, por exemplo:
class User < ApplicationRecord
# rest of User code
def international_phone_number
phone_country_code + phone
end
end
Em vez de definir métodos getter adicionais em nossa User classe, talvez fosse mais simples se pudéssemos, de alguma forma, simplesmente consultar os dados diretamente do nosso banco de dados.
Antes do Rails 7.0, uma maneira de fazer isso seria definir uma international_phone_number coluna em nossa users tabela e, em seguida, usar um before_save callback que criasse um atributo adicional no objeto e calculasse seu valor antes de gravá-lo no banco de dados:
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
Assim, poderíamos consultar essa coluna sempre que necessário:
user = User.create!(phone_country_code: "44", phone: "7900000001")
user.international_phone_number => "447900000001"
Além de parecer um pouco “improvisada”, essa abordagem apresenta outras desvantagens:
Isso adiciona código padrão à nossa
UserclasseTemos um código em nossa
Userclasse que define parcialmenteinternational_phone_number, quando, idealmente, gostaríamos de manter esse tipo de lógica em nossasActiveRecordmigrações.
Desde o Rails 7.0, existe uma solução mais simples para esse problema: as colunas geradas. Uma coluna gerada é um tipo de coluna virtual cujo valor é derivado do valor de outras colunas. Ao contrário do exemplo acima, toda a lógica das colunas geradas está contida no nível do banco de dados.
Esse conceito não é novo. O MySQL conta com colunas virtuais desde a versão 5.7, e vários outros sistemas de gerenciamento de bancos de dados relacionais (RDBMS) possuem funcionalidades equivalentes. No entanto, se você estiver desenvolvendo uma aplicação Rails, é provável que venha a utilizar o PostgreSQL na camada de persistência de dados. O PostgreSQL adicionou Colunas Geradas na PostgreSQL 12.0, e a versão Rails 7.0.0 atualizou o adaptador PostgreSQL do Rails para oferecê-las.
Vamos ver como podemos usar as colunas geradas como parte da nossa implementação.
Para adicionar uma international_phone_number coluna gerada à nossa users , precisaremos criar uma migração para defini-la:
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
Os três primeiros argumentos posicionais do add_column método são os argumentos padrão obrigatórios para esse método específico em uma migração do Rails. Temos um table_name de :users (que é a tabela que queremos atualizar), um column_name de :international_phone_number, e um type de :virtual. Em seguida, vem um options hash com três chaves específicas para definir a :virtual coluna:
:type. Este é o tipo de dados da:virtualcoluna; neste caso, um:string:as. Isso especifica como o valor da coluna deve ser gerado. Neste caso, estamos concatenando ophone_country_codeephonecampos usando o||.:stored. Trata-se de um valor booleano que determina se o valor da:virtualcoluna deve ser calculado quando os dados forem gravados nas colunas usadas para gerá-la (true), ou quando a própria coluna virtual for consultada (false).
Uma ressalva ao uso de colunas geradas é que as colunas das quais o valor da coluna gerada é derivado devem estar definidas na mesma tabela que a própria coluna gerada.
Conclusão
Como vimos, pode haver desafios associados ao uso de dados de números de telefone em nossas applications, especialmente quando são necessários formatos internacionais específicos para esses dados. No entanto, como desenvolvedores de Ruby on Rails, temos à disposição muitos recursos úteis, como o normalizes método e as colunas geradas, que podem tornar nosso código mais limpo e facilitar nosso trabalho.
Talvez você utilize um desses recursos em seu próximo aplicativo Rails para desenvolver algo incrível com as APIs da Vonage Communications. Se for o caso, você pode se cadastrar para obter uma account de desenvolvedor da Vonage para começar a desenvolver imediatamente. Se tiver alguma dúvida ou apenas quiser bater um papo, fique à vontade para nos encontrar no Twitter ou no Slack do Vonage Developer. Boa programação e até a próxima!
Compartilhar:
Karl é um Developer Advocate da Vonage, com foco na manutenção de nossos SDKs de servidor em Ruby e na melhoria da experiência dos desenvolvedores da nossa comunidade. Ele adora aprender, criar coisas, compartilhar conhecimento e tudo o que esteja relacionado à tecnologia da web em geral.