Bouw je Startup The Dashboard Foundation

Wat je gaat creëren

Deze zelfstudie maakt deel uit van de Bouw je Startup met PHP-serie op Envato Tuts +. In deze serie begeleid ik je door het opstarten van een startup van concept naar realiteit met behulp van mijn Meeting Planner app als een realistisch voorbeeld. Elke stap die ik doe, zal ik de Meeting Planner-code vrijgeven als open-source voorbeelden waar je van kunt leren. Ik zal ook opstartgerelateerde zakelijke problemen aanpakken zodra deze zich voordoen.

Aangezien Meeting Planner bijna nadert, hebben we een manier nodig om supportaanvragen met gebruikers te bespreken en activiteit te controleren. Met andere woorden, we moeten een administratief dashboard bouwen met gebruikersbeheer en rapportage. In gesprekken met een adviseur hebben we besproken dat wanneer ik potentiële investeerders benader, ik uitstekende gegevens nodig heb over het gedrag van gebruikers en de groei van de service.. 

In de aflevering van vandaag leggen we de basis voor ons beheerdersdashboard en nemen we enkele van de eerste live en historische rapportages over. We zullen bijvoorbeeld weten hoeveel mensen zich op elk moment hebben geregistreerd, hoeveel vergaderingen gepland zijn en welk percentage uitgenodigde deelnemers de service voldoende vinden om hun eigen vergadering te organiseren. Het was best leuk om dit soort dingen te maken en de gegevens te bekijken, zelfs als we pre-lanceren.

Als u Meeting Planner nog niet hebt uitgeprobeerd (en wil je zelf in de verzamelde gegevens verschijnen), ga je gang en plan je eerste vergadering. Ik neem wel deel aan de opmerkingen hieronder, dus vertel me wat je denkt! Je kunt me ook bereiken via Twitter @reifman. Ik ben vooral geïnteresseerd als u nieuwe functies of onderwerpen voor toekomstige zelfstudies wilt voorstellen.

Ter herinnering: alle code voor Meeting Planner is geschreven in het Yii2 Framework voor PHP. Als je meer wilt weten over Yii2, bekijk dan onze parallelle serie Programming With Yii2.

De basis van het dashboard opbouwen

The Yii Advanced Template

Yii2 biedt front- en backoffice-websites binnen de geavanceerde applicatie-instellingen. Je kunt er meer over lezen in mijn Envato Tuts + tutorial, How to Program with Yii2: Using the Advanced Application Template. In essentie biedt de front-end-site van de geavanceerde sjabloon mensengerichte functionaliteit en is de back-end-site gemaakt voor het dashboard en de beheersite van een service..

Om het te activeren, moest ik gewoon Apache-sites opzetten in mijn MAMP localhost-omgeving en op mijn productie Ubuntu-server. Hier is bijvoorbeeld de Apache-configuratie op de productieserver om het te laden / Backend / web website:

  Servernaam your-administration-site.com DocumentRoot "/ var / www / mp / backend / web"  # gebruik mod_rewrite voor mooie URL-ondersteuning RewriteEngine op # Als een map of bestand bestaat, gebruik dan de aanvraag direct RewriteCond% REQUEST_FILENAME! -f RewriteCond% REQUEST_FILENAME! -d # Stuur het verzoek door naar index.php RewriteRule. index.php  SSLCertificateFile /etc/letsencrypt/live/meetingplanner.io/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/meetingplanner.io/privkey.pem Inclusief /etc/letsencrypt/options-ssl-apache.conf SSLCertificateChainFile / etc / letsencrypt / live /meetingplanner.io/chain.pem  

Onze back-end-site configureren

Vervolgens heb ik een nieuwe lay-out voor de back-end-site gemaakt op basis van de front-endsite, maar met verschillende menu-opties. Ik besloot dat de startpagina zou omleiden naar een pagina met real-time statistieken. En de menu's zouden koppelingen bevatten naar real-time gegevens, gegevens van gisteren om middernacht en historische gegevens. Ik zal hier wat meer uitleg over geven terwijl we verder gaan.

Dit zijn de \ backend \ views \ layouts \ main.php met het menu:

 beginBody ()?> 
