
Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.
Расширение опций планирования
Когда начался этап альфа-тестирования Планировщика собраний , самым явным недостатком была невозможность изменить собрание после того, как оно было запланировано. Но также отсутствовали другие функции, такие как повторная отправка приглашения, потерянного в электронной почте, полное изменение расписания собрания или возможность просмотра и изменения настроек управления, выбранных организатором.
Интересно, что я также начал понимать, что способность легко настраивать собрания после того, как они были запланированы, может создать или разрушить бренд Meeting Planner. Например, есть много социальной инженерии в настройке встречи после планирования. Часто вам нужно спросить участника (ов), можно ли корректировать время или место. Возможно, вы просто хотите встретиться на 15 минут раньше или на следующий день в то же время и в том же месте. Но вы не можете всегда вносить эти изменения без согласия.
Обеспечение легкого для понимания и простого использования сайта со всеми этими возможностями является моей главной директивой. Как я могу добавить растущее число функций, не загромождая пользовательский интерфейс или не слишком скрывая их? Как это будет работать на мобильных и настольных интерфейсах?
В сегодняшнем уроке я расскажу о расширении панели навигации с помощью Bootstrap и об основах построения некоторых расширенных функций планирования в Meeting Planner. На следующей неделе я расскажу о создании более сложной функции, позволяющей участникам запрашивать изменения, а другим — принимать или отклонять их.
Я надеюсь, что вы опробуете все новые функции планирования на живом сайте и поделитесь своими мыслями и отзывами в комментариях ниже. Я участвую в обсуждениях, но вы также можете связаться со мной @reifman в Twitter. Я всегда открыт для новых идей для планировщика собраний, а также предложений для будущих серий.
Напоминаем, что весь код для Meeting Planner предоставляется с открытым исходным кодом и написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с моей параллельной серией Программирование с Yii2 .
Расширение панели навигации
Сначала давайте посмотрим, как расширить существующую панель навигации. Вот список функций, которые я планировал добавить:
- Возможность изменения мест, дат и времени встречи после ее завершения.
- Требование небольших изменений в собрании, например, можем ли мы встретиться на час раньше? Или в другом предложенном месте?
- Пересмотр встречи полностью с теми же участниками и местами.
- Повторение собрания с использованием участников, мест, дней недели и времени суток из предыдущего собрания, чтобы упростить планирование нового собрания.
- Показаны участникам хронологическую историю всех мероприятий по планированию встречи.
- Просмотр и обновление настроек встречи.
Как вы можете видеть, не только было много функциональности для сборки, но я также не имел четкого представления, где разместить его в пользовательском интерфейсе, не создавая беспорядка.
Командная строка также должна меняться в зависимости от статуса встречи. Встречи, которые находились на этапе планирования, имели другие варианты, чем ожидающие, подтвержденные или прошедшие завершенные собрания.
Ранние идеи UX вернулись в Bootstrap
Моя первоначальная идея состояла в том, чтобы предоставить небольшую расширенную настройку ссылка, которая будет отображать скрытую панель команд. Сначала я экспериментировал с этим, но это не было эстетично.
Затем я просмотрел документацию Bootstrap и обнаружил выпадающий список:

Мне понравилось, как это работает. Поэтому я решил поместить большинство расширенных команд в выпадающую кнопку слева.
Вот пример того, как это выглядит на этапе планирования собрания:

