Ce tutoriel fait partie de la Construire votre démarrage avec la série PHP sur Envato Tuts +. Dans cette série, je vous guide dans le lancement d’une startup du concept à la réalité en utilisant mes Planificateur de réunion application comme exemple de la vie réelle. À chaque étape du processus, je publierai le code de Meeting Planner sous forme d’exemples open source à partir desquels vous pourrez apprendre. Je traiterai également les problèmes liés au démarrage au fur et à mesure qu'ils surviennent.
Alors que Meeting Planner approche du lancement de l'alpha, nous avons besoin d'un moyen de répondre aux demandes de support des utilisateurs et de surveiller l'activité. En d'autres termes, nous devons créer un tableau de bord administratif avec gestion des utilisateurs et création de rapports. Lors de discussions avec un conseiller, nous avons expliqué que, à l'approche des investisseurs potentiels, je devrais disposer d'excellentes données détaillant le comportement des utilisateurs et la croissance du service..
Dans l'épisode d'aujourd'hui, nous allons jeter les bases de notre tableau de bord administratif et créer et créer certains des rapports initiaux en direct et historiques. Par exemple, nous saurons combien de personnes se sont inscrites à tout moment, combien de réunions ont été programmées et quel pourcentage de participants invités apprécient suffisamment le service pour pouvoir organiser leur propre réunion. Construire ce genre de choses et voir les données est vraiment amusant, même si nous sommes en pré-lancement.
Si vous n'avez pas encore essayé Meeting Planner (et voulez vous montrer dans les données agrégées vous-même), allez-y et planifiez votre première réunion. Je participe aux commentaires ci-dessous, alors dites-moi ce que vous en pensez! Vous pouvez également me joindre sur Twitter @reifman. Je suis particulièrement intéressé si vous souhaitez suggérer de nouvelles fonctionnalités ou de nouveaux sujets pour de futurs tutoriels..
Pour rappel, tout le code pour Meeting Planner est écrit dans le framework Yii2 pour PHP. Si vous souhaitez en savoir plus sur Yii2, consultez notre série parallèle Programmer avec Yii2.
Yii2 propose des sites Web de front et de back-office dans sa configuration d'application avancée. Vous pouvez en savoir plus à ce sujet dans mon tutoriel Envato Tuts +, Comment programmer avec Yii2: Utiliser le modèle d'application avancée. Pour l'essentiel, le site frontal du modèle avancé fournit des fonctionnalités destinées aux personnes, et le site principal est conçu pour le tableau de bord et le site d'administration d'un service..
Pour l'activer, je devais simplement configurer des sites Apache dans mon environnement hôte local MAMP et sur mon serveur de production Ubuntu. Par exemple, voici la configuration Apache sur le serveur de production pour charger le / backend / web
site:
Nom du serveur your-administration-site.com DocumentRoot "/ var / www / mp / backend / web" # utilisez mod_rewrite pour la jolie prise en charge des URL RewriteEngine on # Si un répertoire ou un fichier existe, utilisez directement la demande RewriteCond% REQUEST_FILENAME! -f RewriteCond% REQUEST_FILENAME! -d # Sinon, transférez la demande à index.php RewriteRule. index.php SSLCertificateFile /etc/letsencrypt/live/meetingplanner.io/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/meetingplanner.io/privkey.pem Inclure /etc/letsencrypt/options-ssl-apache.conf SSLCertateChain /meetingplanner.io/chain.pem
Ensuite, j'ai construit une nouvelle disposition pour le site principal basé sur le site principal, mais avec différentes options de menu. J'ai décidé que la page d'accueil redirigerait vers une page de statistiques en temps réel. Et les menus proposeraient des liens vers des données en temps réel, des données d’hier à minuit et des données historiques. Je vais en expliquer un peu plus à mesure que nous procédons.
Voici le \ backend \ views \ layouts \ main.php avec le menu:
beginBody ()?>Yii :: t ('backend', 'Meeting Planner'), 'brandUrl' => 'https://meetingplanner.io', 'options' => ['class' => 'navbar-inverse navbar-fixed-top ',],]); $ menuItems [] = ['label' => 'Temps réel', 'items' => [['label' => Yii :: t ('frontend', 'Usage'), 'url' => ['/ données / courant ']],]]; $ menuItems [] = ['label' => 'Yesterday', 'items' => [['label' => Yii :: t ('frontend', 'User Data'), 'url' => ['/ données d'utilisateur']], ] ]; $ menuItems [] = ['label' => 'Historique', 'items' => [['label' => Yii :: t ('interface', 'Statistiques'), 'url' => ['/ historique -Les données']], ], ]; if (Yii :: $ app-> user-> isGuest) $ menuItems [] = ['label' => 'Login', 'url' => ['/ site / login']]; else $ menuItems [] = ['label' => 'Compte', 'items' => [['label' => 'Logout ('. Yii :: $ app-> utilisateur-> identité-> nom d'utilisateur. ')', 'url' => ['/ site / logout'], 'linkOptions' => ['data-method' => 'post'],],],]; echo Nav :: widget (['options' => ['class' => 'navbar-nav navbar-right'], 'items' => $ menuItems,]); NavBar :: end (); ?>= Breadcrumbs::widget([ 'links' => isset ($ this-> params ["breadcrumbs"])? $ this-> params ['breadcrumbs']: [],])?> = $content ?>
Pour mes rapports statistiques initiaux, je me suis concentré sur des données simples en temps réel et des données historiques détaillées. Par exemple, les données en temps réel vous indiqueraient le nombre d'utilisateurs et de réunions construits sur le système à ce jour et leur statut..
Les données historiques vous indiqueraient le nombre d’utilisateurs et de réunions terminées au fil du temps, ainsi que d’autres données intéressantes, en particulier les courbes de croissance qui pourraient intéresser les investisseurs potentiels.
La page de données en temps réel doit afficher un instantané en direct de ce qui se passe sur le site. Au départ, je voulais savoir:
Pour ce faire, j'ai créé un modèle back-end DataController.php et Data.php. J'ai également fait un pas en avant et plutôt que de créer du HTML brut dans ma vue pour l'afficher, j'ai créé ActiveDataProviders à partir de mes requêtes et les ai transférées dans les widgets de grille de Yii; le résultat est plus beau et plus simple à construire et à entretenir.
Ce code demande le nombre de réunions dans le système regroupées par leur statut:
fonction statique publique getRealTimeData () $ data = new \ stdClass (); $ data-> meetings = new ActiveDataProvider (['query' => Meeting :: find () -> select (['status, COUNT (*) AS dataCount']) // -> où ('approuvé = 1') -> groupBy (['status']), 'pagination' => ['pageSize' => 20,],]);
Ce code dans /backend/views/data/current.php l'affiche:
title = Yii :: t ('backend', 'Meeting Planner'); ?>Données en temps réel
Des réunions
= GridView::widget([ 'dataProvider' => $ data-> meetings, 'columns' => [['label' => 'Status', 'attribut' => 'status', 'format' => 'raw', 'value' => function ($ model) revenir ''.Meeting :: lookupStatus ($ model-> status).''; ,], 'dataCount',],]); ?>Ça ressemble à ça (les données sont petites car le site n'a pas encore été lancé!):
Ensuite, j'ai créé quelques requêtes supplémentaires en temps réel, et le reste de la page ressemble à ceci:
En ce qui concerne la Personnes actives et Via invite colonnes ci-dessus, si vous invitez une personne à une réunion, nous la comptons comme un utilisateur via Inviter jusqu'à ce qu'elle crée un mot de passe ou associe son compte social. Jusque-là, leur seul accès au planificateur de réunion se fait via votre lien d'invitation par courrier électronique et son identifiant d'authentification..
Évidemment, j'élargirai les options de rapport en temps réel à mesure que le projet évoluera..
Rapporter des données historiques
La génération de rapports historiques pour les activités à l'échelle du système s'est avérée un peu plus complexe. J'ai décidé de créer des couches de collecte de données dépendantes.
La couche inférieure est un tableau UserData qui résume l'état de l'activité du compte historique d'une personne jusqu'à un jour spécifique à minuit. Essentiellement, nous ferons cela tous les soirs.
La couche supérieure est la table HistoricalData qui construit ses calculs à l'aide de la table UserData à partir de la nuit précédente..
J'avais aussi besoin d'écrire du code qui a construit les deux tables à partir de zéro car notre service était un peu actif depuis plusieurs mois..
Je vais vous guider à travers comment j'ai fait ça. Le résultat s'est plutôt bien passé.
Création de migrations de table
Voici la migration de la table pour UserData. Elle contient les données que je voulais calculer tous les soirs pour faciliter les calculs historiques:
fonction publique 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' => Schéma :: TYPE_SMALLINT. 'NOT NULL', 'count_meetings' => Schéma :: TYPE_INTEGER. 'NOT NULL', 'count_meetings_last30' => Schéma :: TYPE_INTEGER. ',' 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' => Schéma :: TYPE_INTEGER. 'NOT NULL', 'updated_at' => Schema :: TYPE_INTEGER. 'NOT NULL', $, Options_table); $ this-> addForeignKey ('fk_user_data_user_id', '% user_data', 'id_utilisateur', '% user', 'id', 'CASCADE', 'CASCADE');Par exemple,
count_meeting_participant_last30
est le nombre de réunions auxquelles cette personne a été invitée au cours des 30 derniers jours.Voici la migration de table pour
Données historiques
-presque toutes les colonnes de ce tableau doivent être calculées à partir de différentes couches de données:fonction publique 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', //% des utilisateurs invités par les autres propriétaires d'une réunion 'percent_invited_own_meeting' => Schéma :: TYPE_FLOAT. 'NON NULL', ' percent_participant '=> Schema :: TYPE_FLOAT.' NOT NULL ',' percent_participant_last30 '=> Schéma :: TYPE_FLOAT.' NOT NULL ',' count_users '=> Schéma :: TYPE_INTEGER.' NOT NULL ',' count_meetings '>> Schema :: TYPE_INTEGER. 'NOT NULL', 'count_meetings_planning' => Schéma :: TYPE_INTEGER. 'NOT NULL', 'count_places' => Schéma :: TYPE_INTEGER. 'NOT NULL', 'average_meetings' => Schéma :: TYPE_FLOAT. ' NOT NULL ',' average_friends '=> Schema :: TYPE_FLOAT.' NOT NULL ',' average_places '=> Schema :: TYPE_FLOAT.' NOT NULL ',' source_google '=> Schéma :: TYPE_INTEGER.' NOT NULL ',' source_facebook '=> Schema :: TYPE_INTEGER.' NOT NULL ',' source_linkedin '=> Schema :: TY PE_INTEGER. 'NOT NULL',], $ tableOptions);En discutant avec mon conseiller, nous avons réalisé que les investisseurs potentiels voudraient savoir comment les internautes réagissent au site. J'ai créé une mesure pour une métrique appelée
percent_invited_own_meeting
, abrégé pour le pourcentage d'utilisateurs invités à leur première réunion qui ont aimé le service suffisamment pour pouvoir l'utiliser pour planifier leur propre réunion dans le futur. Je passerai en revue plus en détail les calculs ci-dessous.Les migrations résident toutes dans / console / migrations. Voici à quoi cela ressemble quand vous exécutez les migrations de base de données.
$ ./yii migrate / up Yii Outil de migration (basé sur Yii v2.0.8) Total 2 nouvelles migrations à appliquer: m160609_045838_create_user_data_table m160609_051532_create_historical_data_table Appliquer les migrations ci-dessus? (yes | no) [no]: yes *** application de m160609_045838_create_user_data_table> créer une table % user_data… done (heure: 0.003s)> ajouter une clé étrangère fk_user_data_user_id: % user_data (user_id) références % user (id)… done (time: 0.004s) *** appliqué m160609_045838_create_user_data_table (time: 0.013s) *** application de m160609_051532_create_historical_data_tata_table> create table % history_data… done (time: 0.003s) ** * appliqué m160609_051532_create_historical_data_table (time: 0.005s) 2 migrations ont été appliquées. Migré avec succès.Rassembler les données de rapport
Chaque nuit après minuit, une tâche en arrière-plan calculera les statistiques de la nuit précédente. Voici la méthode d'arrière-plan:
fonction publique actionOvernight () $ Since = mktime (0, 0, 0); $ after = mktime (0, 0, 0, 2, 15, 2016); UserData :: Calculate (false, $ after); HistoricalData :: Calculate (false, $ after);J'ai mis en place un travail cron à exécuter
actionNuit
à 1h15 tous les jours. Remarque: Lorsque vous êtes concentré sur la programmation jour et nuit de démarrage, un travail cron concerne toutes les actions que vous obtiendrez au cours de la nuit..Pour construire l’histoire du passé, j’ai créé un livre unique
recalculer ()
une fonction. Cela vide les tables et construit chaque table comme s’il se passait un jour à la fois..fonction statique publique recalc () UserData :: reset (); HistoricalData :: reset (); $ after = mktime (0, 0, 0, 2, 15, 2016); $ Since = mktime (0, 0, 0, 4, 1, 2016); while ($ depuis < time()) UserData::calculate($since,$after); HistoricalData::calculate($since,$after); // increment a day $since+=24*60*60;Noter la
après
time est une solution de contournement pour exclure certains des premiers utilisateurs qui se sont inscrits avant de pouvoir planifier une réunion. Je voulais que les données historiques reflètent une description plus précise de l'activité récente (il y a actuellement quelques centaines de comptes plus anciens sans activité). Je vais probablement retirer cela à une date ultérieure.Calcul de la table de données utilisateur
Voici le code qui remplit le
Données d'utilisateur
table de nuit:fonction statique publique calcule ($ puisque = faux, $ après = 0) if ($ depuis === faux) $ depuis = mktime (0, 0, 0); $ monthago = $ depuis- (60 * 60 * 24 * 30); $ all = User :: find () -> where ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->tout(); foreach ($ all as $ u) // crée un nouvel enregistrement pour l’utilisateur ou met à jour l’ancien $ ud = UserData :: find () -> où (['user_id' => $ u-> id]) -> un ( ) if (is_null ($ ud)) $ ud = new UserData (); $ ud-> user_id = $ u-> id; $ ud-> save (); $ user_id = $ u-> id; // compte les réunions qu'ils ont organisées $ ud-> count_meetings = Meeting :: find () -> où (['owner_id' => $ user_id]) -> andWhere ('created_at<'.$since)->compter(); $ ud-> count_meetings_last30 = Meeting :: find () -> où (['owner_id' => $ user_id]) -> etWhere ('created_at<'.$since)->etWhere ('created_at> ='. $ monthago) -> count (); // compte les réunions auxquelles ils ont été invités à $ ud-> count_meeting_participant = Participant :: find () -> Where (['participant_id' => $ user_id]) -> andWhere ('created_at<'.$since)->compter(); $ ud-> count_meeting_participant_last30 = Participant :: find () -> Where (['participant_id' => $ user_id]) -> andWhere ('created_at<'.$since)->etWhere ('created_at> ='. $ monthago) -> count (); // compte les places et amis $ ud-> count_places = UserPlace :: find () -> where (['user_id' => $ user_id]) -> andWhere ('created_at<'.$since)->compter(); $ ud-> count_friends = Friend :: find () -> où (['user_id' => $ user_id]) -> andWhere ('created_at<'.$since)->compter(); // calcule d'abord l'invitation que Propre - participant, puis l'organisateur $ first_invite = Participant :: find () -> where (['participant_id' => $ user_id]) -> andWhere ('created_at<'.$since)->orderby ('created_at asc') -> un (); $ first_organized = Meeting :: find () -> Where (['owner_id' => $ user_id]) -> andWhere ('created_at<'.$since)->orderby ('created_at asc') -> un (); $ 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 ();Il s’agit surtout de compter les totaux des utilisateurs de réunions, lieux, amis et, dans certains cas, dans les intervalles de temps des 30 derniers jours..
Voici le code qui détecte si cet utilisateur a choisi de planifier une réunion à l'aide du service après avoir été invité:
$ 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;Calcul des données historiques
Voici le code qui exploite
Données d'utilisateur
peuplerDonnées historiques
:fonction statique publique calcule ($ puisque = faux, $ après = 0) if ($ depuis === faux) $ depuis = mktime (0, 0, 0); // créer un nouvel enregistrement pour la date ou mettre à jour un répertoire existant $ hd = HistoricalData :: find () -> où (['date' => $ depuis]) -> un (); if (is_null ($ hd)) $ hd = new HistoricalData (); $ hd-> date = $ depuis; $ action = 'save'; else $ action = 'update'; // calcule $ count_meetings_completed $ hd-> count_meetings_completed = Meeting :: find () -> où (['status' => Meeting :: STATUS_COMPLETED]) -> etWhere ('created_at<'.$since)->compter();; // calcule $ count_meetings_planning $ hd-> count_meetings_planning = Réunion :: find () -> où ('status<'.Meeting::STATUS_COMPLETED)->andWhere ('created_at<'.$since)->compter();; // calcule $ count_places $ hd-> count_places = Place :: find () -> où ('created_at>'. $ after) -> etWhere ('created_at<'.$since)->compter(); // calcule $ source_google $ hd-> source_google = Auth :: find () -> où (['source' => 'google']) -> count (); // calcule $ source_facebook $ hd-> source_facebook = Auth :: find () -> où (['source' => 'facebook']) -> count (); // calcule $ source_linkedin $ hd-> source_linkedin = Auth :: find () -> où (['source' => 'linkedin']) -> count (); // total utilisateurs $ total_users = UserData :: find () -> count (); // calcule $ count_users $ hd-> count_users = $ total_users; // User :: find () -> where ('status <>'. User :: STATUS_DELETED) -> andWhere ('created_at>'. $ After) -> count (); $ total_friends = Friend :: find () -> où ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->compter(); $ total_places = Place :: find () -> où ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->compter(); 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 () -> Où ('count_meetings> 0') -> count () / $ total_users; $ hd-> percent_own_meeting_last30 = UserData :: find () -> où ('count_meetings_last30> 0') -> count () / $ total_users; $ hd-> percent_participant = UserData :: find () -> Où ('count_meeting_participant> 0') -> count () / $ total_users; $ hd-> percent_participant_last30 = UserData :: find () -> Où ('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 ();Il récapitule les totaux et calcule les pourcentages et les moyennes.
Voici à quoi ressemble le produit fini:
Même si nous ne voyons l'analyse que de l'utilisation pré-alpha, les données sont intrigantes et leur utilité potentielle semble excellente. Et, bien sûr, il sera facile d’étendre la collecte et l’analyse de données en utilisant le code fondamental que je vous ai partagé aujourd’hui..
À propos, le pourcentage d’invités invités à planifier leurs propres réunions est d’environ 9% (mais c’est un petit ensemble de données)..
Vous vous demandez probablement si nous pouvons tracer ces colonnes. J'espère aborder cela dans un tutoriel de suivi, qui nécessite toujours une interaction avec les déesses éditoriales. Juste pour votre information, tout le monde ne s'éloigne pas de ces conversations. Je lui demanderai également de m'autoriser à écrire sur des fonctionnalités d'administration telles que la désactivation d'utilisateurs, le renvoi de mots de passe, etc..
Si vous n'entendez plus parler de moi, sachez que le Seigneur de la lumière a trouvé un usage pour moi.
Et après?
Comme mentionné, je travaille actuellement avec acharnement pour préparer Meeting Planner à la libération d'alpha. Je me concentre principalement sur les principales améliorations et fonctionnalités qui faciliteront la publication de la version alpha.
Je surveille tout dans Asana maintenant, ce que je vais écrire dans un prochain tutoriel; ça a été incroyablement utile. Il y a aussi quelques nouvelles fonctionnalités intéressantes toujours sur leur chemin. (En tant que professeur de yoga, je pense que Asana est le pire nom de produit de tous les temps. Ils ont essentiellement pris un terme commun dans le yoga, prononcé ahsana ou ah-sana, et ont changé la prononciation en a-sauna - et l'ont mis dans leurs vidéos d'introduction. L'année dernière, il n'a pas été facile de consulter les membres de l'équipe cliente pour savoir ce qu'ils ont mis dans un sauna et de parler à des yogis au sujet d'āsana.
Je commence également à me concentrer davantage sur les efforts de collecte d’investissements à venir avec Meeting Planner. Je commence tout juste à expérimenter WeFunder sur la base de la mise en œuvre des nouvelles règles de la SEC en matière de financement participatif. S'il vous plaît envisager de suivre notre profil. Je vais aussi écrire plus à ce sujet dans un prochain tutoriel.
Encore une fois, en attendant d'autres épisodes, planifiez votre première réunion et testez les modèles avec vos amis avec des boîtes aux lettres Gmail. Aussi, je vous serais reconnaissant de partager votre expérience ci-dessous dans les commentaires, et vos suggestions m'intéresseront toujours. Vous pouvez également me joindre directement sur Twitter @reifman. Vous pouvez également les publier sur le site de support de Meeting Planner..
Surveillez les prochains tutoriels de la série Construire votre démarrage avec PHP.
Liens connexes
- Planificateur de réunion
- La page We Funder du planificateur de réunion
- Programmation avec Yii2: Mise en route
- L'échange de développeurs Yii2
- Prononcer Asana