() translation by (you can also view the original English article)



Este tutorial forma parte de la serie Construyendo su startup con PHP en Envato Tuts+. En esta serie, te guío a través del lanzamiento de una startup desde el concepto a la realidad utilizando mi aplicación Meeting Planner como un ejemplo de la vida real. En cada paso del camino, lanzaré el código de Meeting Planner como ejemplos de código abierto de los que puede aprender. También abordaré los problemas comerciales relacionados con el startup a medida que surjan.
Introducción
Buenos días. Hoy, le explicaré cómo utilicé Google API para importar contactos de personas en Meeting Planner. El objetivo es hacer que sea más rápido para las personas invitar a sus amigos a las reuniones.
Si no ha intentado programar una reunión con Meeting Planner, pruébelo. Si usa su cuenta de Google para registrarse, puede visitar la página de amigos que se encuentra arriba e Importar sus contactos de Google. Comparte tus pensamientos y comentarios en los comentarios a continuación.
Participo
en las discusiones, pero también puedes contactarme @reifman en Twitter
(recientemente mi cuenta fue verificada, así que debo ser tan genial
como Justin Beebert (nota para los dioses editoriales.Tengo confianza
en esta ortografía. Creo que es correcta Déjalo.) Siempre estoy abierto a
nuevas ideas de funciones para
Meeting Planner, así como sugerencias para futuros episodios de
series.
Como recordatorio, todo el código para Meeting Planner se proporciona como código abierto y está escrito en Yii2 Framework para PHP. Si desea obtener más información acerca de Yii2, consulte mis series paralelas Programando con Yii2.
Pensando en la integración de contactos de Google
La página de amigos
Muchas personas tienen miles de contactos en su cuenta de Google, y pocos son importantes para ellos. Pero, para la mayoría, no hay forma de discernir cuáles son y cuáles no.
Creo que el tamaño de la tabla de usuarios en Meeting Planner afecta el rendimiento general del servicio. No quería importar contactos que quizás nunca sean relevantes en la tabla User.
Esto creó algunas complicaciones tanto en UX como en el código donde las personas buscan y acceden a sus amigos en el servicio.
Al final, decidí crear una tabla separada para contactos y mostrarla por separado en la interfaz de usuario por el momento.



Elegir participantes para las reuniones
Donde todo esto conduce es que será más fácil para las personas agregar amigos de sus contactos simplemente escribiendo los primeros caracteres. Estoy usando un widget de Typeahead en la ventana emergente Agregar participantes que se muestra a continuación:



Después de haber importado mis contactos de Google, se integran con mis amigos (personas a las que ya he invitado a reuniones o que me han invitado).
En este caso, comencé a escribir sar y aparecen un montón de nombres de prefijo Sar:



Encontrar a alguien para invitar a una reunión desde sus Contactos de Google se vuelve bastante rápido y fácil (hasta que agregue muchos de ellos, que menciono más adelante).
Problemas de privacidad
Tampoco quiero abusar de los fideicomisos de las personas mediante el uso indebido de sus miles de contactos. En este momento, ni siquiera ofreceremos a las personas la oportunidad de invitar a todos sus contactos de Google a Meeting Planner, aunque podríamos ofrecer esto en el futuro. Ciertamente no enviaremos correos electrónicos masivos sin permiso.
Escribir el código
Si aún no lo has hecho, echa un vistazo a Construyendo su startup con PHP: Simplificando Onramp con OAuth. Ese es el episodio en el que primero autenticé las API de Google para el inicio de sesión y registro de OAuth.
Google tiene cuidado con la seguridad con sus API. A la luz de lo que sucedió recientemente con los hacks de Yahoo, aprecio esto más profundamente. Sin embargo, esto hace que sus API sean más difíciles que otras para autenticarse y trabajar.
De hecho, encontré que la API de contactos de Google fluía de la manera más confusa, frustrante y difícil que he tenido que escribir. Y la API de Google desaprueba a los programadores de PHP: somos los últimos en obtener un código de muestra.
Vamos a sumergirnos
Creando la tabla de direcciones
Dado que ya existe una tabla UserContact para el teléfono del usuario y las direcciones de Skype, decidí llamar a la dirección de la tabla. Aquí está la migración para crearlo:
1 |
<?php
|
2 |
use yii\db\Schema; |
3 |
use yii\db\Migration; |
4 |
|
5 |
class m160904_001244_create_table_address extends Migration |
6 |
{
|
7 |
public function up() |
8 |
{
|
9 |
$tableOptions = null; |
10 |
if ($this->db->driverName === 'mysql') { |
11 |
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; |
12 |
}
|
13 |
|
14 |
$this->createTable('{{%address}}', [ |
15 |
'id' => Schema::TYPE_PK, |
16 |
'user_id' => Schema::TYPE_BIGINT.' NOT NULL', |
17 |
'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', |
18 |
'firstname' =>Schema::TYPE_STRING.' NOT NULL', |
19 |
'lastname' =>Schema::TYPE_STRING.' NOT NULL', |
20 |
'fullname' =>Schema::TYPE_STRING.' NOT NULL', |
21 |
'email' =>Schema::TYPE_STRING.' NOT NULL', |
22 |
'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', |
23 |
'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', |
24 |
], $tableOptions); |
25 |
$this->addForeignKey('fk_address_user_id', '{{%address}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); |
26 |
}
|
27 |
|
28 |
public function down() |
29 |
{
|
30 |
$this->dropForeignKey('fk_address_user_id', '{{%address}}'); |
31 |
$this->dropTable('{{%address}}'); |
32 |
}
|
33 |
}
|
Por supuesto, utilicé el Gii de Yii para ayudarme con el controlador, el modelo y las vistas. Esto se ha cubierto anteriormente en la serie de startup.
Aumento de la autenticación API de Google
Puede recordar la página Credenciales de Google en el tutorial mencionado anteriormente:



Puede encontrarlo desde Google Developers Console.
Debe agregar direcciones URL para todos sus entornos (desarrollo, puesta en escena, producción, etc.) y para cada controlador y método. Esto complica los esfuerzos para trabajar con su API de Contactos, pero probablemente también protege mejor los datos de las personas.



Importación de contactos de Google
Aquí está la API de Contactos de Google v3.0. No me di cuenta cuando comencé a escribir el código que ahora recomiendan People API para el acceso de solo lectura. Desafortunadamente, mi código está usando la API de lectura / escritura. De acuerdo, entonces no soy un genio. Los empresarios raramente lo son, incluso Bill Gates dice que tuvo suerte.
En
general, encontré que la API de Contactos de Google es una de las API
más confusas y difíciles que he usado.
Si tuviera un mayor nivel de experiencia en el desarrollo de API de Google o si hubiera dedicado más tiempo a trabajar en esto, podría haber encontrado un enfoque más simple. Pero, por lo que pude ver, era importante que hicieras todo con la API desde una URL. En mi caso, https://meetingplanner.io/address/import.
Y Google devuelve las claves y le redirige a esta URL repetidamente, por lo que debe estar atento al estado de la API y trabajar en torno a esto.
Supongo que todo esto se hace para aumentar la seguridad, pero requiere una gestión de estado integrada en lo que de otro modo serían simples solicitudes API. La administración del estado puede ahorrar tiempo, pero solo si la documentación y el código de muestra son buenos. En este caso, para PHP, no lo son.
Empezando
Veamos AddressController.php
actionImport()
:
1 |
public function actionImport() { |
2 |
// imports contacts from the google api
|
3 |
$address = new Address(); |
4 |
// create session cookies
|
5 |
$session = Yii::$app->session; |
6 |
// if we request code reset, then remove the google_code cookie
|
7 |
// i.e. if google_code expires
|
8 |
if (isset($_GET['reset']) && !$session->has('google_code_reset')) { |
9 |
// prevent loops
|
10 |
$session->set('google_code_reset'); |
11 |
// reset the google_code
|
12 |
$session->remove('google_code'); |
13 |
$this->redirect(['import']); |
14 |
}
|
Importando Google Above, estoy viendo si Google quiere restablecer sus tokens de API. En este caso, eliminé las cookies en las que las había almacenado y volví a dirigirme al método para comenzar de nuevo.
Hacer la solicitud de un token
A continuación, hago mi primera solicitud a Google a través de su biblioteca cliente PHP:
1 |
// always remove the reset request cookie
|
2 |
$session->remove('google_code_reset'); |
3 |
// build the API request
|
4 |
$redirect_uri=Url::home(true).'address/import'; |
5 |
$session->open(); |
6 |
$client = new \Google_Client(); |
7 |
$client -> setApplicationName('Meeting Planner'); |
8 |
$client -> setClientid( Yii::$app->components['authClientCollection']['clients']['google']['clientId']); |
9 |
$client -> setClientSecret(Yii::$app->components['authClientCollection']['clients']['google']['clientSecret']); |
10 |
$client -> setRedirectUri($redirect_uri); |
11 |
$client -> setAccessType('online'); |
12 |
$client -> setScopes('https://www.google.com/m8/feeds'); |
13 |
$googleImportUrl = $client -> createAuthUrl(); |
La biblioteca de Google PHP está en beta. De hecho, PHP generalmente es una idea de último momento para Google. Por lo tanto, no siempre es fácil trabajar en PHP con sus API.
Observe arriba que $redirect_uri
de Google es el mismo método otra vez:
'address/import'
.
A continuación, tratamos de colocar el token del parámetro de consulta en una cookie:
1 |
// moves returned code to session variables and returns here
|
2 |
if (isset($_GET['code'])) |
3 |
{
|
4 |
$auth_code = $_GET['code']; |
5 |
$session->set('google_code',$auth_code); |
6 |
header('Location: '.Url::home(true).'address/import'); |
7 |
// do not remove - breaks the API
|
8 |
exit; // do not replace with yii app end |
9 |
// do not remove above exit
|
10 |
} else { |
11 |
$session_code = $session->get('google_code'); |
12 |
if (!isset($session_code)) { |
13 |
$this->redirect( $googleImportUrl); |
14 |
}
|
15 |
}
|
Si obtiene el código de Google, debe configurarlo en la cookie y redirigirlo a la página nuevamente. Eso es extraño para mí.
Alegremente, también encontré que si no creaba un bucle cuando faltaba el código, para volver a la misma página otra vez, no funcionaría de manera consistente.
Luego, construimos y le pedimos a Google que presente al usuario un diálogo de permiso para dar acceso a Meeting Planner a sus contactos:
1 |
if (isset($session_code)) { |
2 |
$auth_code = $session_code; |
3 |
$fields=array( |
4 |
'code'=> urlencode($auth_code), |
5 |
'client_id'=> urlencode(Yii::$app->components['authClientCollection']['clients']['google']['clientId']), |
6 |
'client_secret'=> urlencode(Yii::$app->components['authClientCollection']['clients']['google']['clientSecret']), |
7 |
'redirect_uri'=> urlencode($redirect_uri), |
8 |
'grant_type'=> urlencode('authorization_code'), |
9 |
);
|
10 |
// Requests the access token
|
11 |
$post = ''; |
12 |
foreach($fields as $key=>$value) |
13 |
{
|
14 |
$post .= $key.'='.$value.'&'; |
15 |
}
|
16 |
$post = rtrim($post,'&'); |
17 |
$result = $address->curl('https://accounts.google.com/o/oauth2/token',$post); |
18 |
$response = json_decode($result); |
19 |
if (isset($response->error)) { |
20 |
if ($response->error_description == 'Code was already redeemed.') { |
21 |
$session->remove('google_code'); |
22 |
return $this->redirect(['import']); |
23 |
}
|
24 |
if ($response->error_description == 'Invalid code.') { |
25 |
$session->remove('google_code'); |
26 |
return $this->redirect(['import']); |
27 |
}
|
28 |
var_dump($response); |
29 |
echo Yii::t('frontend','There was an error. Please contact support.'); |
30 |
}
|
31 |
if (isset($response->access_token) || empty($response->access_token)) { |
32 |
$accesstoken = $response->access_token; |
33 |
} else { |
34 |
echo Yii::t('frontend','There was an error. No access token. Please contact support.'); |
35 |
}
|



Tuve
que agregar mucha administración de errores para descubrir por qué no
funcionaba y lograr que todo esto funcionara de manera consistente.
Lo adivinaste, si hay una condición de error, a menudo me redirecciona a este mismo método de controlador.
Procesamiento de datos devueltos
Cuando todo funciona, el código fácil y divertido procesa los datos. Actualmente, tomamos 1,000 entradas cinco veces, construyendo repetidamente solicitudes de paginación que, por supuesto, se envían a esta URL:
1 |
$url = 'https://www.google.com/m8/feeds/contacts/default/full?max-results='.$max_results.'&start-index='.$startIndex.'&alt=json&v=3.0&oauth_token='.$accesstoken; |
2 |
$xmlresponse = $address->curl($url); |
Traducir el
XML de Google (que también es complejo, con nombres de variables
impares para desarrolladores de PHP, por ejemplo, claves como $contact['gd$email'][0]['address']
con un signo de dólar en el medio.
A continuación, hacemos cada solicitud, ejecutando los datos JSON y tomando los nombres de los contactos para agregarlos a la tabla de direcciones:
1 |
// Requests the data
|
2 |
$startIndex = 1; |
3 |
$request_data = true; |
4 |
$max_results = Address::CONTACTS_PAGE_SIZE; |
5 |
$numberPages = 0; |
6 |
while ($request_data && $numberPages <5) { |
7 |
//echo 'calling with startIndex: '.$startIndex.'<br />';
|
8 |
$url = 'https://www.google.com/m8/feeds/contacts/default/full?max-results='.$max_results.'&start-index='.$startIndex.'&alt=json&v=3.0&oauth_token='.$accesstoken; |
9 |
$xmlresponse = $address->curl($url); |
10 |
$contacts = json_decode($xmlresponse,true); |
11 |
if (!isset($contacts['feed']['entry'])) { |
12 |
//var_dump ($url);
|
13 |
//var_dump ($xmlresponse);
|
14 |
exit; |
15 |
}
|
16 |
$resultsCount =count($contacts['feed']['entry']); |
17 |
//echo 'count: '.$resultsCount.'<br />';
|
18 |
//var_dump (count($contacts['feed']['entry']));
|
19 |
// process out contacts without email addresses
|
20 |
//$return = array();
|
21 |
if ($resultsCount>0) { |
22 |
foreach($contacts['feed']['entry'] as $contact) { |
23 |
if (isset($contact['gd$email'])) { |
24 |
$temp = array ( |
25 |
'firstname' => (isset($contact['gd$name']['gd$givenName']['$t'])?$contact['gd$name']['gd$givenName']['$t']:''), |
26 |
'lastname' => (isset($contact['gd$name']['gd$familyName']['$t'])?$contact['gd$name']['gd$familyName']['$t']:''), |
27 |
'fullname'=> $contact['title']['$t'], |
28 |
'email' => $contact['gd$email'][0]['address'], |
29 |
);
|
30 |
//$return[]=$temp;
|
31 |
$address->add($temp); |
32 |
} else { |
33 |
continue; |
34 |
}
|
35 |
}
|
36 |
if ($resultsCount<$max_results) { |
37 |
Yii::$app->getSession()->setFlash('success', Yii::t('backend','Your contacts have been imported.')); |
38 |
return $this->redirect(['/friend','tab'=>'address']); |
39 |
}
|
40 |
}
|
41 |
//var_dump($return);
|
42 |
$numberPages++; |
43 |
$startIndex+=$max_results; |
44 |
}
|
Trabajar con la API de Contactos de Google fue muy difícil, no estaba bien documentado, y desperdiciaba mucho tiempo para mí. Si bien he trabajado con muchas API con éxito, sé que no soy un experto en esto. Debido a problemas de seguridad, realmente no quiero golpear a Google por esto; en muchos de estos casos, probablemente saben por qué lo hacen de ciertas maneras.
Pero está bien burlarse un poco, ¿verdad?
En primer lugar, todo el mundo en Google es un genio, y lo probaron de nuevo adquiriendo API.ai, un servicio brillante que vincula su botón de registro con su formulario de inicio de sesión. Realmente, lo hicieron:
Estoy seguro de que el equipo de diligencia debida de Google vio en él un genio que permanece más allá de los mortales como yo. Deben haber dicho: "¡Guau, los programadores de API.ai son genios como nuestros equipos de AdSense y DFP! ¡Vamos a agregarlos al alfabeto!"
Como puedo estar hablando con inversores angélicos y de VC sobre Meeting Planner en el futuro, quiero ser humilde. Pero me horrorizaría si mi página de inicio hiciera esto y uno de mis posibles inversores lo notara.
El nuevo Alphabet (la nueva compañía matriz de Google) es tan indulgente.
Extender el formulario Agregar Participantes
Finalmente, echemos un vistazo al código detrás del Formulario de Agregar Participantes extendido. Básicamente, estoy tomando los correos electrónicos de la tabla de amigos y luego los correos electrónicos de la tabla de direcciones:
1 |
$friendsEmail=[]; |
2 |
$friendsId=[]; |
3 |
$fq = Friend::find()->where(['user_id'=>Yii::$app->user->getId()])->all(); |
4 |
// to do - add a display name field for right side of input
|
5 |
$fa = Address::find() |
6 |
->select(['id','email']) |
7 |
->where(['user_id'=>Yii::$app->user->getId()]) |
8 |
->limit(5000) |
9 |
->all(); |
10 |
foreach ($fq as $f) { |
11 |
$friendsEmail[]=$f->friend->email; // get constructed name fields |
12 |
$friendsId[]=$f->id; |
13 |
}
|
14 |
foreach ($fa as $f) { |
15 |
$friendsEmail[]=$f->email; // get constructed name fields |
16 |
$friendsId[]=$f->id; |
17 |
}
|
18 |
if (count($friendsEmail)>0) { |
19 |
?>
|
20 |
<p><strong>Choose From Your Friends</strong></p> |
21 |
<select class="combobox input-large form-control" id="participant-email" name="Participant[email]"> |
22 |
<option value="" selected="selected"><?= Yii::t('frontend','type or click to choose friends') // chg meetingjs if change string ?></option> |
23 |
<?php |
24 |
foreach ($friendsEmail as $email) { |
25 |
?>
|
26 |
<option value="<?= $email;?>"><?= $email;?></option> |
27 |
<?php
|
28 |
}
|
29 |
?>
|
30 |
<?php
|
31 |
}
|
32 |
?>
|
33 |
</select>
|
Sin embargo, el valor de la opción es solo el correo electrónico, ya que se hubiera vuelto más complicado designar qué tipo de amigo es este (de la tabla Amigo o de la Dirección).
No estoy muy orgulloso del código y el enfoque anterior, pero corriendo hacia la versión beta, hice concesiones para hacerlo.
Con 5.000 contactos de Google en el menú desplegable de mis amigos, el rendimiento es más lento. Probablemente necesite vincular mejor el control a una búsqueda de base de datos AJAX pronto.
Y perdí mucho tiempo al principio al tratar de ampliar la tabla de Amigos para incluir a las personas a las que invité, así como a los Contactos de Google. Sin embargo, esto se convirtió en un lío de consultas de bases de datos relacionadas difíciles de gestionar. Las relaciones de la tabla de usuario de la tabla Amigos comenzaron a fallar para las filas de Contactos donde serían nulas, y esto realmente resultó ser muy difícil de resolver. Gestionar la eliminación de las claves externas existentes hacia arriba y hacia abajo a través de migraciones también es traicionero.
Pensamientos finales
Estas características fueron excelentes ejemplos de los desafíos de confiar en las API con documentación deficiente en el idioma elegido y de hacer concesiones en la arquitectura del código por el momento para lanzar las características a un cronograma de lanzamiento (optando por no cortarlas).
Y ciertamente todavía hay problemas con el desempeño de
agregar
participante y la UX de la página de amigos que deben
corregirse.
Honestamente, el alcance de Meeting Planner ha llegado al punto en que hacerlo como una persona es desalentador. Y sería útil tener más recursos (es decir, miembros del equipo).
Finalmente, si aún no lo ha hecho, programe su primera reunión con Meeting Planner ahora mismo. Déjame saber lo que piensas en los comentarios a continuación. También puede comunicarse conmigo @reifman. Siempre estoy abierto a nuevas ideas de características y sugerencias de temas para futuros tutoriales.
También se está preparando un tutorial sobre crowdfunding, así que por favor siga nuestra página WeFunder Meeting Planner.
Estén
atentos para todo esto y más próximos tutoriales visitando la serie
Construyendo su startup con PHP.