This is part 2 of our tutorial showing off some features of the new PHP client library. Haven’t checked out part one? Well, that’s an easy thing to solve.

Still here? Still haven’t seen the first part? Fine. In that we built simple SMS group chat using the new PHP client library. You could join and leave a group, and every message sent to the group was relayed to the other members.

In this second part, we’re going to let those members access an archive of the group’s messages in their browsers.

All we have are the user’s phone numbers, no username, no password. Fortunately, that phone number is a reasonable form of identity. And this isn’t an uncommon scenario. Many apps on-board users with not much more than a phone number to keep the process frictionless, and need to use that identifier for authentication later on.

So let’s take a look at how we can do just that.

Set Up

Since we already have a working app, we only need some basic HTML for our login forms and the archive of messages. We’ll unashamedly lift the basic page from Bootstrap’s example templates, and remove the entire navigation section. As brevity is to wit, CDNs are to tutorials, so we’ll update that template to load bootstrap’s CSS and Javascript shims from MaxCDN, and jQuery from Google:

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> 1 2 3 4 < link href = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel = "stylesheet" > <script src = "https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js" > </script> <script src = "https://oss.maxcdn.com/respond/1.4.2/respond.min.js" > </script>

View in Context

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> 1 2 3 <script src = "https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js" > </script> <script src = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" > </script>

View in Context

We can replace the contents of the template’s <div class="container"> with what amounts to a ‘layout’. Then include a very simple view by echo ing whatever is in the $content variable:

<div class="container"> <div class="row"> <h1>Group Chat Tutorial</h1> <p class="lead"> This is a simple tutorial of Nexmo and the PHP Client Library<br> Here you can find a log of your group messages. </p> </div> <?php echo $content ?> </div><!-- /.container --> 1 2 3 4 5 6 7 8 9 10 11 12 < div class = "container" > < div class = "row" > < h1 > Group Chat Tutorial < / h1 > < p class = "lead" > This is a simple tutorial of Nexmo and the PHP Client Library < br > Here you can find a log of your group messages . < / p > < / div > <?php echo $content ?> < / div > < ! -- / . container -- >

View in Context

We’re using the same Mongo database and Nexmo client as our existing application, so we can copy the few lines of bootstrap code from inbound.php . We’re going to use PHP’s $_SESSION support to track the user’s login status, so we’ll start the session as well.

$config = require __DIR__ . '/../bootstrap.php'; $nexmo = new \Nexmo\Client(new \Nexmo\Client\Credentials\Basic($config['nexmo']['key'], $config['nexmo']['secret'])); $mongo = new \MongoDB\Client($config['mongo']['uri']); $db = $mongo->selectDatabase($config['mongo']['database']); session_start(); 1 2 3 4 5 6 7 $ config = require __DIR_ _ . '/../bootstrap.php' ; $ nexmo = new \ Nexmo \ Client ( new \ Nexmo \ Client \ Credentials \ Basic ( $ config [ 'nexmo' ] [ 'key' ] , $ config [ 'nexmo' ] [ 'secret' ] ) ) ; $ mongo = new \ MongoDB \ Client ( $ config [ 'mongo' ] [ 'uri' ] ) ; $ db = $ mongo -> selectDatabase ( $ config [ 'mongo' ] [ 'database' ] ) ; session_start ( ) ;

View in Context

Save this as index.php , and we’re off to a great start.

Login Form

To allow users to authenticate using their phone number, we’ll need a simple form where they can provide that number. We know it’ll end up where <?php echo $content ?> is, so we’ll create an HTML fragment following Bootstrap’s form styles and call it login.html :

