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

Phone Numbers in Rails mit Normalizes und generierten Spalten

Zuletzt aktualisiert am January 11, 2024

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. 1 für die USA oder 44 für das Vereinigte Königreich)

  • Für die führende + oder die internationale Vorwahl (z.B. 00) weggelassen werden

  • Damit der Amtskennziffer (z.B. 0) weggelassen werden

  • Fü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:

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

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-67

Dies 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#delete Methode. 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 Bereich 0-9 liegen, entfernt werden sollen (d. h. alle nicht numerischen Zeichen).

  • Die String#delete_prefix Methode. Ähnlich wie die Methode delete gibt 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 den delete Methodenaufruf 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:

  1. 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 ein 0 an ihre Telefonnummern als Vorwahl anhängen, wenn sie innerhalb des Landes gewählt werden (so genannter Trunk Access), aber dies 0 ist 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), die 1 als Amtskennziffer verwenden. Es gibt auch einige andere Ausreißer: einige Länder verwenden 8, Ungarn verwendet 06, Mexiko verwendet 01, und die Mongolei verwendet 01 oder 02.

    Ein noch schwierigerer Sonderfall ist Italien (zusammen mit San Marino und Vatikanstadt), das verwendet verwendet hat 0 als Vorwahl für den Fernverkehr verwendet hat, jetzt aber enthält die 0 als Teil der Nummer für sowohl interne und für internationale Anrufe und Griechenland, das 2 für Festnetznummern und 6 für mobile Numbers. Um mit all diesen Randfällen fertig zu werden, könnte ein robusterer Ansatz für unsere normalizes Implementierung in der Produktion die bedingte Verwendung verschiedene Lambdas zu verwenden, die auf dem eingegebenen Ländercode basieren.

  2. Der Zweck von normalizes ist 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 User Klasse

  • Wir haben Code in unserer User Klasse, der teilweise die international_phone_numberdefiniert, obwohl wir diese Art von Logik idealerweise in unseren ActiveRecord Migrationen.

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 :virtual Spalte, in diesem Fall eine :string

  • :as. Dies gibt an, wie der Wert für die Spalte erzeugt werden soll. In diesem Fall verketten wir die phone_country_code und phone Felder mit Hilfe des PostgreSQL || Operator.

  • :stored. Dies ist ein boolescher Wert, der bestimmt, ob der Wert für die :virtual Spalte 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:

https://a.storyblok.com/f/270183/373x376/e8d3211236/karl-lingiah.png
Karl LingiahRuby-Entwickler Advocate

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.