
Teilen Sie:
Karl ist Developer Advocate bei Vonage und kümmert sich um die Wartung unserer Ruby Server SDKs und die Verbesserung der Entwicklererfahrung für unsere Community. Er liebt es zu lernen, Dinge zu entwickeln, Wissen zu teilen und alles, was allgemein mit Webtechnologie zu tun hat.
Phone Numbers in Rails mit Normalizes und generierten Spalten
Lesedauer: 10 Minuten
Wenn Sie eine Anwendung entwickeln, die die Vonage Kommunikations-APIsverwenden, werden Sie wahrscheinlich irgendwann mit Telefonnummern arbeiten müssen. Egal, ob Ihre Anwendung automatische Text-zu-Sprache-Telefonanrufe über die Voice APIoder SMS- oder WhatsApp-Nachrichten über die Messages APIoder die Implementierung einer Zwei-Faktor-Authentifizierung mit der Verify APImüssen Sie für diese Interaktionen eine Zielrufnummer angeben.
Internationale Telefonnummern
Wie zu erwarten, benötigen die API-Endpunkte Telefonnummern in einem bestimmten und einheitlichen Format. Das von den Vonage APIs verwendete Numbers-Format folgt dem internationalen Standard E.164und wird erwartet:
Eine internationale Ländervorwahl (z.B.
1für die USA oder44für das Vereinigte Königreich)Für die führende
+oder die internationale Vorwahl (z.B.00) weggelassen werdenDamit der Amtskennziffer (z.B.
0) weggelassen werdenFür die Nummer selbst, um Sonderzeichen auszuschließen (z. B.
()oder-)
Eine US-Nummer hätte zum Beispiel das Format 14155550101, und eine britische Nummer hätte das Format 447700900123.
Ein typisches Szenario bei der Erstellung von Webanwendungen ist, dass die Quelle Ihrer Telefonnummern-Daten die Benutzereingaben sind, die als Teil eines Anmeldungs-Workflows erfasst werden. Die Sicherstellung, dass die Eingabedaten im richtigen Format für die spätere Verwendung mit der externen API vorliegen, kann einige Herausforderungen mit sich bringen. Wenn Sie jedoch eine Ruby on Rails-Anwendung erstellen, können einige kürzlich hinzugefügte Funktionen bei der Bewältigung dieser Herausforderungen sehr hilfreich sein.
Beispiel Workflow
Bevor wir uns mit diesen Funktionen befassen, sollten wir uns den Anmelde-Workflow etwas genauer ansehen. In einer Ruby on Rails-Anwendung definieren wir wahrscheinlich ein User Modell und ein UsersControllerzusammen mit den notwendigen View Templates. Unser users/new Template könnte, wenn es gerendert wird, etwa so aussehen:
Rendered Rails 'users/new' view template screenshot
Beachten Sie, dass in diesem Beispiel Phone Country Code und Phone Number separate Formularfelder sind und daher in einer Standard-Rails-Implementierung als separate Attribute unseres Modells in der Datenbank persistiert werden. Zum Beispiel könnten wir erwarten das unser UsersController Code in den folgenden Zeilen enthalten:
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
Wir könnten beide Daten in einer einzigen Eingabe in unserem Formular kombinieren, aber das würde dem Benutzer mehr Verantwortung für die korrekte Eingabe der Daten aufbürden und möglicherweise die Bereinigung der Daten im Back-End erschweren. Wenn wir den Telefon-Ländercode als eigenes Formularfeld haben, können wir die Daten auf eine bestimmte Gruppe von Werten einschränken, zum Beispiel mit einem Auswahlmenü. Es gibt eine Reihe von Möglichkeiten, dieses Feld zu implementieren, die ich hier nicht näher erläutern werde, aber schauen Sie sich unbedingt das countries gem und country_select gem.
In einer Rails-Anwendung werden wir wahrscheinlich Rails-Formular-Helper in unseren View-Templates verwenden, und im Fall des Telefonnummernfeldes selbst wird dies wahrscheinlich der telephone_field Hilfsprogramm (oder sein Alias, phone_field). In der gerenderten Ansicht, erstellt dieser Helfer ein HTML <input> Element mit einem type Attribut mit einem Wert von tel. Abgesehen von dem Wert für type (der von Handy-Browsern verwendet werden kann, um zu bestimmen, dass eine numerische Tastatur angezeigt werden soll), ist dieses Feld funktionell äquivalent zu einem text Feld des Typs (in Browsern, die keine telnicht unterstützen, fällt dies auf eine Standard text Typ-Eingabe zurück).
Im Gegensatz zu einigen anderen Eingabearten (z. B. email oder url), unterstützen Browser bei diesem Feldtyp keine automatische Eingabeüberprüfung. Natürlich können wir dem Benutzer neben dem Eingabefeld Hinweise zur Formatierung geben, und könnten vielleicht das pattern Attribut auf dem Element verwenden, um die Eingabe einzuschränken oder eine JavaScript-basierte Lösung zu verwenden. Als verantwortungsbewusste Entwickler sollten wir jedoch nicht vollständig Als verantwortungsbewusste Entwickler sollten wir uns jedoch nicht vollständig auf den Endbenutzer oder die Front-End-Validierung verlassen - es ist immer ratsam, unsere Benutzereingaben zu bereinigen, bevor sie in einer Datenbank gespeichert werden. Im Zusammenhang mit der Eingabe von Telefonnummern und insbesondere bei der Unterstützung eines globalen Kundenstamms bedeutet dies, dass man auf eine große Vielfalt potenzieller Nummernformate vorbereitet zu sein. Im Folgenden finden Sie einige Beispiele für verschiedene Telefonnummernformate, die auf der ganzen Welt verwendet werden:
77 12 3000
012345-60000
(01234) 500000
06-10000000
0123-400-000
01234/500000-67Dies ist der Ort, an dem Ruby on Rails normalizes Methode sehr nützlich sein kann.
Normalisiert
Die normalizes Methode wurde hinzugefügt in Rails 7.1. Die Methode ist enthalten in ActiveRecord::Base über das Normalization Modul eingebunden und daher für alle Rails-Modelle verfügbar, die von der ApplicationRecord abstrakten Klasse erben. Bei der Verwendung benötigt die Methode eine Args-Liste mit einem oder mehreren :names (die den Attributen entsprechen, die Sie normalisieren möchten) und ein :with Schlüsselwortargument, dessen Wert ein Ruby-Lambda ist, wobei das Argument des Lambdas das zu normalisierende Attribut darstellt.
Nachfolgend sehen Sie, wie die Verwendung von normalize in unserem Szenario mit der Telefonnummer aussehen könnte:
class User < ApplicationRecord
normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
Im obigen Beispiel, :phone eine Zeichenkette, also verwenden wir innerhalb des Lambdas zwei Methoden der Ruby String Klasse, um den Eingabewert zu transformieren:
Die
String#deleteMethode. Diese Methode gibt eine Kopie des String-Objekts zurück, für das sie aufgerufen wurde, wobei alle durch das Argument selector angegebenen Zeichen entfernt wurden. Hier gibt der Selektor durch die Verwendung des Caret^gibt der Selektor an, dass alle Zeichen nicht im Bereich0-9liegen, entfernt werden sollen (d. h. alle nicht numerischen Zeichen).Die
String#delete_prefixMethode. Ähnlich wie die Methodedeletegibt diese Methode eine Kopie des String-Objekts zurück, auf dem sie aufgerufen wurde, aber in diesem Fall werden nur die Zeichen am Anfang der Zeichenkette, die mit der als Argument übergebenen Zeichenkette übereinstimmen. Hier entfernt sie das Zeichen'0'(wenn es vorhanden ist) aus der Zeichenkette, die durch dendeleteMethodenaufruf zurückgegeben wird.
Mit dem normalizes Aufruf, der zu unserer User Klasse hinzugefügt wurde, können wir unsere Implementierung schnell in der Rails-Konsole testen, indem wir die normalize_value_for Methode:
$ 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"
Es war möglich, diese Art der Datentransformation durchzuführen vor vor Rails 7.1 und der Hinzufügung der normalizes Methode, aber die Implementierung wäre ein wenig aufwendiger gewesen.
Eine Möglichkeit wäre die Verwendung eines Active Record Rückruf wie z.B. before_save:
class User < ApplicationRecord
before_save :sanitize_phone
private
def sanitize_phone
phone&.delete("^0-9")&.delete_prefix("0")
end
end
Eine andere Möglichkeit wäre, die Setter-Methode außer Kraft zu setzen:
class User < ApplicationRecord
def phone=(value)
super(value&.delete("^0-9")&.delete_prefix("0"))
end
end
Der normalizes Ansatz ist jedoch einfacher und sauberer zu implementieren, insbesondere wenn Sie mehrere Attribute auf die gleiche Weise umwandeln müssen:
class User < ApplicationRecord
normalizes :home_phone, :work_phone, with: -> phone { phone.delete("^0-9").delete_prefix("0") }
end
Bei den anderen beiden Ansätzen müssten wir mehrere Setter außer Kraft setzen oder mehrere Hilfsmethoden definieren.
Beachten Sie auch die Verwendung von Rubys sicheren Navigationsoperators & in diesen anderen Implementierungen. Damit wird Ruby angewiesen, einen Methodenaufruf zu überspringen, wenn der Aufrufer nilist, und seine Verwendung hier verhindert, dass ein NoMethodError in einer Situation ausgelöst wird, in der das :phone Attribut war nil. Aufgrund der Art und Weise, wie der sichere Navigationsoperator funktioniert, müssen wir daran denken, ihn für jeden Methodenaufruf in die Kette aufzunehmen. Mit normalizeserhalten wir dieses Verhalten standardmäßig umsonst. Das heißt, wenn :phone ist nilist, wird das Lambda überhaupt nicht ausgeführt.
Vorbehalte
Ein paar Vorbehalte zu unserem Beispiel normalizes Implementierung:
Sie deckt nicht jede Situation oder jeden Grenzfall, der in der Produktion auftreten kann. Zum Beispiel ist der
delete_prefix("0")Teil vorhanden, weil viele Länder ihren Telefonnummern ein0an ihre Telefonnummern als Vorwahl anhängen, wenn sie innerhalb des Landes gewählt werden (so genannter Trunk Access), aber dies0ist jedoch nicht erforderlich, wenn Sie international wählen (stattdessen wird eine internationale Vorwahl vor der internationalen Landesvorwahl für das Land, in das Sie wählen, verwendet).Leider verfahren nicht alle Länder nach demselben Prinzip. Zunächst einmal verlangen nicht alle Länder einen Fernmeldezugangscode. Von den Ländern, die dies tun, verwenden die meisten
0aber es gibt auch eine beträchtliche Anzahl von Ländern (einschließlich der USA), die1als Amtskennziffer verwenden. Es gibt auch einige andere Ausreißer: einige Länder verwenden8, Ungarn verwendet06, Mexiko verwendet01, und die Mongolei verwendet01oder02.Ein noch schwierigerer Sonderfall ist Italien (zusammen mit San Marino und Vatikanstadt), das verwendet verwendet hat
0als Vorwahl für den Fernverkehr verwendet hat, jetzt aber enthält die0als Teil der Nummer für sowohl interne und für internationale Anrufe und Griechenland, das2für Festnetznummern und6für mobile Numbers. Um mit all diesen Randfällen fertig zu werden, könnte ein robusterer Ansatz für unserenormalizesImplementierung in der Produktion die bedingte Verwendung verschiedene Lambdas zu verwenden, die auf dem eingegebenen Ländercode basieren.Der Zweck von
normalizesist es, sicherzustellen, dass Ihre Daten einem bestimmten Format entsprechen, bevor sie in einer Datenbank gespeichert werden. Was es nicht ist die Überprüfung, ob die Zahl gültig und/oder vertrauenswürdig ist. Um dies zu tun, sollten Sie sich die Integration der Vonage Number Insight API als Teil Ihres Anmelde-Workflows zu integrieren (obwohl das den Rahmen dieses Artikels sprengen würde).
Für die Zwecke dieses Artikels gehen wir jedoch davon aus, dass wir mit unseren Daten an diesem Punkt zufrieden sind. Da wir nun aber unsere Daten haben, wie können wir verwenden sie? Das bringt uns zu der zweiten Ruby on Rails-Funktion, die ich behandeln möchte: Generated Columns.
Generierte Spalten
Wir haben die Daten unserer Telefonnummern gespeichert, aber :phone_country_code und :phone sind separate Attribute unseres User Modells. Die Migration für unser User Modell könnte etwa so aussehen:
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
Beim Erstellen oder Aktualisieren eines Userwerden diese Attribute separat gesetzt und aufgerufen:
user = User.new(phone_country_code: "44", phone: "7900000001")
user.phone_country_code # => "44"
user.phone # => "7900000001"
Wenn Verwendung dieser Daten mit Vonage-Kommunikations-APIs, zum Beispiel zum Senden einer SMS über die Messages APIzu senden, müssen wir kombinieren. die Landesvorwahl und die Telefonnummer:
message = Vonage::Messaging::Message.sms(message: "Hello from Vonage!")
Vonage.messaging.send(
to: "447900000001",
from: "Vonage",
**message
)(Hinweis: Das obige Beispiel verwendet das Vonage Ruby SDK über den Vonage Rails-Initialisierer)
Eine Möglichkeit wäre, die Daten am Ort der Nutzung zu kombinieren:
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
)Ein Nachteil dieses Ansatzes ist, dass wir diese kombinierten Daten an mehreren Stellen in unserer Code-Basis verwenden könnten und dieses Muster mit jeder jeder Verwendung der Daten wiederholen. Wenn wir unseren Code DRY halten wollen, wäre ein besserer Ansatz, einen Weg zu bieten, auf die bereits kombinierten Daten zuzugreifen, zum Beispiel als 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
)Eine Möglichkeit, dies zu tun, wäre die Definition einer Methode in unserem User Modell eine Methode zu definieren, die beide Werte kombiniert, zum Beispiel:
class User < ApplicationRecord
# rest of User code
def international_phone_number
phone_country_code + phone
end
end
Anstatt zusätzliche Getter-Methoden in unserer User Klasse zu definieren, wäre es vielleicht besser, wenn wir die Daten direkt von unserer Datenbank abfragen könnten.
Vor Rails 7.0 war eine Möglichkeit, dies zu erreichen, die Definition einer international_phone_number Spalte in unserer users Tabelle zu definieren und dann einen before_save Callback zu verwenden, der ein zusätzliches Attribut für das Objekt erstellt und dessen Wert berechnet, bevor es in der Datenbank gespeichert wird:
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
Wir könnten diese Spalte dann bei Bedarf abfragen:
user = User.create!(phone_country_code: "44", phone: "7900000001")
user.international_phone_number => "447900000001"
Abgesehen davon, dass sich dieser Ansatz etwas "hakelig" anfühlt, hat er auch andere Nachteile:
Es fügt Boiler-Plate-Code zu unserer
UserKlasseWir haben Code in unserer
UserKlasse, der teilweise dieinternational_phone_numberdefiniert, obwohl wir diese Art von Logik idealerweise in unserenActiveRecordMigrationen.
Seit Rails 7.0 gibt es eine sauberere Lösung für dieses Problem: generierte Spalten. Eine generierte Spalte ist eine Art von virtuellen Spalte deren Wert von dem Wert anderer Spalten abgeleitet wird. Im Gegensatz zum obigen Beispiel befindet sich die gesamte Logik für generierte Spalten auf der Datenbankebene.
Dieses Konzept ist nicht neu. MySQL verfügt seit MySQL 5.7 über virtuelle Spalten, und mehrere andere RDBMS haben eine ähnliche Funktionalität. Wenn Sie eine Rails-Anwendung entwickeln, ist es jedoch wahrscheinlich, dass Sie PostgreSQL als Datenpersistenzschicht verwenden werden. PostgreSQL hinzugefügt Generierte Spalten in PostgreSQL 12.0und der Rails 7.0.0 Veröffentlichung aktualisierte den Rails PostgreSQL Adapter, um diese zu unterstützen.
Schauen wir uns an, wie wir generierte Spalten als Teil unserer Implementierung verwenden können.
Zum Hinzufügen einer international_phone_number generierte Spalte zu unserer bestehenden users Tabelle hinzuzufügen, müssen wir eine Migration erstellen, um sie zu definieren:
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
Die ersten drei Positionsargumente für die add_column Methode Aufrufs sind die standardmäßig erforderlichen Argumente für diese spezielle Methode in einer Rails-Migration. Wir haben eine table_name von :users (das ist die Tabelle die wir aktualisieren wollen), ein column_name von :international_phone_number, und ein type von :virtual. Darauf folgt ein options Hash mit drei Schlüsseln, die spezifisch für die Definition der :virtual Spalte:
:type. Dies ist der Datentyp für die:virtualSpalte, in diesem Fall eine:string:as. Dies gibt an, wie der Wert für die Spalte erzeugt werden soll. In diesem Fall verketten wir diephone_country_codeundphoneFelder mit Hilfe des PostgreSQL||Operator.:stored. Dies ist ein boolescher Wert, der bestimmt, ob der Wert für die:virtualSpalte berechnet werden soll, wenn die Daten geschrieben werden in die Spalten geschrieben werden, die zur Erzeugung der Spalte verwendet werden (true), oder wenn die virtuelle Spalte selbst abgefragt wird (false).
Ein Vorbehalt bei der Verwendung von generierten Spalten ist, dass die Spalten, von denen der Wert der generierten Spalte abgeleitet wird, in der gleichen Tabelle definiert sein muss wie die generierte Spalte selbst.
Schlussfolgerung
Wie wir gesehen haben, kann die Verwendung von Telefonnummern in unseren Applications eine Herausforderung darstellen, vor allem dann, wenn bestimmte internationale Formate für diese Daten erforderlich sind. Als Ruby on Rails-Entwickler stehen uns jedoch viele nützliche Funktionen zur Verfügung, wie z. B. die normalizes Methode und Generated Columns, die unseren Code sauberer und unser Leben einfacher machen können.
Vielleicht werden Sie eine dieser Funktionen in Ihrer nächsten Rails-Anwendung verwenden, um etwas Großartiges mit den Vonage Kommunikations-APIs zu entwickeln. Wenn ja, dann können Sie sich für ein kostenloses Vonage Entwickler Account um sofort mit der Entwicklung zu beginnen. Wenn Sie Fragen haben oder einfach nur plaudern möchten, können Sie sich gerne mit uns auf Twitter oder auf dem Vonage Entwickler-Slack. Viel Spaß beim Programmieren, und bis zum nächsten Mal!
Teilen Sie:
Karl ist Developer Advocate bei Vonage und kümmert sich um die Wartung unserer Ruby Server SDKs und die Verbesserung der Entwicklererfahrung für unsere Community. Er liebt es zu lernen, Dinge zu entwickeln, Wissen zu teilen und alles, was allgemein mit Webtechnologie zu tun hat.