<div class="row"> <h3>Login with your number</h3> <form class="form-inline" method="post" action=""> <div class="form-group"> <label for="number">Number</label> <input type="text" class="form-control" id="number" name="number" placeholder="14845551324"> </div> <button type="submit" class="btn btn-default">Login</button> </form> </div> 1 2 3 4 5 6 7 8 9 10 11 < div class = "row" > < h3 > Login with your number < / h3 > < form class = "form-inline" method = "post" action = "" > < div class = "form-group" > < label for = "number" > Number < / label > < input type = "text" class = "form-control" id = "number" name = "number" placeholder = "14845551324" > < / div > < button type = "submit" class = "btn btn-default" > Login < / button > < / form > < / div >

View in Context

To have that spectacular form rendered by default if no other $content is defined, we’ll add a few lines back in index.php , right before the HTML starts:

if(!isset($content)){ $content = file_get_contents(__DIR__ . '/login.html'); } 1 2 3 4 if ( ! isset ( $ content ) ) { $ content = file_get_contents ( __DIR_ _ . '/login.html' ) ; }

View in Context

Everything is setup to log the user in – we just need to do that with their phone number. Nexmo’s Verify API provides exactly that capability. We provide it the user’s number and a brand to identify our app to the user, and Nexmo will generate a single use code and send it to the user. The Verify API returns us a request_id to use later, once the user provides our app with the single use code they were sent. That verifies that they are indeed the owner of that phone number.

First, right after session_start() , we check that the user has submitted a phone number:

if (isset($_POST['number'])) { } 1 2 3 4 if ( isset ( $ _POST [ 'number' ] ) ) { }

And then we use the Nexmo client’s Verify support to start the verification process. $nexmo->verify()->start() will return a Nexmo\Verify\Verification object, and we can use that in the future to check that the user has provided the correct single use code. Since that will happen in a future request, we need to store the Verification object somewhere.

To make tracking user logins possible, we’ll store the Verification object in the database, associated with the user’s number. To easily track all the past login attempts, we’ll use an array, and just $push the current Verification to the end of that array. If the user has never logged in before, we’ll have Mongo create the record (instead of trying to update one that doesn’t exist). The ‘upsert’ option lets us do that.

To actually put the Verification object in the database, we’ll just serialize it as a string using the Nexmo client library. That ensures that after serializing and unserializing the Verification object is properly setup to be used with the client library.

We’ll wrap it all in a try block, so we can handle any errors gracefully:

try { $verification = $nexmo->verify()->start([ 'number' => $_POST['number'], 'brand' => 'GroupChat' ]); $db->selectCollection('logins')->updateOne( ['_id' => $_POST['number']], ['$push' => ['verifications' => $nexmo->serialize($verification)]], ['upsert' => true] ); } catch (Exception $e) { } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 try { $ verification = $ nexmo -> verify ( ) -> start ( [ 'number' = > $ _POST [ 'number' ] , 'brand' = > 'GroupChat' ] ) ; $ db -> selectCollection ( 'logins' ) -> updateOne ( [ '_id' = > $ _POST [ 'number' ] ] , [ '$push' = > [ 'verifications' = > $ nexmo -> serialize ( $ verification ) ] ] , [ 'upsert' = > true ] ) ; } catch ( Exception $ e ) { }

View in Context

With the Verification object in the database, we also need to make sure our application knows that the user is in the middle of a this verification process. We’ll stick their phone number in $_SESSION . This will both signal that we’re expecting the user to provide a single use code, and let us retrieve the Verification object, since we used the user’s number when storing it.

A Location: / header will redirect the user back to the application. This avoids the user hitting ‘refresh’ and the POST trying to start another verification process.

We can set the $_SESSION variable and redirect right after the call to the database:

$db->selectCollection('logins')->updateOne( ['_id' => $_POST['number']], ['$push' => ['verifications' => serialize($verification)]], ['upsert' => true] ); $_SESSION['number'] = $_POST['number']; header('Location: /'); return; 1 2 3 4 5 6 7 8 9 10 $ db -> selectCollection ( 'logins' ) -> updateOne ( [ '_id' = > $ _POST [ 'number' ] ] , [ '$push' = > [ 'verifications' = > serialize ( $ verification ) ] ] , [ 'upsert' = > true ] ) ; $ _SESSION [ 'number' ] = $ _POST [ 'number' ] ; header ( 'Location: /' ) ; return ;