Обратите внимание, что bootstrap предлагает класс для выпадающих меню. Панель команд, расположенная внизу страницы, использует раскрывающийся список следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/* the partial position knows to set the dropclass variable up or down */
echo $this->render(‘_command_bar_past’, [
‘model’=>$model,
‘isPast’=>true,
‘dropclass’=>’dropup’,
‘isOwner’ => $isOwner,
]);
/* the resulting view applies the dropclass */
<div class=»command-bar»>
<div class=»row»>
<div class=»col-xs-4″>
<div class=»<?= $dropclass ?>» >
<button class=»btn btn-default dropdown-toggle» type=»button» id=»dropdownMenu1″ data-toggle=»dropdown» aria-haspopup=»true» aria-expanded=»true»>
<?= Yii::t(‘frontend’,’Options’);?>
<span class=»caret»>
</button>
<ul class=»dropdown-menu» aria-labelledby=»dropdownMenu1″>
<?php
if (!$isPast && ($model->viewer == Meeting::VIEWER_ORGANIZER || $meetingSettings->participant_reopen)) {
?>
<li><?= Html::a(Yii::t(‘frontend’, ‘Make changes’), [‘reopen’,’id’=>$model->id],
[‘title’=>Yii::t(‘frontend’,’tbd’)]);
|
Я также создал файлы частичного просмотра, которые будут отображаться в зависимости от статуса собрания. Например, в /frontend/views/meeting/view_confirmed.php вы можете увидеть, что _command_bar_past.php _command_bar_confirmed.php _command_bar_past.php или _command_bar_confirmed.php :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
<?php
if ( $model->status >= $model::STATUS_COMPLETED) {
…
echo $this->render(‘_command_bar_past’, [
‘model’=>$model,
‘isPast’=>true,
‘dropclass’=>’dropdown’,
‘isOwner’ => $isOwner,
]);
} else {
echo $this->render(‘_command_bar_confirmed’, [
‘model’=>$model,
‘meetingSettings’ => $meetingSettings,
‘showRunningLate’=>$showRunningLate,
‘isPast’=>$isPast,
‘dropclass’=>’dropdown’,
‘isOwner’ => $isOwner,
]);
}
?>
|
Определение доступа к командам для разных зрителей
Я не хотел, чтобы все могли видеть все команды. Организаторы увидят больше команд, чем участников, но настройки собрания часто также предоставляют им доступ.
Вот пример определения, отображать ли возможность повторно открыть собрание и внести в него изменения, как если бы он все еще находился на этапе планирования:
|
1
2
3
4
5
6
7
|
<ul class=»dropdown-menu» aria-labelledby=»dropdownMenu1″>
<?php
if (!$isPast && ($model->viewer == Meeting::VIEWER_ORGANIZER
||
?>
<li><?= Html::a(Yii::t(‘frontend’, ‘Make changes’), [‘reopen’,’id’=>$model->id],
[‘title’=>Yii::t(‘frontend’,’tbd’)]);
|
Этот параметр включается в раскрывающееся меню Bootstrap, если собрание не прошло дату начала, а просмотрщик является организатором или организатор разрешил участникам вносить изменения.
Теперь давайте углубимся в построение некоторых функций для этих различных команд.
Построение команд расширенного планирования
Невозможно охватить всю новую работу, которая вошла в эти функции планирования. Для краткости я рассмотрю только основы и любые уникальные аспекты различных команд.
Каждая функция занимает больше времени
После реализации некоторых более глубоких функций безопасности в Meeting Planner я стараюсь придерживаться более строгих стандартов кодирования по мере добавления новых функций. Я трачу больше времени на доступ к контроллеру, проверку моделей и ограничение скорости.
Кроме того, бета-версия будет поддерживать несколько участников собрания, поэтому я должен с самого начала разрабатывать код с учетом этого.
В целом, все, что я делаю, занимает немного больше времени, чем в режиме быстрого прототипирования.
Когда я создавал все эти различные функции для собраний, я часто чувствовал возросшую нагрузку при добавлении в базу кода. Вы заметите эти проверки в коде ниже.
Также есть внимание на исторический журнал собраний, который мы собираемся сделать видимым. Поэтому каждое действие часто требует записи в журнале. И записи журнала полезны для поддержки ограничения скорости.
Внесение изменений в встречу
Один из простых способов внесения изменений в совещание, которое было завершено, — просто позволить организатору повторно открыть его, вернув его на этап планирования. Затем они могут добавить дату и время, выбрать новые и снова завершить собрание.
Я также хотел, чтобы организатор предоставил возможность своим участникам сделать это. Это потребовало, наконец, создания возможности просмотра и обновления настроек собрания.
Я добавил настройки, чтобы участники могли либо запрашивать изменения организаторов, либо вносить их напрямую.
Когда собрание создано, его настройки инициализируются с настройками пользователя по умолчанию:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public function initializeMeetingSetting($meeting_id,$owner_id) {
$checkMtgStg = MeetingSetting::find()->where([‘meeting_id’ => $meeting_id])->one();
if (is_null($checkMtgStg)) {
// load meeting creator (owner) user settings to initialize meeting_settings
UserSetting::initialize($owner_id);
$user_setting = UserSetting::find()->where([‘user_id’ => $owner_id])->one();
$meeting_setting = new MeetingSetting();
$meeting_setting->meeting_id = $meeting_id;
$meeting_setting->participant_add_place=$user_setting->participant_add_place;
$meeting_setting->participant_add_date_time=$user_setting->participant_add_date_time;
$meeting_setting->participant_choose_place=$user_setting->participant_choose_place;
$meeting_setting->participant_choose_date_time=$user_setting->participant_choose_date_time;
$meeting_setting->participant_finalize=$user_setting->participant_finalize;
$meeting_setting->participant_reopen=$user_setting->participant_reopen;
$meeting_setting->participant_request_change=$user_setting->participant_request_change;
$meeting_setting->save();
}
}
|
Вы можете увидеть результаты нового контроллера и вид обновления для MeetingSettings ниже:

Вот /frontend/controllers/MeetingController.php’s actionReopen() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public function actionReopen($id) {
$m = $this->findModel($id);
$m->setViewer();
// also check reopen()
if ($m->viewer == Meeting::VIEWER_ORGANIZER || $m->meetingSettings->participant_reopen) {
if ($m->reopen()) {
Yii::$app->getSession()->setFlash(‘success’, Yii::t(‘frontend’,’The meeting has now been reopened so you can make changes.’));
} else {
Yii::$app->getSession()->setFlash(‘error’, Yii::t(‘frontend’,’Sorry, you are not allowed to reopen a meeting this many times. Try creating a new meeting.’));
}
} else {
Yii::$app->getSession()->setFlash(‘error’, Yii::t(‘frontend’,’Sorry, you are not allowed to do this.’));
}
return $this->redirect([‘view’, ‘id’ => $id]);
}
|
А вот код модели Meeting.php, чтобы перевести собрание обратно в режим планирования:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
public function reopen() {
// when organizer or participant with permission asks to make changes
if (MeetingLog::withinActionLimit($this->id,MeetingLog::ACTION_REOPEN,Yii::$app->user->getId(),7)) {
$this->status = Meeting::STATUS_SENT;
$this->update();
$this->increaseSequence();
MeetingLog::add($this->id,MeetingLog::ACTION_REOPEN,Yii::$app->user->getId());
return true;
} else {
// over limit per meeting
return false;
}
}
|
withinActionLimit проверяет, сколько раз кто-то пытается открыть собрание. IncreaseSequence для файла .ics — поскольку дата, время и место встречи меняются, необходимо указать файл ics.
На изображении ниже показана встреча, которая была подтверждена множеством расширенных функций:

Когда пользователь нажимает кнопку « Внести изменения» в вышеприведенном меню, статус собрания возвращается к планированию, и он может вернуться, чтобы обновить дату, время и место:

Перенести встречу
Если события заставили участников осознать, что им просто нужно начать сначала, опция « Перепланировать» отменяет текущее собрание и создает новое приглашение к планированию.
В настоящее время я ограничиваю эту функцию организаторами (не участниками), но могу расширить ее позже. Метод Meeting.php::Reschedule() поддерживает любого человека, выполняющего действие:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
public function reschedule() {
$newOwner = $user_id = Yii::$app->user->getId();
// user can only cancel their own Meeting
if ($this->owner_id == $user_id) {
$addParticipant = false;
$this->cancel($user_id);
MeetingLog::add($this->id,MeetingLog::ACTION_RESCHEDULE,$user_id);
} else {
// if user is participant — needs to reverse
if (!isAttendee($this->id,$user_id)) {
// user isn’t owner or participant — error
return false;
} else {
// reverse the owner and participant
$addParticipant = $this->owner_id;
}
}
// create new meeting — as copy of old meeting
$m = new Meeting();
$m->attributes = $this->attributes;
$m->owner_id = $newOwner;
$m->status = Meeting::STATUS_PLANNING;
$m->created_at = time();
$m->updated_at = time();
$m->logged_at = 0;
$m->cleared_at = 0;
$m->sequence_id = 0;
$m->save();
// clone the selected place (not all of them)
$chosenPlace = $this->getChosenPlace($this->id);
if ($chosenPlace!==false) {
$mp = new MeetingPlace;
$mp->suggested_by = $newOwner;
$mp->attributes = $chosenPlace->attributes;
$mp->meeting_id = $m->id;
$mp->created_at = time();
$mp->updated_at = time();
$mp->save();
}
// clone the participants
foreach ($this->participants as $p) {
// skip if reschedule new owner was a participant
if ($p->participant_id==$user_id) {
continue;
}
// note Participant afterSave will create choices for place
$clone_p = new Participant();
$clone_p->attributes = $p->attributes;
$clone_p->email = User::findOne($p->participant_id)->email;
$clone_p->meeting_id = $m->id;
$clone_p->invited_by = $newOwner;
$clone_p->status = Participant::STATUS_DEFAULT;
$clone_p->created_at = time();
$clone_p->updated_at = time();
$clone_p->save();
}
// if participant asked to reschedule — not yet allowed
if ($addParticipant!==false) {
$newP = new Participant();
$newP->meeting_id = $m->id;
$newP->participant_id = $addParticipant;
$newP->invited_by = $user_id;
$newP->status = Participant::STATUS_DEFAULT;
$newP->created_at = time();
$newP->updated_at = time();
$newP->save();
}
return $m->id;
}
|
Участники и выбранное место клонируются в новом собрании.
Повторить встречу
Другой подход к планированию — дать участникам возможность дублировать прошлые встречи. В будущем я мог бы позволить зрителю решить, какие участники, места и время будут дублироваться, но на данный момент Планировщик собраний создает новую встречу с теми же участниками в том же месте — и идентичный день недели и время первого дня и две недели в будущем.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
public function repeat() {
// to do — expand repeat meeting to have more options
// eg pick same day and time in future week or two
// eg duplicate chosenplace or all places
// eg duplicate all participants or just some (complicated if participant duplicates)
$newOwner = $user_id = Yii::$app->user->getId();
// if user is participant — needs to reverse
if ($this->owner_id == $user_id) {
$addParticipant = false;
} else {
if (!isAttendee($this->id,$user_id)) {
// user isn’t owner or participant — error
return false;
} else {
// reverse the owner and participant
$addParticipant = $this->owner_id;
}
}
// create new meeting — as copy of old meeting
$m = new Meeting();
$m->attributes = $this->attributes;
$m->owner_id = $newOwner;
$m->status = Meeting::STATUS_PLANNING;
$m->created_at = time();
$m->updated_at = time();
$m->logged_at = 0;
$m->cleared_at = 0;
$m->sequence_id = 0;
$m->save();
// get prior meetings selected time and create two future times for the next two weeks
$chosenTime=$this->getChosenTime($this->id);
$mt1 = MeetingTime::createTimePlus($m->id,$m->owner_id,$chosenTime->start,$chosenTime->duration);
$mt2 = MeetingTime::createTimePlus($m->id,$m->owner_id,$mt1->start,$chosenTime->duration);
// clone the selected place (not all of them)
$chosenPlace = $this->getChosenPlace($this->id);
if ($chosenPlace!==false) {
$mp = new MeetingPlace;
$mp->suggested_by = $newOwner;
$mp->attributes = $chosenPlace->attributes;
$mp->meeting_id = $m->id;
$mp->created_at = time();
$mp->updated_at = time();
$mp->save();
}
// clone the participants
foreach ($this->participants as $p) {
// skip if reschedule new owner was a participant
if ($p->participant_id==$user_id) {
continue;
}
// note Participant afterSave will create choices for place
$clone_p = new Participant();
$clone_p->attributes = $p->attributes;
$clone_p->email = User::findOne($p->participant_id)->email;
$clone_p->meeting_id = $m->id;
$clone_p->status = Participant::STATUS_DEFAULT;
$clone_p->created_at = time();
$clone_p->updated_at = time();
$clone_p->save();
}
// if participant asked to repeat
// add the prior owner as a participant
if ($addParticipant!==false) {
$newP = new Participant();
$newP->meeting_id = $m->id;
$newP->participant_id = $addParticipant;
$newP->invited_by = $user_id;
$newP->status = Participant::STATUS_DEFAULT;
$newP->created_at = time();
$newP->updated_at = time();
$newP->save();
}
MeetingLog::add($this->id,MeetingLog::ACTION_REPEAT,$user_id,0);
return $m->id;
}
|
MeetingTime::createTimePlus() ниже добавляет время встречи в тот же день недели и время дня, но в будущем на одну неделю, даже если исходная встреча произошла несколько месяцев назад. Цикл while был необходим для старых встреч.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public static function createTimePlus($meeting_id,$suggested_by,$start,$duration,$timeInFuture = 604800) {
// finds time in multiples of a week or timeInFuture seconds ahead past the present time
$newStart = $start+$timeInFuture;
while ($newStart<time()) {
$newStart+=$timeInFuture;
}
$mt = new MeetingTime();
$mt->meeting_id = $meeting_id;
$mt->start = $newStart;
$mt->duration = $duration;
$mt->end = $mt->start+($mt->duration*3600);
$mt->suggested_by = $suggested_by;
$mt->status = MeetingTime::STATUS_SUGGESTED;
$mt->updated_at = $mt->created_at = time();
$mt->save();
return $mt;
}
|
Отправить приглашения
Я также создал функцию повторной отправки на случай, если участник не получит свое первоначальное приглашение или окончательное подтверждение.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static function resend($id) {
$sender_id = Yii::$app->user->getId();
// check if within resend limit
$cnt = MeetingLog::find()
->where([‘actor_id’=>$sender_id])
->andWhere([‘meeting_id’=>$id])
->andWhere([‘action’=>MeetingLog::ACTION_RESEND])
->count();
if ($cnt >= Meeting::RESEND_LIMIT ) {
return false;
} else {
$m = Meeting::findOne($id);
if ($m->status == Meeting::STATUS_SENT) {
$m->send($sender_id,true);
// resend the planning invitation
} else if ($m->status == Meeting::STATUS_CONFIRMED) {
// resend the confirmed invitation
$m->finalize($sender_id,true);
}
MeetingLog::add($id,MeetingLog::ACTION_RESEND,$sender_id,0);
return true;
}
}
|
Я планирую перестроить функции исходящей электронной почты, чтобы они работали асинхронно и облегчали повторную доставку, но, к счастью, текущие методы хорошо сработали в сценарии повторной отправки с небольшими изменениями.
История встреч
На протяжении всей серии мы регистрировали все изменения, внесенные в собрания. Теперь у участников есть возможность просмотреть историю планирования. Это выглядит так:

MeetingLogController.php проверяет, что зритель является участником собрания, и подготавливает данные для просмотра журнала:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public function actionView($id)
{
if (!Meeting::isAttendee($id,Yii::$app->user->getId())) {
$this->redirect([‘site/authfailure’]);
}
$timezone = MiscHelpers::fetchUserTimezone(Yii::$app->user->getId());
Yii::$app->timeZone = $timezone;
$searchModel = new MeetingLogSearch();
$dataProvider = $searchModel->search([‘MeetingLogSearch’=>[‘meeting_id’=>$id]]);
$m= Meeting::findOne($id);
return $this->render(‘index’, [
‘searchModel’ => $searchModel,
‘dataProvider’ => $dataProvider,
‘meeting_id’ => $id,
‘subject’ => $m->getMeetingHeader(‘log’),
‘timezone’ => $timezone,
]);
}
|
Затем /frontend/views/meeting-log/index.php отображает данные, показанные выше:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
<?php Pjax::begin();
<div class=»meeting-log-index»>
<h1><?php echo Html::encode($this->title) ?></h1>
<?php echo GridView::widget([
‘dataProvider’ => $dataProvider,
//’filterModel’ => $searchModel,
‘columns’ => [
[
‘label’=>’Actor’,
‘attribute’ => ‘actor_id’,
‘format’ => ‘raw’,
‘value’ => function ($model) {
return ‘<div>’.MiscHelpers::getDisplayName($model->actor_id).'</div>’;
},
],
[
‘label’=>’Action’,
‘attribute’ => ‘action’,
‘format’ => ‘raw’,
‘value’ => function ($model) {
return ‘<div>’.$model->getMeetingLogCommand().'</div>’;
},
],
[
‘label’=>’Item’,
‘attribute’ => ‘item_id’,
‘format’ => ‘raw’,
‘value’ => function ($model) {
return ‘<div>’.$model->getMeetingLogItem().'</div>’;
},
],
[
‘label’=>’Created’,
‘attribute’ => ‘created_at’,
‘format’ => ‘raw’,
‘value’ => function ($model) {
return ‘<div>’.Yii::$app->formatter->asDatetime($model->created_at,»hh:ss MMM d»).'</div>’;
},
],
],
]);
?>
<?= Html::a(Yii::t(‘frontend’, ‘Return to Meeting’), [‘meeting/view’, ‘id’ => $meeting_id],
[‘class’ => ‘btn btn-primary btn-info’,
‘title’=>Yii::t(‘frontend’,’Return to meeting page’),
]);
<?php Pjax::end();
|
Что дальше?
Сейчас я нахожусь на интенсивном спринте кода, чтобы завершить бета-релиз. Боги редакции Envato Tuts + старались изо всех сил, чтобы отвлечь меня роботами, управляющими сознанием, и звуками, напоминающими OKCupid, из их приложения для работы на iOS, но они потерпели неудачу. Разработка Meeting Planner продолжается быстрыми темпами.
Решение социальной инженерии и UX для участников собрания, чтобы запросить и отреагировать на небольшие изменения в расписании, может сделать или разрушить бренд Meeting Planner, и это то, над чем я работаю больше всего.
Если вы еще этого не сделали, запланируйте свою первую встречу с Планировщиком совещаний . Я также планирую написать учебное пособие по краудфандингу, поэтому просим вас посетить нашу страницу Планировщика собраний WeFunder . Вы также можете обратиться ко мне @reifman . Я всегда открыт для новых идей и тематических предложений для будущих уроков.
Следите за всеми этими и другими учебниками, ознакомившись с серией « Построение стартапа на PHP» .