Spanish (Español) translation by Elías Nicolás (you can also view the original English article)



Este tutorial es parte de la serie Construyendo tu Startup con PHP en Envato Tuts +. En esta serie, te estaré guiando como poner en marcha un startup desde el concepto hasta la realidad utilizando mi aplicación de Meeting Planner como un ejemplo de la vida real. Cada paso en el camino, publicaré el código de Meeting Planner como ejemplos de código abierto de los que puedes aprender. Voy también abordaré las cuestiones de negocios relacionados con la puesta en marcha que puedan surgir.
Como se acerca al lanzamiento alfa de Meeting Planner, necesitamos una manera de abordar las solicitudes de apoyo con los usuarios y monitorear la actividad. En otras palabras, necesitamos construir un panel de control (dashboard) administrativo con gestión de usuarios e informes. En conversaciones con un consejero, ya comentamos que al acercarme a potenciales inversores, tendré que tener excelentes datos que detallan el comportamiento de los usuarios y el crecimiento del servicio.
En el episodio de hoy, vamos a construir las bases para nuestro panel de control (dashboard) administrativo y crear en él algunos reportes iniciales en vivo e históricos. Por ejemplo, vamos a saber cuántas personas se ha registrado en cualquier momento, cuántas reuniones se han programado y qué porcentaje de los participantes invitados le gusta el servicio lo suficiente para ir a organizar su propia reunión. Realmente ha sido bastante divertido construir estas cosas y ver los datos, incluso si estamos en pre-lanzamiento.
Si aún no has probado Meeting Planner (y quieres aparecer en los datos agregado), sigue adelante y programa tu primera reunión. Yo participo en los comentarios más abajo, así que Dime lo que piensas! También puede contactarme en Twitter @reifman. Me interesa especialmente si quieres sugerir nuevas funcionalidades o temas para futuros tutoriales.
Como recordatorio, todo el código para el Meeting Planner está escrito en el framework de Yii2 para PHP. Si desea más información sobre Yii2, revisa nuestra serie paralela programación con Yii2.
Construyendo la base del Panel de Control (dashboard)
La plantilla avanzada de Yii
Yii2 ofrece laparte frontal y posterior de sitios web dentro de su configuración de aplicación avanzada. Puedes leer más sobre él en mi tutorial de Envato Tuts +, Como programar con Yii2: usando la plantilla de aplicación avanzada. Basicamente, la plantilla avanzada frontal (front-end) del sitio proporciona funcionalidad orientada a los usuario, y la parte posterior (back-end) del sitio es para el panel de control y administración del sitio.
Para activarlo, sólo necesito configurar Apache en mi ordenador local dentro de mi MAMP y en mi Ubuntu server de producción. Por ejemplo, aquí está la configuración de Apache en el servidor de producción para cargar el sitio web /backend/web
<IfModule mod_ssl.c> <VirtualHost *:443> ServerName your-administration-site.com DocumentRoot "/var/www/mp/backend/web" <Directory "/var/www/mp/backend/web"> # use mod_rewrite for pretty URL support RewriteEngine on # If a directory or a file exists, use the request directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Otherwise forward the request to index.php RewriteRule . index.php </Directory> SSLCertificateFile /etc/letsencrypt/live/meetingplanner.io/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/meetingplanner.io/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf SSLCertificateChainFile /etc/letsencrypt/live/meetingplanner.io/chain.pem </VirtualHost> </IfModule>
Configurando el codigo de la parte del servidor (Back-end) de nuestro sitio
A continuación, construí un nuevo diseño para el código del lado del servidor (back-end) del sitio basado en el códio del lado del usuario (front-end) del sitio, pero con diferentes opciones de menú. He decidido que la página inicial del sitio redirija a una página de estadísticas en tiempo real. Y los menús ofrecen enlaces a datos en tiempo real, datos de ayer a la medianoche y datos históricos. Explicar un poco más de esto a medida que avancemos.