View in Context

But what if there’s an error? Our catch block is currently empty. Since this is a simple tutorial, we’ll just grab the exception’s message so it can be passed on to the user (something you shouldn’t do in real life, as those messages may contain sensitive information):

} catch (Exception $e) { $error = $e->getMessage(); } 1 2 3 4 } catch ( Exception $ e ) { $ error = $ e -> getMessage ( ) ; }

View in Context

To wrap up this new error support, we need to update the UI a bit. Right before we echo $content in our HTML, we’ll show the error (if there is one):

<?php if (isset($error)): ?> <div class="alert alert-danger" role="alert"> <?php echo $error ?> </div> <?php endif; ?> <?php echo $content ?> 1 2 3 4 5 6 7 8 <?php if ( isset ( $error ) ) : ?> < div class = "alert alert-danger" role = "alert" > <?php echo $error ?> < / div > <?php endif ; ?> <?php echo $content ?>

View in Context

Finally, before we try to log the user in, we should check to see if they’re actually a user. That can be done with a query to our Mongo database. If we count the groups that user has been a member of and get 0 , we’ll leverage our new error handling and throw an exception.

We’ll add that check before we start the verification process:

$groups = $db->selectCollection('users')->count([ 'user' => $_POST['number'] ]); if(!$groups){ throw new Exception('Could not find that number.'); } $verification = $nexmo->verify()->start([ 'number' => $_POST['number'], 'brand' => 'GroupChat' ]); 1 2 3 4 5 6 7 8 9 10 11 12 13 $ groups = $ db -> selectCollection ( 'users' ) -> count ( [ 'user' = > $ _POST [ 'number' ] ] ) ; if ( ! $ groups ) { throw new Exception ( 'Could not find that number.' ) ; } $ verification = $ nexmo -> verify ( ) -> start ( [ 'number' = > $ _POST [ 'number' ] , 'brand' = > 'GroupChat' ] ) ;

View in Context

Getting the Code

If we give it a test run now, the verification process is started (and our phone is sent the code) but there’s no place to actually enter the single use code. Let’s add that by creating another simple form and put it in [ code.html ][code.htm]:

<div class="row"> <h3>Login with your number</h3> <form class="form-inline" method="post" action=""> <div class="form-group"> <label for="code">Code</label> <input type="text" class="form-control" id="code" name="code" placeholder="1234"> </div> <button type="submit" class="btn btn-default">Login</button> </form> <p>(We just sent a code to your phone.)</p> </div> 1 2 3 4 5 6 7 8 9 10 11 12 < div class = "row" > < h3 > Login with your number < / h3 > < form class = "form-inline" method = "post" action = "" > < div class = "form-group" > < label for = "code" > Code < / label > < input type = "text" class = "form-control" id = "code" name = "code" placeholder = "1234" > < / div > < button type = "submit" class = "btn btn-default" > Login < / button > < / form > < p > ( We just sent a code to your phone . ) < / p > < / div >

View in Context

We’ll also need to modify index.php to check if $_SESSION['number'] is set (we set that when the verification process starts). We don’t want users to be able to start a verification process when there’s one in progress, so we’ll modify the existing if (isset($_POST['number'])) to only be considered if $_SESSION['number'] is not set.