Yii :: t ('backend', 'Meeting Planner'), 'brandUrl' => 'https://meetingplanner.io', 'options' => ['class' => 'navbar-inverse navbar-fixed-top ',],]); $ menuItems [] = ['label' => 'Realtime', 'items' => [['label' => Yii :: t ('frontend', 'Gebruik'), 'url' => ['/ data / stroom ']],]]; $ menuItems [] = ['label' => 'Yesterday', 'items' => [['label' => Yii :: t ('frontend', 'User Data'), 'url' => ['/ gebruikersgegevens']], ] ]; $ menuItems [] = ['label' => 'Historisch', 'items' => [['label' => Yii :: t ('frontend', 'Statistieken'), 'url' => ['/ historisch -data ']],],]; if (Yii :: $ app-> user-> isGuest) $ menuItems [] = ['label' => 'Inloggen', 'url' => ['/ site / login']]; else $ menuItems [] = ['label' => 'Account', 'items' => [['label' => 'Uitloggen ('. Yii :: $ app-> gebruiker-> identiteit-> gebruikersnaam. ')', 'url' => ['/ site / logout'], 'linkOptions' => ['data-methode' => 'post'],],],]; echo Nav :: widget (['options' => ['class' => 'navbar-nav navbar-right'], 'items' => $ menuItems,]); NavBar :: uiteinde (); ?>
isset ($ this-> params ['breadcrumbs'])? $ this-> params ['breadcrumbs']: [],])?>

De eerste rapportage bouwen

Voor mijn eerste statistische rapportage heb ik me gericht op eenvoudige realtime gegevens en gedetailleerde historische gegevens. Gegevens in real-time vertellen u bijvoorbeeld het aantal gebruikers en vergaderingen dat tot nu toe op het systeem is gebouwd en hun status. 

De historische gegevens vertellen u het aantal gebruikers en vergaderingen dat in de loop van de tijd is voltooid, evenals andere interessante gegevens, met name de groeicurves die ik en potentiële beleggers mogelijk interesseren.

Realtime gegevens

De real-time data-pagina moet een live snapshot laten zien van wat er op de site gebeurt. Aanvankelijk wilde ik weten:

  • Hoeveel vergaderingen zijn er in het systeem?
  • Hoeveel gebruikers zijn er??
  • Wat is hun status??

Om dit te bereiken, heb ik een back-end DataController.php en Data.php-model gemaakt. Ik heb ook een stap voorwaarts gedaan en in plaats van ruwe HTML te maken naar mijn mening om dit weer te geven, maakte ik ActiveDataProviders van mijn query's en voerde ze deze naar de grid-widgets van Yii; het resultaat ziet er beter uit en is eenvoudiger te bouwen en te onderhouden.

Deze code bevraagt ​​het aantal vergaderingen in het systeem gegroepeerd op hun status:

openbare statische functie getRealTimeData () $ data = new \ stdClass (); $ data-> meetings = nieuwe ActiveDataProvider (['query' => Meeting :: find () -> selecteer (['status, COUNT (*) AS dataCount']) // -> where ('approved = 1') -> groupBy (['status']), 'pagination' => ['pageSize' => 20,],]); 

Deze code in /backend/views/data/current.php geeft het weer:

title = Yii :: t ('backend', 'Meeting Planner'); ?> 

Realtime gegevens

vergaderingen

$ data-> meetings, 'columns' => [['label' => 'Status', 'attribute' => 'status', 'format' => 'raw', 'value' => functie ($ model) return '
'.Meeting :: lookupStatus ($ model-> status).'
'; ,], 'dataCount',],]); ?>

Het ziet er zo uit (de gegevens zijn klein omdat de site nog niet is gelanceerd!):

Vervolgens heb ik nog enkele realtime query's gemaakt en ziet de rest van de pagina er als volgt uit:

Betreffende de Mensen actief en Via uitnodiging Bovenstaande kolommen, als u een persoon uitnodigt voor een vergadering, tellen we ze als een gebruiker via Uitnodigen totdat ze een wachtwoord maken of hun sociale account koppelen. Tot die tijd is hun enige toegang tot Meeting Planner via uw link voor e-mailuitnodigingen en de verificatie-ID.

Uiteraard zal ik de real-time rapportage-opties uitbreiden naarmate het project evolueert.

Historische gegevens rapporteren

Het genereren van historische rapportage voor activiteiten in het hele systeem bleek een beetje meer betrokken te zijn. Ik besloot om een ​​aantal lagen voor afhankelijke gegevensverzameling te maken.

De onderste laag is een UserData-tabel die de status van de historische accountactiviteit van een persoon tot een specifieke dag om middernacht samenvat. In wezen doen we dit elke avond.

De bovenste laag is de HistoricalData-tabel waarmee de berekeningen worden gemaakt met behulp van de tabel UserData van de avond ervoor.

Ik moest ook code schrijven die de twee tabellen helemaal opnieuw opbouwde, omdat onze service een aantal maanden een beetje actief was geweest.

Ik zal je laten zien hoe ik dit heb gedaan. Het resultaat bleek vrij goed.

Tabelmigraties maken

Hier is de tabelmigratie voor UserData - deze bevat de gegevens die ik 's nachts wilde berekenen om de historische berekeningen te ondersteunen:

public function up () $ tableOptions = null; if ($ this-> db-> driverName === 'mysql') $ tableOptions = 'KARAKTER 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 '); 

Bijvoorbeeld, count_meeting_participant_last30 is het aantal vergaderingen waarin deze persoon is uitgenodigd in de afgelopen 30 dagen. 

Hier is de tabelmigratie voor Historische gegevens-bijna alle kolommen in deze tabel moeten worden berekend op basis van verschillende gegevenslagen:

public function up () $ tableOptions = null; if ($ this-> db-> driverName === 'mysql') $ tableOptions = 'KARAKTER 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', //% van de gebruikers uitgenodigd door anderen die een vergadering bezitten 'percent_invited_own_meeting' => Schema :: TYPE_FLOAT. 'NOT NULL', ' percent_participant '=> Schema :: TYPE_FLOAT.' NOT NULL ',' percent_particip__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 :: TY PE_INTEGER. 'NOT NULL',], $ tableOptions);

In overleg met mijn adviseur realiseerden we ons dat potentiële investeerders willen weten hoe mensen reageren op de site. Ik heb een meting gemaakt voor een gegeven met de naam percent_invited_own_meeting, afkorting van het percentage gebruikers dat was uitgenodigd voor hun eerste vergadering, die de service voldoende vond om het te gebruiken om hun eigen vergadering in de toekomst te plannen. Ik zal meer over de berekeningen een beetje verder hieronder bespreken.

De migraties bevinden zich allemaal in / console / migraties. Zo ziet het eruit wanneer u de databasemigraties uitvoert.

$ ./yii migrate / up Yii Migration Tool (gebaseerd op Yii v2.0.8) Totaal 2 nieuwe migraties die moeten worden toegepast: m160609_045838_create_user_data_table m160609_051532_create_historical_data_table Pas de bovenstaande migraties toe? (yes | no) [no]: yes *** apply 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 (tijd: 0.004s) *** toegepast m160609_045838_create_user_data_table (tijd: 0.013s) *** apply m160609_051532_create_historical_data_table> create table % historical_data ... done (time: 0.003s) ** * applied m160609_051532_create_historical_data_table (time: 0.005s) Er zijn 2 migraties toegepast. Migratie succesvol voltooid.

Verzamelen van de rapportagegegevens

Elke nacht na middernacht berekent een achtergrondtaak de statistieken van de vorige nacht. Dit is de achtergrondmethode:

 public function actionOvernight () $ since = mktime (0, 0, 0); $ after = mktime (0, 0, 0, 2, 15, 2016); UserData :: berekenen (false, $ na); HistoricalData :: berekenen (false, $ na);  

Ik heb een cron-taak ingesteld om te worden uitgevoerd actionOvernight om 1:15 uur dagelijks. Opmerking: als je geconcentreerd programmeert en dag en nacht programmeert bij een startup, gaat een cron-taak over alle actionOvernight die je krijgt.

Om de geschiedenis van het verleden op te bouwen, heb ik een eenmalige creatie gemaakt herberekenen () functie. Dit spoelt de tabellen en bouwt elke tafel op alsof het elke dag gebeurt. 

public static function recalc () UserData :: reset (); HistoricalData :: reset (); $ after = mktime (0, 0, 0, 2, 15, 2016); $ since = mktime (0, 0, 0, 4, 1, 2016); terwijl ($ sinds < time())  UserData::calculate($since,$after); HistoricalData::calculate($since,$after); // increment a day $since+=24*60*60;  

Merk op na Tijd is een tijdelijke oplossing om sommige vroege gebruikers die zich aanmeldden uit te sluiten voordat ze een vergadering konden plannen. Ik wilde dat de historische gegevens een nauwkeuriger weergave van recente activiteit weergeven (momenteel zijn er een paar honderd oudere accounts zonder enige activiteit). Ik zal dit waarschijnlijk op een later tijdstip verwijderen.

De gebruikergegevenstabel berekenen

Hier is de code die de Gebruikersgegevens tafel 's nachts:

openbare statische functie bereken ($ since = false, $ after = 0) if ($ since === false) $ since = mktime (0, 0, 0);  $ monthago = $ since- (60 * 60 * 24 * 30); $ all = Gebruiker: find () -> where ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->allemaal(); foreach ($ all als $ u) // nieuwe record aanmaken voor gebruiker of update oude $ ud = UserData :: find () -> where (['user_id' => $ u-> id]) -> one ( ); if (is_null ($ ud)) $ ud = nieuwe UserData (); $ ud-> user_id = $ u-> id; $ UD> save ();  $ user_id = $ u-> id; // tel vergaderingen die ze hebben georganiseerd $ ud-> count_meetings = Meeting :: find () -> where (['owner_id' => $ user_id]) -> andWhere ('created_at<'.$since)->optellen (); $ ud-> count_meetings_last30 = Meeting :: find () -> where (['owner_id' => $ user_id]) -> andWhere ('created_at<'.$since)->andWhere ( 'created_at> =' $ monthago.) -> count (); // tel vergaderingen waarvoor ze waren uitgenodigd $ ud-> count_meeting_participant = Deelnemer :: find () -> where (['participant_id' => $ user_id]) -> andWhere ('created_at<'.$since)->optellen (); $ ud-> count_meeting_participant_last30 = Deelnemer: find () -> where (['participant_id' => $ user_id]) -> andWhere ('created_at<'.$since)->andWhere ( 'created_at> =' $ monthago.) -> count (); // tel plaatsen en vrienden $ ud-> count_places = UserPlace :: find () -> where (['user_id' => $ user_id]) -> andWhere ('created_at<'.$since)->optellen (); $ ud-> count_friends = Friend :: find () -> where (['user_id' => $ user_id]) -> andWhere ('created_at<'.$since)->optellen (); // bereken uitnodiging dan Eigen - deelnemer eerst, dan organisator $ first_invite = Deelnemer: 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 (); 

Het zijn meestal alleen totalen voor gebruikers van vergaderingen, plaatsen, vrienden en in sommige gevallen binnen tijdsperioden van de afgelopen 30 dagen. 

Hier is de code die detecteert of deze gebruiker heeft gekozen om een ​​vergadering te plannen met behulp van de service nadat hij is uitgenodigd:

$ 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;  

HistorischData berekenen

Dit is de code die wordt gebruikt Gebruikersgegevens bevolken Historische gegevens:

openbare statische functie bereken ($ since = false, $ after = 0) if ($ since === false) $ since = mktime (0, 0, 0);  // maak een nieuw record voor de datum of update bestaande $ hd = HistoricalData :: find () -> where (['date' => $ since]) -> one (); if (is_null ($ hd)) $ hd = new HistoricalData (); $ hd-> date = $ since; $ action = 'opslaan';  else $ action = 'update';  // bereken $ count_meetings_completed $ hd-> count_meetings_completed = Meeting :: find () -> where (['status' => Meeting :: STATUS_COMPLETED]) -> andWhere ('created_at<'.$since)->optellen () ;; // bereken $ count_meetings_planning $ hd-> count_meetings_planning = Meeting :: find () -> where ('status<'.Meeting::STATUS_COMPLETED)->andWhere ( 'created_at<'.$since)->optellen () ;; // bereken $ count_places $ hd-> count_places = Plaats :: find () -> where ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->optellen (); // bereken $ source_google $ hd-> source_google = Auth :: find () -> where (['source' => 'google']) -> count (); // bereken $ source_facebook $ hd-> source_facebook = Auth :: find () -> where (['source' => 'facebook']) -> count (); // bereken $ source_linkedin $ hd-> source_linkedin = Auth :: find () -> where (['source' => 'linkedin']) -> count (); // totale gebruikers $ total_users = UserData :: find () -> count (); // bereken $ count_users $ hd-> count_users = $ total_users; // Gebruikers :: find () -> waarin (status <> 'Gebruiker :: STATUS_DELETED.) -> andWhere ( 'created_at>' $ na.) -> count (); $ total_friends = Friend :: find () -> where ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->optellen (); $ total_places = Plaats: find () -> where ('created_at>'. $ after) -> andWhere ('created_at<'.$since)->optellen (); 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 (); 

Het somt totalen samen en berekent percentages en gemiddelden.

Dit is hoe het eindproduct eruit ziet:

Hoewel we de analyse van alleen pre-alpha-gebruik zien, zijn de gegevens intrigerend en het potentiële nut hiervan lijkt uitstekend. En het is natuurlijk eenvoudig om de gegevensverzameling en -analyse uit te breiden met de basiscode die ik vandaag met u heb gedeeld.

Trouwens, het percentage uitgenodigde gebruikers dat hun eigen vergaderingen plant is ongeveer 9% (maar het is een kleine dataset).

Je vraagt ​​je waarschijnlijk af of we deze kolommen in kaart kunnen brengen. Ik hoop dat aan te pakken in een vervolg-zelfstudie, waarvoor altijd interactie met de redactionele godinnen nodig is. JULLIE, niet iedereen loopt weg van die gesprekken. Ik zal haar ook vragen mij toe te staan ​​te schrijven over beheerfuncties zoals het uitschakelen van gebruikers, het opnieuw verzenden van wachtwoorden, enz.

Als u niet meer van mij hoort, weet dan dat de Heer van het Licht een gebruik voor mij heeft gevonden.

Wat is het volgende?

Zoals gezegd, ben ik momenteel koortsachtig aan het werk om Meeting Planner voor te bereiden op het vrijgeven van alfa. Ik ben vooral gericht op de belangrijkste verbeteringen en functies waardoor de alpha-release soepel verloopt.

Ik volg nu alles in Asana, waarover ik zal schrijven in een volgende tutorial; het was ongelooflijk behulpzaam. Er zijn ook een aantal interessante nieuwe functies die nog steeds onderweg zijn. (Als yogaleraar denk ik dat Asana de slechtste productnaam ooit is, ze hebben in principe een veel gebruikte term in yoga uitgesproken als āsana of ah-sana en de uitspraak veranderd in a-sauna - en zetten dat in hun inleidende video's. was vorig jaar niet makkelijk om met de teamleden van het team te praten over wat ze in een sauna deden en met yogi's over āsana te praten. Maar ik dwaal af.)

Ik begin me ook meer te gaan richten op de komende investering in het verzamelen van informatie met Meeting Planner. Ik begin net te experimenteren met WeFunder op basis van de implementatie van de nieuwe crowdfundingregels van de SEC. Overweeg om ons profiel te volgen. Ik zal hier ook meer over schrijven in een toekomstige tutorial.

Nogmaals, terwijl u wacht op meer afleveringen, plant u uw eerste vergadering en test u de sjablonen met uw vrienden met Gmail-postvakken. Ik zou het ook op prijs stellen als u uw ervaringen hieronder in de opmerkingen deelt en ik ben altijd geïnteresseerd in uw suggesties. Je kunt me ook rechtstreeks op Twitter @reifman bereiken. U kunt ze ook op de ondersteuningssite van Meeting Planner plaatsen.

Kijk uit voor komende tutorials in de Building Your Startup With PHP-serie.

Gerelateerde Links

  • Meeting Planner
  • De Funder-pagina van Meeting Planner
  • Programmeren met Yii2: Aan de slag
  • De Yii2 Developer Exchange
  • Asana uitspreken