Aquí está el \backend\views\layouts\main.php con el menú:
<body> <?php $this->beginBody() ?> <div class="wrap"> <?php NavBar::begin([ 'brandLabel' => Yii::t('backend','Meeting Planner'), 'brandUrl' => 'https://meetingplanner.io', 'options' => [ 'class' => 'navbar-inverse navbar-fixed-top', ], ]); $menuItems[] = [ 'label' => 'Real Time', 'items' => [ ['label' => Yii::t('frontend','Usage'), 'url' => ['/data/current']], ] ]; $menuItems[] = [ 'label' => 'Yesterday', 'items' => [ ['label' => Yii::t('frontend','User Data'), 'url' => ['/user-data']], ] ]; $menuItems[]=[ 'label' => 'Historical', 'items' => [ ['label' => Yii::t('frontend','Statistics'), 'url' => ['/historical-data']], ], ]; if (Yii::$app->user->isGuest) { $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; } else { $menuItems[] = [ 'label' => 'Account', 'items' => [ ['label' => 'Logout (' . Yii::$app->user->identity->username . ')', 'url' => ['/site/logout'], 'linkOptions' => ['data-method' => 'post'], ], ], ]; } echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], 'items' => $menuItems, ]); NavBar::end(); ?> <div class="container"> <?= Breadcrumbs::widget([ 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], ]) ?> <?= $content ?> </div> </div>
Construyendo el reporte inicial
Para mi reporte de estadística inicial, me enfoqué en simples datos en tiempo real y datos históricos detallados. Por ejemplo, datos en tiempo real te dirán el número de usuarios y reuniones construidos en el sistema y su estado.
Los datos históricos te dirán el número de usuarios y reuniones completadas así como otros datos interesantes, sobre todo las curvas de crecimiento que yo y los posibles inversores queremos ocuparnos.
Datos en tiempo real
La página de datos en tiempo real necesita mostrar una instantánea en vivo de lo que está sucediendo en el sitio. Inicialmente, quería saber:
- ¿Cuántas reuniones existen en el sistema?
- ¿Cuántos usuarios están?
- ¿Cuál es su estado?
Para lograr esto, he creado un controlador DataController.php y modelo Data.php en el back-end. También di un paso adelante y en lugar de crear HTML crudo en mi vista para mostrar esto, he creado ActiveDataProviders de mis consultas y alimento a los diseño de columnas de Yii; el resultado se ve mejor y es más sencillo de construir y mantener.
Este código de consulta el número de sesiones en el sistema agrupado por su condición de:
public static function getRealTimeData() { $data = new \stdClass(); $data->meetings = new ActiveDataProvider([ 'query' => Meeting::find() ->select(['status,COUNT(*) AS dataCount']) //->where('approved = 1') ->groupBy(['status']), 'pagination' => [ 'pageSize' => 20, ], ]);
Este código en /backend/views/data/current.php muestra:
<?php /* @var $this yii\web\View */ use yii\grid\GridView; use common\models\User; use frontend\models\Meeting; $this->title = Yii::t('backend','Meeting Planner'); ?> <div class="site-index"> <div class="body-content"> <h1>Real Time Data</h1> <h3>Meetings</h3> <?= GridView::widget([ 'dataProvider' => $data->meetings, 'columns' => [ [ 'label'=>'Status', 'attribute' => 'status', 'format' => 'raw', 'value' => function ($model) { return '<div>'.Meeting::lookupStatus($model->status).'</div>'; }, ], 'dataCount', ], ]); ?>
Aparece así (los datos son pocos ya que el sitio no se ha lanzado todavía!):



Luego, he creado unas pocas más consultas en tiempo real, y el resto de la página aparece as:



Con respecto a las columnas arriba de personas activas y a través de invitación, si invitas a una persona a una reunión, lo contamos como un usuario a través de invitación hasta que crean una contraseña o vinculan su cuenta social. Hasta entonces, su único acceso al Meeting Planner es a través de un link de invitación por correo electrónico y su id de autenticación.
Obviamente, Expandiremos las opciones de informes en tiempo real con la evolución del proyecto.
Reporting de datos históricos
Generando informes históricos para actividades de todo el sistema resulta un poco más trabajoso. He decidido crear algunas capas de recopilación de datos dependientes.
La capa inferior es una tabla de datos de usuario que resume el estado histórico de actividad de una cuenta de una persona hasta un día específico a la medianoche. Básicamente, haremos esto todas las noches.
La capa superior es la tabla de HistoricalData que basa sus cálculos usando la tabla de datos de usuario desde la noche anterior.
También se debe escribir el código para las dos tablas desde cero ya que el servicio ha estado un poco activo durante varios meses.
Te guiar a través de cómo lo hice. El resultado torna muy bien.
Creación de tablas de migraciones
Aquí está la tabla de migración de datos de usuario, que contiene los datos que quería calcular todas las noches para ayudar a los cálculos históricos:
public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%user_data}}', [ 'id' => Schema::TYPE_PK, 'user_id' => Schema::TYPE_BIGINT.' NOT NULL', 'is_social' => Schema::TYPE_SMALLINT.' NOT NULL', 'invite_then_own' => Schema::TYPE_SMALLINT.' NOT NULL', 'count_meetings' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_last30' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meeting_participant' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meeting_participant_last30' => Schema::TYPE_INTEGER.' NOT NULL', 'count_places' => Schema::TYPE_INTEGER.' NOT NULL', 'count_friends' => Schema::TYPE_INTEGER.' NOT NULL', 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions); $this->addForeignKey('fk_user_data_user_id', '{{%user_data}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); }
Por ejemplo, count_meeting_participant_last30
es cuántas reuniones invitaron a esta persona que en los últimos 30 días.
Aquí está la tabla de migración para HistoricalData — casi todas las columnas de esta tabla deben calcularse de diversas capas de datos:
public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%historical_data}}', [ 'id' => Schema::TYPE_PK, 'date'=> Schema::TYPE_INTEGER.' NOT NULL', 'percent_own_meeting' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_own_meeting_last30' => Schema::TYPE_FLOAT.' NOT NULL', // % of users invited by others who own a meeting 'percent_invited_own_meeting' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_participant' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_participant_last30' => Schema::TYPE_FLOAT.' NOT NULL', 'count_users' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_completed' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_planning' => Schema::TYPE_INTEGER.' NOT NULL', 'count_places' => Schema::TYPE_INTEGER.' NOT NULL', 'average_meetings' => Schema::TYPE_FLOAT.' NOT NULL', 'average_friends' => Schema::TYPE_FLOAT.' NOT NULL', 'average_places' => Schema::TYPE_FLOAT.' NOT NULL', 'source_google' => Schema::TYPE_INTEGER.' NOT NULL', 'source_facebook' => Schema::TYPE_INTEGER.' NOT NULL', 'source_linkedin' => Schema::TYPE_INTEGER.' NOT NULL', ], $tableOptions);
En discusión con mi Asesor, nos dimos cuenta de que posibles inversores querrán saber cómo las personas responden al sitio. He creado una medición de una métrica llamada percent_invited_own_meeting
, el nombre corto para el porcentaje de usuarios invitados a la primera reunión que les ha gustado lo suficiente para utilizarlo para programar sus propias reuniones en el futuro. Analizaré más sobre los cálculos un poco más abajo.
Toas las migraciones residen en /console/migrations. Así es como lucen cuando ejecutas las migraciones de base de datos.
$ ./yii migrate/up Yii Migration Tool (based on Yii v2.0.8) Total 2 new migrations to be applied: m160609_045838_create_user_data_table m160609_051532_create_historical_data_table Apply the above migrations? (yes|no) [no]:yes *** applying m160609_045838_create_user_data_table > create table {{%user_data}} ... done (time: 0.003s) > add foreign key fk_user_data_user_id: {{%user_data}} (user_id) references {{%user}} (id) ... done (time: 0.004s) *** applied m160609_045838_create_user_data_table (time: 0.013s) *** applying m160609_051532_create_historical_data_table > create table {{%historical_data}} ... done (time: 0.003s) *** applied m160609_051532_create_historical_data_table (time: 0.005s) 2 migrations were applied. Migrated up successfully.
Tomando los datos de informes
Cada noche después de la medianoche, una tarea en segundo plano va a calcular las estadísticas de la noche anterior. Este es el método de fondo:
public function actionOvernight() { $since = mktime(0, 0, 0); $after = mktime(0, 0, 0, 2, 15, 2016); UserData::calculate(false,$after); HistoricalData::calculate(false,$after); }
Configuré una tarea de cron para ejecutar actionOvernight a la 1:15 AM todos los días. Nota: Cuando estás enfocado intensamente en programar día y noche en startup, una tarea de cron es todo acerca de actionOvernight.
Para construir la historia del pasado, he creado una función de recalc()
una sola vez. Esto limpia las tablas y construye cada tabla como si sucediera un día a la vez.
public static function recalc() { UserData::reset(); HistoricalData::reset(); $after = mktime(0, 0, 0, 2, 15, 2016); $since = mktime(0, 0, 0, 4, 1, 2016); while ($since < time()) { UserData::calculate($since,$after); HistoricalData::calculate($since,$after); // increment a day $since+=24*60*60; } }
Nota:
El tiempo after
es una solución para excluir a algunos de los
primeros usuarios que se registraron antes de poder programar una
reunión. Quería
que los datos históricos reflejaran una representación más precisa de
la actividad reciente (actualmente hay un par de cientos de cuentas más
antiguas sin actividad). Lo más probable es que quitare esto proximamente.
Cálculo de la tabla de datos de usuario
Aquí está el código que rellena la tabla UserData
todas las noches:
public static function calculate($since=false,$after = 0) { if ($since===false) { $since = mktime(0, 0, 0); } $monthago = $since-(60*60*24*30); $all = User::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->all(); foreach ($all as $u) { // create new record for user or update old one $ud = UserData::find()->where(['user_id'=>$u->id])->one(); if (is_null($ud)) { $ud = new UserData(); $ud->user_id = $u->id; $ud->save(); } $user_id = $u->id; // count meetings they've organized $ud->count_meetings = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_meetings_last30 = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->andWhere('created_at>='.$monthago)->count(); // count meetings they were invited to $ud->count_meeting_participant = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_meeting_participant_last30 = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->andWhere('created_at>='.$monthago)->count(); // count places and Friends $ud->count_places = UserPlace::find()->where(['user_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_friends = Friend::find()->where(['user_id'=>$user_id])->andWhere('created_at<'.$since)->count(); // calculate invite than Own - participant first, then organizer $first_invite = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->orderby('created_at asc')->one(); $first_organized = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->orderby('created_at asc')->one(); $ud->invite_then_own =0; if (!is_null($first_invite) && !is_null($first_organized)) { if ($first_invite->created_at < $first_organized->created_at && $first_organized->created_at < $since) { // they were invited as a participant earlier than they organized their own meeting $ud->invite_then_own =1; } } if (Auth::find()->where(['user_id'=>$user_id])->count()>0) { $ud->is_social =1; } else { $ud->is_social =0; } $ud->update(); } }
En su mayor parte se trata de contar totales para los usuarios de reuniones, lugares, amigos y en algunos casos dentro de los intervalos de tiempo de los últimos 30 días.
Este es el código que detecta si este usuario eligió programar una reunión usando el servicio después de haber sido invitado:
$ud->invite_then_own =0; if (!is_null($first_invite) && !is_null($first_organized)) { if ($first_invite->created_at < $first_organized->created_at && $first_organized->created_at < $since) { // they were invited as a participant earlier than they organized their own meeting $ud->invite_then_own =1; } }
Cálculo de los datos históricos
Aquí está el código que aprovecha UserData
para rellenar HistoricalData
:
public static function calculate($since = false,$after=0) { if ($since === false) { $since = mktime(0, 0, 0); } // create new record for date or update existing $hd = HistoricalData::find()->where(['date'=>$since])->one(); if (is_null($hd)) { $hd = new HistoricalData(); $hd->date = $since; $action = 'save'; } else { $action = 'update'; } // calculate $count_meetings_completed $hd->count_meetings_completed = Meeting::find()->where(['status'=>Meeting::STATUS_COMPLETED])->andWhere('created_at<'.$since)->count();; // calculate $count_meetings_planning $hd->count_meetings_planning = Meeting::find()->where('status<'.Meeting::STATUS_COMPLETED)->andWhere('created_at<'.$since)->count();; // calculate $count_places $hd->count_places = Place::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); // calculate $source_google $hd->source_google = Auth::find()->where(['source'=>'google'])->count(); // calculate $source_facebook $hd->source_facebook = Auth::find()->where(['source'=>'facebook'])->count(); // calculate $source_linkedin $hd->source_linkedin = Auth::find()->where(['source'=>'linkedin'])->count(); // total users $total_users = UserData::find()->count(); // calculate $count_users $hd->count_users = $total_users; //User::find()->where('status<>'.User::STATUS_DELETED)->andWhere('created_at>'.$after)->count(); $total_friends = Friend::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); $total_places = Place::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); if ($total_users >0) { $hd->average_meetings = ($hd->count_meetings_completed+$hd->count_meetings_planning)/$total_users; $hd->average_friends = $total_friends/$total_users; $hd->average_places = $total_places/$total_users; $hd->percent_own_meeting = UserData::find()->where('count_meetings>0')->count() / $total_users; $hd->percent_own_meeting_last30 = UserData::find()->where('count_meetings_last30>0')->count() / $total_users; $hd->percent_participant = UserData::find()->where('count_meeting_participant>0')->count() / $total_users; $hd->percent_participant_last30 = UserData::find()->where('count_meeting_participant_last30>0')->count() / $total_users; $query = (new \yii\db\Query())->from('user_data'); $sum = $query->sum('invite_then_own'); $hd->percent_invited_own_meeting=$sum/$total_users; } if ($action=='save') { $hd->save(); } else { $hd->update(); } }
Está resumiendo totales y calculando porcentajes y promedios.
Así es como se ve el producto acabado:



A pesar de que estamos viendo el análisis de sólo el uso pre-alfa, los datos son intrigantes, y la utilidad potencial de esto parece excelente. Y, por supuesto, será fácil ampliar la recopilación y el análisis de datos utilizando el código de fundación que compartí hoy.
Por cierto, el porcentaje de usuarios invitados que van a programar sus propias reuniones es de alrededor del 9% (pero es un pequeño conjunto de datos).
Probablemente se esté preguntando si podemos graficar estas columnas. Espero abordar eso en un tutorial de seguimiento, que siempre requiere interacción con las diosas editoriales. Sólo para su informacion, no todo el mundo se aleja de esas conversaciones. También le pediré que me permita escribir sobre características de administración como deshabilitar usuarios, reenviar contraseñas, etc.



Si no escuchas más de mí, sé que el Señor de la Luz ha encontrado un uso para mí.
¿Que sigue?
Como se mencionó, actualmente estoy trabajando febrilmente para preparar Meeting Planner para la liberación alfa. Estoy principalmente centrado en las mejoras clave y características que harán que la versión alfa vaya sin problemas.
Estoy siguiendo todo en Asana ahora, de lo cual escribiré en un próximo tutorial; Ha sido increíblemente útil. También hay algunas nuevas características interesantes todavía por estrenar. (Como profesor de yoga, creo que Asana es el peor nombre de producto de todos los tiempos. Básicamente han tomado un término común en el yoga pronunciado āsana o ah-sana y cambiaron la pronunciación a a-sauna—y poner eso en sus videos introductorios. No fue fácil consultar el año pasado hablando con los miembros del equipo del cliente sobre lo que ponen en una sauna y hablar con personas de yoga sobre āsana. Pero yo divago.)
También estoy empezando a concentrarme más en el próximo esfuerzo de recolección de inversiones con Meeting Planner. Estoy empezando a experimentar con WeFunder basado en la implementación de las nuevas reglas de crowdfunding de la SEC. Por favor considere seguir nuestro perfil. También escribiré más sobre esto en un futuro tutorial.
Una
vez más, mientras espera más episodios, programe su primera reunión y
pruebe las plantillas con sus amigos con buzones de correo de Gmail. Además,
le agradecería que compartiera su experiencia a continuación en los
comentarios, y siempre estoy interesado en sus sugerencias. También puede contactarme directamente en Twitter @reifman. También puede publicarlos en el sitio de soporte de Meeting Planner.
Vea los próximos tutoriales en la serie Creando su startup con PHP.
Enlaces relacionados
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weekly