if (isset($_SESSION['number'])) { $content = file_get_contents(__DIR__ . '/code.html'); } elseif (isset($_POST['number'])) { //... } 1 2 3 4 5 6 if ( isset ( $ _SESSION [ 'number' ] ) ) { $ content = file_get_contents ( __DIR_ _ . '/code.html' ) ; } elseif ( isset ( $ _POST [ 'number' ] ) ) { //... }

View in Context

That handles displaying the single use code form to the users, but how do we process that once they submit a code? One more modification to our if statement to handle the case of the user providing a code in $_POST['code'] during an ongoing verification ( $_SESSION['number'] is set).

Just like the start of the verification, we’ll use a try block to handle any errors:

if (isset($_SESSION['number']) and isset($_POST['code'])) { try { } catch (Exception $e) { } } elseif (isset($_SESSION['number'])) { $content = file_get_contents(__DIR__ . '/code.html'); } elseif (isset($_POST['number'])) { //... } 1 2 3 4 5 6 7 8 9 10 11 12 if ( isset ( $ _SESSION [ 'number' ] ) and isset ( $ _POST [ 'code' ] ) ) { try { } catch ( Exception $ e ) { } } elseif ( isset ( $ _SESSION [ 'number' ] ) ) { $ content = file_get_contents ( __DIR_ _ . '/code.html' ) ; } elseif ( isset ( $ _POST [ 'number' ] ) ) { //... }

We need to get the ongoing verification from the database. That’s a query using the number we have in $_SESSION['number'] as the _id . Since we’re really only interested in the latest verification, we can use Mongo’s projection support to only return the ‘verifications’ field, and $slice it so we only get the last element of that array.

If for some reason we can’t find the login attempt, we’ll throw an Exception and let the catch block handle it:

$login = $db->selectCollection('logins')->findOne( ['_id' => $_SESSION['number']], ['projection' => ['verifications' => ['$slice' => -1]]] ); if(!$login){ throw new Exception('Could not find verification.'); } 1 2 3 4 5 6 7 8 9 $ login = $ db -> selectCollection ( 'logins' ) -> findOne ( [ '_id' = > $ _SESSION [ 'number' ] ] , [ 'projection' = > [ 'verifications' = > [ '$slice' = > - 1 ] ] ] ) ; if ( ! $ login ) { throw new Exception ( 'Could not find verification.' ) ; }

View in Context

After finding the login, we’ll unserialize the Verification object,

using the Nexmo client to ensure it’s properly setup to work with the library.

We could use the Nexmo client to check if the code is valid:

$nexmo->verify()->check($verification, $_POST['code']); 1 2 $ nexmo -> verify ( ) -> check ( $ verification , $ _POST [ 'code' ] ) ;

However, the client library matches any error response from the API with an Exception. In this case an invalid code would result in an error from the API. We could handle that by catching the exception, but it’s not unexpected that a user would provide the wrong code.

Using the Verification object directly provides an interface not tied directly to the API response, and allows us to use an if statement to check the validity of the code.

$verification = $nexmo->unserialize($login['verifications'][0]); if($verification->check($_POST['code'])){ $_SESSION['user'] = $verification->getNumber(); header('Location: /'); return; } else { $error = 'Invalid Code'; $content = file_get_contents(__DIR__ . '/code.html'); } 1 2 3 4 5 6 7 8 9 10 11 $ verification = $ nexmo -> unserialize ( $ login [ 'verifications' ] [ 0 ] ) ; if ( $ verification -> check ( $ _POST [ 'code' ] ) ) { $ _SESSION [ 'user' ] = $ verification -> getNumber ( ) ; header ( 'Location: /' ) ; return ; } else { $ error = 'Invalid Code' ; $ content = file_get_contents ( __DIR_ _ . '/code.html' ) ; }

View in Context

If the code is valid, we can track that the user is logged in by setting $_SESSION['user'] . Now logged in, we can redirect back to the app and avoid any issue with the refresh button. If the code is invalid we just set an $error , and load the code.html template so the user can try again.

Our catch block is similar to the one we used when starting the verification process. Since we already handled checking the validity of the code, any exception means something went wrong with the verification process. We’ll unset the $_SESSION['number'] so the app knows we’re no longer trying to complete a verification process, and will show the user the login form:

try { //... } catch (Exception $e) { $error = $e->getMessage(); $_SESSION['number'] = null; } 1 2 3 4 5 6 7 try { //... } catch ( Exception $ e ) { $ error = $ e -> getMessage ( ) ; $ _SESSION [ 'number' ] = null ; }

View in Context

Accessing the Archive

Now that $_SESSION['user'] marks the user as logged in and contains the identify of the user (their phone number), we can build the last part of this application and show the user an archive of their group chats. One more modification to our if statement adds support for this condition. Like other actions, we’ll wrap things in a try block for error handling, but we’ll also set $content in the catch so the login form isn’t displayed:

if (isset($_SESSION['user'])) { try { } catch (Exception $e) { $content = ''; $error = $e->getMessage(); } } elseif (isset($_SESSION['number']) and isset($_POST['code'])) { //... } elseif (isset($_SESSION['number'])) { $content = file_get_contents(__DIR__ . '/code.html'); } elseif (isset($_POST['number'])) { //... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if ( isset ( $ _SESSION [ 'user' ] ) ) { try { } catch ( Exception $ e ) { $ content = '' ; $ error = $ e -> getMessage ( ) ; } } elseif ( isset ( $ _SESSION [ 'number' ] ) and isset ( $ _POST [ 'code' ] ) ) { //... } elseif ( isset ( $ _SESSION [ 'number' ] ) ) { $ content = file_get_contents ( __DIR_ _ . '/code.html' ) ; } elseif ( isset ( $ _POST [ 'number' ] ) ) { //... }

To show the user an archive of their chats, we first need to know what groups they’ve joined. That’s a simple query much like the count query we used when they logged in. If somehow they have no groups (shouldn’t ever happen, as we’re checking that during login), we can throw an exception:

$groups = $db->selectCollection('users')->find([ 'user' => $_SESSION['user'] ])->toArray(); if (!$groups) { throw new Exception('User has no groups.'); return; } 1 2 3 4 5 6 7 8 9 $ groups = $ db -> selectCollection ( 'users' ) -> find ( [ 'user' = > $ _SESSION [ 'user' ] ] ) -> toArray ( ) ; if ( ! $ groups ) { throw new Exception ( 'User has no groups.' ) ; return ; }

View in Context

If they’re a member of more than one group, we need to let them pick which archive they want. We’ll use a query string parameter for that, and if it’s not set just default to the first group:

if(isset($_GET['group'])){ foreach ($groups as $group) { if ($group['group'] == $_GET['group']) { $selected = $group; break; } } } if (!isset($selected)) { $selected = reset($groups); } 1 2 3 4 5 6 7 8 9 10 11 12 13 if ( isset ( $ _GET [ 'group' ] ) ) { foreach ( $ groups as $ group ) { if ( $ group [ 'group' ] == $ _GET [ 'group' ] ) { $ selected = $ group ; break ; } } } if ( ! isset ( $ selected ) ) { $ selected = reset ( $ groups ) ; }

View in Context

Now that we know the user and the group, we can get all the message they were sent. Since they can leave a group at any time and join again later, we’ll only show them the message they were sent. And of course, any message they sent to the group:

$query = [ 'group' => $selected['group'], '$or' => [ ['user' => $_SESSION['user']], ['sends.user' => $_SESSION['user']] ] ]; 1 2 3 4 5 6 7 8 $ query = [ 'group' = > $ selected [ 'group' ] , '$or' = > [ [ 'user' = > $ _SESSION [ 'user' ] ] , [ 'sends.user' = > $ _SESSION [ 'user' ] ] ] ] ;

View in Context

We’ll set the sorting to be by date descending, and query the database:

$options = [ 'sort' => ['date' => -1] ]; $messages = $db->selectCollection('logs')->find($query, $options); 1 2 3 4 5 6 $ options = [ 'sort' = > [ 'date' = > - 1 ] ] ; $ messages = $ db -> selectCollection ( 'logs' ) -> find ( $ query , $ options ) ;

View in Context

For everything else, we simply set $content to the contents of our template file since they were simple forms. This time our template is a bit more complex, as it renders the archive and provides the option to select a different group. This means it expects two variables, $groups and $messages , which happen to be variables in the current scope.

We can accomplish a very simple template render using output buffering to capture the output when we include the template and store it in $content .

ob_start(); include __DIR__ . '/messages.phtml'; $content = ob_get_clean(); 1 2 3 4 ob_start ( ) ; include __DIR_ _ . '/messages.phtml' ; $ content = ob_get_clean ( ) ;

View in Context

Now let’s take a look at creating messages.phtml . We’ll use bootstrap’s dropdown buttons to create a nice set of links to all the user’s groups. We’ll also loop through the messages and place them into bootstrap’s ‘media object’:

<div class="row"> <div class="btn-group"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Group <span class="caret"></span> </button> <ul class="dropdown-menu"> <?php foreach ($groups as $group): ?> <li><a href="?group=<?php echo $group['group']; ?>"><?php echo $group['group']; ?></a></li> <?php endforeach; ?> </ul> </div> <?php foreach($messages as $message): ?> <div class="media"> <div class="media-body"> <h4 class="media-heading"><?php echo $message['name'] ?></h4> <?php echo $message['text'] ?> </div> </div> <?php endforeach; ?> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 < div class = "row" > < div class = "btn-group" > < button type = "button" class = "btn btn-default dropdown-toggle" data - toggle = "dropdown" aria - haspopup = "true" aria - expanded = "false" > Group < span class = "caret" > < / span > < / button > < ul class = "dropdown-menu" > <?php foreach ( $groups as $group ) : ?> < li > < a href = "?group= <?php echo $group [ 'group' ] ; ?> " > <?php echo $group [ 'group' ] ; ?> < / a > < / li > <?php endforeach ; ?> < / ul > < / div > <?php foreach ( $messages as $message ) : ?> < div class = "media" > < div class = "media-body" > < h4 class = "media-heading" > <?php echo $message [ 'name' ] ?> < / h4 > <?php echo $message [ 'text' ] ?> < / div > < / div > <?php endforeach ; ?> < / div >

View in Context

Logout

To let the user logout of the app, we just need to unset $_SESSION['user'] . We’ll unset $_SESSION['number'] as well to be sure the application doesn’t think we’re in the middle of a verification, and allow a logout even during a login:

if (isset($_GET['logout'])) { $_SESSION['user'] = null; $_SESSION['number'] = null; header('Location: /'); return; } 1 2 3 4 5 6 7 if ( isset ( $ _GET [ 'logout' ] ) ) { $ _SESSION [ 'user' ] = null ; $ _SESSION [ 'number' ] = null ; header ( 'Location: /' ) ; return ; }

View in Context

And we’ll add a complementary bootstrap button to the UI, showing the logout button if a user is logged in or in the process of logging in:

<?php if(isset($_SESSION['user']) OR isset($_SESSION['number'])): ?> <p><a href="?logout=1" class="btn btn-default">Logout</a> </p> <?php endif; ?> 1 2 3 4 <?php if ( isset ( $_SESSION [ 'user' ] ) OR isset ( $_SESSION [ 'number' ] ) ) : ?> < p > < a href = "?logout=1" class = "btn btn-default" > Logout < / a > < / p > <?php endif ; ?>

View in Context

Next Steps

Now we’ve created a simple UI that allows the user to login with just their phone number – because that’s the only identifier our app has for the user. This same process can be used for any application that onboards users quickly using just their phone number.

But the same basic verification flow can be used to add second factor authentication to an exsisting application with a normal username and password login. It can also be a powerful addition to signup when used to confirm that the phone number a user is providing is actually theirs – key when your application will associate them with other users based on phone numbers.

Hopefully, this introduction to the new PHP client library has been successful in showing the direction we’re headed when it comes to client libraries. We’d love your feedback on that – both on the general direction, or the PHP library specifically.