


This tutorial is part of the Building Your Startup With PHP series on Envato Tuts+. In this series, I'm guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I'll release the Meeting Planner code as open-source examples you can learn from. I'll also address startup-related business issues as they arise.
Recently, I asked if our startup series has inspired any of your own entrepreneurial ideas and got you writing code. If so, please share a bit with us in the comments.
Let's Meet, Visit My Schedule With Me Page
For a long time since I began this project, I've wanted Meeting Planner and Simple Planner to have a publicly accessible page you can share with people to schedule a meeting with you. In other words, "Sure, let's meet, just visit my schedule with me page at Meeting Planner, I'm Bernie Sanders (no space)."
In today's tutorial, I'll show you I've done it using Yii's routing and some of the related issues that came up.
If you haven't tried scheduling a meeting yet, you can see how it's done in this video:
The Schedule With Me page would be something like the PayPal Pay Me page:



I do participate in the comment threads below, so tell me what you think! You can also reach me on Twitter @lookahead_io. I'm especially interested if you want new features or to suggest topics for future tutorials.
As a reminder, all of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you'd like to learn more about Yii2, check out our parallel series Programming With Yii2.
Let's get started.
Planning the Schedule With Me Page



Note: Bernie's not actually a Meeting Planner user, as far as I know.
Every Meeting Planner user has a unique username, e.g. berniesanders, and I decided to use this for the schedule with me URL. There were a few challenges to this feature:
- Designing the page
- Working with Yii Routing to map a root path to each person
- Managing the signup, login, and return to schedule
Designing the Page
Inspired by the PayPal Pay Me page (above) and others like it, I wanted to keep things simple initially. I used a responsive grid with offsets and centering:
1 |
<div class="col-lg-8 col-lg-offset-2 |
2 |
col-xs-10 col-xs-offset-1
|
3 |
col-md-8 col-md-offset-2"> |
4 |
<div class="centered schedule-me"> |
5 |
Here's the /frontend/views/meeting/scheduleme.php view for the page:
1 |
<div class="scheduleme-top"> |
2 |
<div class="row"> |
3 |
<div class="col-lg-8 col-lg-offset-2 col-xs-10 col-xs-offset-1 col-md-8 col-md-offset-2"> |
4 |
<div class="centered schedule-me"> |
5 |
<?php
|
6 |
if ($userprofile->avatar<>'') { |
7 |
echo '<img src="'.Yii::getAlias('@web').'/uploads/avatar/sqr_'.$userprofile->avatar.'" class="profile-image"/>'; |
8 |
} else { |
9 |
echo \cebe\gravatar\Gravatar::widget([ |
10 |
'email' => $user->email, |
11 |
'options' => [ |
12 |
'class'=>'profile-image', |
13 |
'alt' => $user->username, |
14 |
],
|
15 |
'size' => 128, |
16 |
]);
|
17 |
}
|
18 |
?>
|
19 |
<h1><?= $displayName ?></h1> |
20 |
<p class="lead"> |
21 |
<?php if (Yii::$app->user->isGuest) { ?> |
22 |
<?= Html::a(Yii::t('frontend','Schedule a meeting with me'),['site/signup'])?> |
23 |
<?php } else { ?> |
24 |
<?= Html::a(Yii::t('frontend','Schedule a meeting with me'),['meeting/create','with'=>$user->username])?> |
25 |
<?php } ?> |
26 |
<p>
|
27 |
</div>
|
28 |
</div>
|
29 |
</div>
|
30 |
</div>
|
The code displays the profile image the user uploaded in user settings or uses a general Gravatar.
Of course, I used /frontend/web/css/site.css to customize the margins, border, and background:
1 |
.scheduleme-top { |
2 |
margin-top:8%; |
3 |
} |
4 |
|
5 |
.schedule-me { |
6 |
background-color:#f3f3f3; |
7 |
min-width:500px; |
8 |
padding:100px 25px 65px 25px; |
9 |
border:1px solid #e0e0e0; |
10 |
} |
11 |
|
12 |
// responsive adjustments |
13 |
@media only screen |
14 |
and (min-device-width: 320px) |
15 |
and (max-device-width: 667px) |
16 |
and (-webkit-min-device-pixel-ratio: 2) { |
17 |
|
18 |
... |
19 |
|
20 |
.schedule-me { |
21 |
min-width:75%; |
22 |
padding:60px 10px 40px 10px; |
23 |
} |
Managing the Yii Routing Changes
The routing for how Yii handles incoming browser requests is handled in /frontend/config/main.php under components. If you're not careful with configuring this, you can destroy your whole application as incoming requests fail out to error pages.
Here's the earlier routing before schedule with me:
1 |
<?php
|
2 |
$config = parse_ini_file('/var/secure/mp.ini', true); |
3 |
|
4 |
$params = array_merge( |
5 |
require(__DIR__ . '/../../common/config/params.php'), |
6 |
require(__DIR__ . '/../../common/config/params-local.php'), |
7 |
require(__DIR__ . '/params.php'), |
8 |
require(__DIR__ . '/params-local.php') |
9 |
);
|
10 |
return [ |
11 |
'components' => [ |
12 |
...
|
13 |
'urlManager' => [ |
14 |
'class' => 'yii\web\UrlManager', |
15 |
'enablePrettyUrl' => true, |
16 |
'showScriptName' => false, |
17 |
//'enableStrictParsing' => false,
|
18 |
'rules' => [ |
19 |
'place' => 'place', |
20 |
'place/yours' => 'place/yours', |
21 |
'place/create' => 'place/create', |
22 |
'place/create_geo' => 'place/create_geo', |
23 |
'place/create_place_google' => 'place/create_place_google', |
24 |
'place/view/<id:\d+>' => 'place/view', |
25 |
'place/update/<id:\d+>' => 'place/update', |
26 |
'place/<slug>' => 'place/slug', |
27 |
'<controller:\w+>/<id:\d+>' => '<controller>/view', |
28 |
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>', |
29 |
'daemon/<action>' => 'daemon/<action>', // incl eight char action |
30 |
'site/<action>' => 'site/<action>', // incl eight char action |
31 |
'features' => 'site/features', |
32 |
'about' => 'site/about', |
33 |
'wp-login|wp-admin' => 'site/neverland', |
34 |
'<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity', |
35 |
// note - currently actions with 8 letters and no params will fail
|
36 |
'<controller:\w+>/<action:\w+>' => '<controller>/<action>', |
37 |
],
|
I've written a bit before about routes in How to Program With Yii2: Sluggable Behavior, part of our Yii programming series, and you can read more background in the Yii documentation.
In Building Your Startup: Meetings With Multiple Participants, I wrote two episodes about dynamic paths by username for unique meeting URLs as shown below:
'<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity',
This broke lots of two-item routes such as meeting/[meeting_id] until I moved up more dynamic mapping above it to take precedence:
1 |
'<controller:\w+>/<id:\d+>' => '<controller>/view', |
2 |
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>', |
3 |
And any second item paths with characters needed to be statically defined because our identity strings for meetings are eight characters, e.g. features
.
Routes such as features
are fixed, which goes to controller sites
and action features
as shown above. Remaining features are mapped dynamically as in: '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
.
Trying to create a one-item dynamic variable route such as /[username], e.g. https://meetingplanner.io/berniesanders, broke lots of single-item routes such as https://meetingplanner.io/about and the reminders page https://meetingplanner.io/reminder.
So I had to begin statically defining many of them.
Here's the final routing with new static routes for one-word paths:
1 |
'urlManager' => [ |
2 |
'class' => 'yii\web\UrlManager', |
3 |
'enablePrettyUrl' => true, |
4 |
'showScriptName' => false, |
5 |
//'enableStrictParsing' => false,
|
6 |
'rules' => [ |
7 |
'place' => 'place', |
8 |
'place/yours' => 'place/yours', |
9 |
'place/create' => 'place/create', |
10 |
'place/create_geo' => 'place/create_geo', |
11 |
'place/create_place_google' => 'place/create_place_google', |
12 |
'place/view/<id:\d+>' => 'place/view', |
13 |
'place/update/<id:\d+>' => 'place/update', |
14 |
'place/<slug>' => 'place/slug', |
15 |
'<controller:\w+>/<id:\d+>' => '<controller>/view', |
16 |
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>', |
17 |
'daemon/<action>' => 'daemon/<action>', // incl eight char action |
18 |
'site/<action>' => 'site/<action>', // incl eight char action |
19 |
'features' => 'site/features', |
20 |
'about' => 'site/about', |
21 |
'wp-login|wp-admin' => 'site/neverland', |
22 |
'<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity', |
23 |
'meeting' => 'meeting', |
24 |
'friend' => 'friend', |
25 |
'reminder' => 'reminder', |
26 |
'user-contact' => 'user-contact', |
27 |
'user-profile' => 'user-profile', |
28 |
'user-setting' => 'user-setting', |
29 |
'<username>' => 'meeting/scheduleme', |
30 |
// note - currently actions with 8 letters and no params will fail
|
31 |
'<controller:\w+>/<action:\w+>' => '<controller>/<action>', |
32 |
],
|
33 |
],
|
And you can check out "Bernie's page" and schedule a meeting with him here:
https://simpleplanner.io/berniesanders
Note: Meeting Planner and Simple Planner work interchangeably, and I run both sites to offer users multiple brands. Simple Planner is for social get-togethers, and Meeting Planner is for more business-related stuff.
Managing Signup and Login From the Schedule With Me Page
Most people who initially visit a schedule with me page won't have an account with us. So they'll be redirected when they click Schedule With Me to the signup or login page.
After they log in, we want to return them to the meeting creation page pre-loaded with the owner of the schedule with me page added as a participant. We use setReturnUrl
to do this:
1 |
Yii::$app->user->setReturnUrl(['meeting/create/','with'=>$u->username]); |
It updates the session (usually through a cookie) so that after a person signs up or logs in, they are returned to the target page.
Here's the full /frontend/controllers/MeetingController.php actionScheduleme
method:
1 |
public function actionScheduleme() { |
2 |
$username = Yii::$app->request->getPathInfo(); |
3 |
$u = User::find() |
4 |
->where(['username'=>$username]) |
5 |
->one(); |
6 |
if (is_null($u)) { |
7 |
return $this->goHome(); |
8 |
} elseif (!Yii::$app->user->isGuest) { |
9 |
if (Yii::$app->user->getId()==$u->id) { |
10 |
Yii::$app->getSession()->setFlash('info', Yii::t('frontend','Welcome to your public scheduling page.')); |
11 |
}
|
12 |
}
|
13 |
$userprofile = \frontend\models\UserProfile::find() |
14 |
->where(['user_id'=>$u->id]) |
15 |
->one(); |
16 |
Yii::$app->user->setReturnUrl(['meeting/create/','with'=>$u->username]); |
17 |
return $this->render('scheduleme', [ |
18 |
'user'=>$u, |
19 |
'displayName'=> MiscHelpers::getDisplayName($u->id,true), |
20 |
'userprofile' => $userprofile, |
21 |
]);
|
22 |
}
|
Adding the Schedule Page Owner as a Participant
Here's the /frontend/controllers/MeetingController.php actionCreate
method:
1 |
public function actionCreate($with = '') |
2 |
{
|
3 |
...
|
4 |
if ($with<>'') { |
5 |
$u = User::find() |
6 |
->where(['username'=>$with]) |
7 |
->one(); |
8 |
if (!is_null($u)) { |
9 |
$with_id =$u->id; |
10 |
} else { |
11 |
Yii::$app->getSession()->setFlash('error', Yii::t('frontend','Sorry, we could not locate anyone by that name. Visit support if you need assistance.')); |
12 |
$with_id =0; |
13 |
}
|
14 |
} else { |
15 |
$with_id =0; |
16 |
}
|
17 |
// prevent creation of numerous empty meetings
|
18 |
$meeting_id = Meeting::findEmptyMeeting(Yii::$app->user->getId(),$with_id); |
19 |
if ($meeting_id===false) { |
20 |
// otherwise, create a new meeting
|
21 |
$model = new Meeting(); |
22 |
$model->owner_id= Yii::$app->user->getId(); |
23 |
$model->sequence_id = 0; |
24 |
$model->meeting_type = 0; |
25 |
$model->subject = Meeting::DEFAULT_SUBJECT; |
26 |
$model->save(); |
27 |
$model->initializeMeetingSetting($model->id,$model->owner_id); |
28 |
$meeting_id = $model->id; |
29 |
}
|
30 |
if ($with_id!=0) { |
31 |
Participant::add($meeting_id,$with_id,Yii::$app->user->getId()); |
32 |
}
|
33 |
$this->redirect(['view', 'id' => $meeting_id]); |
34 |
}
|
It processes the user id of the schedule with me page owner as $with_id
to add them as a participant. And it also checks first to make sure there isn't already a meeting in place between these two people—to prevent duplicates:
1 |
public static function findEmptyMeeting($user_id,$with_id = 0) { |
2 |
// if meeting with someone see if it exists already
|
3 |
if ($with_id!=0) { |
4 |
// check for meeting with one participant with with_id
|
5 |
$meetings = Meeting::find()->where(['owner_id'=>$user_id,'status'=>Meeting::STATUS_PLANNING])->limit(7)->orderBy(['id' => SORT_DESC])->all(); |
6 |
foreach ($meetings as $m) { |
7 |
if (!is_null($m) && (count($m->participants)==1 && $m->participants[0]->participant_id==$with_id)) { |
8 |
return $m->id; |
9 |
}
|
10 |
}
|
11 |
}
|
12 |
// looks for empty meeting in last seven
|
13 |
$meetings = Meeting::find()->where(['owner_id'=>$user_id,'status'=>Meeting::STATUS_PLANNING])->limit(7)->orderBy(['id' => SORT_DESC])->all(); |
14 |
foreach ($meetings as $m) { |
15 |
if (!is_null($m) and ($m->subject==Meeting::DEFAULT_SUBJECT || $m->subject=='') and (count($m->participants)==0 && count($m->meetingPlaces)==0 && count($m->meetingTimes)==0)) { |
16 |
return $m->id; |
17 |
}
|
18 |
}
|
19 |
return false; |
20 |
}
|
I originally added the feature to prevent users from creating new meetings when there was already another empty new meeting they'd created previously.
Looking Ahead
There will be some things about this page I'll clean up in the future as I speak to real users and gather feedback. Perhaps I'll automatically share the user's most frequent days and times for meetings. And I'll create a user setting to turn off your scheduling page in case you don't want it.
In Closing
All the work I've done recently with Bootstrap to create a better responsive interface for Meeting Planner made it easier for me to quickly code the schedule with me page.
Making sure the new Yii routes worked and didn't break anything on the site was the hardest part of this. I also went and checked all of my Ajax calls to make sure none of them were affected.
I hope today's tutorial was useful to you in learning to customize site URLs for your user base and the basics of MVC routing.
Have your own thoughts? Ideas? Feedback? You can always reach me on Twitter @lookahead_io directly. Watch for upcoming tutorials here in the Building Your Startup With PHP series. Some cool features are on their way.
Again, if you haven't tried out Meeting Planner or Simple Planner yet, go ahead and schedule your first meeting.
Related Links