Статьи

Создание вашего стартапа с помощью PHP: планирование доступности и выбора

Конечный продукт
Что вы будете создавать

Это руководство является частью серии « Построение стартапа на PHP» для Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом шагу я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, которые вы можете изучить. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения

Весь код для Meeting Planner написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с серией моих параллельных программ по программированию на Yii2 в Tuts +. Вы также можете посетить мой сайт базы знаний по вопросам Yii2, The Yii2 Developer Exchange .

Кодирование для функциональности расписания встреч охватывает как минимум четыре эпизода. Это второй из этих четырех эпизодов, в котором основное внимание уделяется добавлению AJAX на страницу планирования, чтобы пользователи могли устанавливать свою доступность и выбирать места, даты и время. Если вы пропустили предыдущее руководство по планированию собрания , вернитесь и прочитайте его, прежде чем продолжить.

В следующем уроке мы рассмотрим доставку приглашения на собрание по электронной почте. Позже мы вернемся к оптимизации и полировке пользовательского интерфейса, потому что это имеет решающее значение для успеха этого продукта.

Страница просмотра расписания собраний

Представление расписания встречи было относительно сложным для кодирования. Визуальное расположение столбцов таблицы не имеет прямого отношения к тому, как мы храним соответствующие данные в нашей базе данных и схеме. К счастью, каждое собрание не имеет большого набора данных о месте и дате и времени, поэтому это не представляет особой проблемы с производительностью.

В нашей схеме доступность места для организаторов и участников (т. Е. Является ли место приемлемым для них для этого собрания) хранится в таблице MeetingPlaceChoice . Используя нашу реляционную модель, каждое собрание имеет много MeetingPlaces которые имеют много MeetingPlaceChoices .

Не путайте таблицу MeetingPlaceChoice с окончательным выбором места, сохраненного в MeetingPlace->status .

Таблица, показанная выше, будет выглядеть иначе, когда ее увидит организатор:

  • Место 1 | Организатор Участник | MeetingPlace.choice
  • Место 2 | Организатор Участник | MeetingPlace.choice

С момента, когда участник просматривает это:

  • Место 1 | Участник | Организатор (может быть, можно сделать MeetingPlace.choice)
  • Место 2 | Участник | Организатор (может быть, можно сделать MeetingPlace.choice)

Теперь давайте обсудим, как реализовать эти таблицы в представлениях.

На данный момент я решил отображать каждую область, например место или дату и время, на своей собственной панели Bootstrap с таблицами .

В \frontend\views\meeting\view.php вы увидите включение для панели « \frontend\views\meeting\view.php , например:

1
2
3
4
<?= $this->render(‘../meeting-place/_panel’, [
    ‘model’=>$model,
    ‘placeProvider’ => $placeProvider,
]) ?>

Вот часть файла представления панели meeting-place . Это устанавливает сетку таблицы и включает виджет представления списка для отображения строк:

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
<?php
use yii\helpers\Html;
use yii\widgets\ListView;
?>
<div class=»panel panel-default»>
  <!— Default panel contents —>
  <div class=»panel-heading»>
    <div class=»row»>
      <div class=»col-lg-6″><h4><?= Yii::t(‘frontend’,’Places’) ?></h4></div>
      <div class=»col-lg-6″ ><div style=»float:right;»><?= Html::a(», [‘meeting-place/create’, ‘meeting_id’ => $model->id], [‘class’ => ‘btn btn-primary glyphicon glyphicon-plus’]) ?></div>
    </div>
  </div>
  </div>
 
  <?php
   if ($placeProvider->count>0):
  ?>
  <table class=»table»>
     <thead>
     <tr class=»small-header»>
       <td></td>
       <td ><?=Yii::t(‘frontend’,’You’) ?></td>
        <td ><?=Yii::t(‘frontend’,’Them’) ?></td>
        <td >
          <?php
           if ($placeProvider->count>1) echo Yii::t(‘frontend’,’Choose’);
          ?> </tr>
    </thead>
    <?= ListView::widget([
           ‘dataProvider’ => $placeProvider,
           ‘itemOptions’ => [‘class’ => ‘item’],
           ‘layout’ => ‘{items}’,
           ‘itemView’ => ‘_list’,
           ‘viewParams’ => [‘placeCount’=>$placeProvider->count],
       ]) ?>
  </table>
     
  <?php else: ?>
  <?php endif;
 
</div>

Давайте внимательнее посмотрим на список мест meeting-place .

В Yii Listview будет отображена строка данных для каждого Места. Код работает почти одинаково для времени даты.

Я использую виджет ввода Yii2 Switch Krajee для Bootstrap Switch вместо скучных флажков и комбинированных окон:

Примеры ввода переключателя начальной загрузки

Мне нравится, как опция трех состояний позволяет нам показать участникам уникальное состояние до того, как они сделают выбор; это также позволяет нам показать организатору, что участник еще не сделал выбор.

Давайте пройдемся по коду столбец за столбцом. Вот панель Place и таблица, которую мы реализуем:

Панель Place

В первом столбце я использую хелпер ссылок Yii Html, чтобы связать название места с его собственной страницей просмотра — обратите внимание, как мы используем слаг места.

01
02
03
04
05
06
07
08
09
10
11
<?php
use yii\helpers\Html;
use yii\helpers\BaseUrl;
use \kartik\switchinput\SwitchInput;
 
?>
 
<tr >
  <td style >
        <?= Html::a($model->place->name,BaseUrl::home().’/place/’.$model->place->slug) ?>
  </td>

Чтобы найти выбор организатора, мы перебираем массив MeetingPlaceChoices , сопоставляя user_id с meeting->owner_id :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<td style>
     <?
     foreach ($model->meetingPlaceChoices as $mpc) {
       if ($mpc->user_id == $model->meeting->owner_id) {
           if ($mpc->status == $mpc::STATUS_YES)
             $value = 1;
           else
             $value =0;
           echo SwitchInput::widget([
           ‘type’=>SwitchInput::CHECKBOX,
           ‘name’ => ‘meeting-place-choice’,
           ‘id’=>’mpc-‘.$mpc->id,
           ‘value’ => $value,
           ‘pluginOptions’ => [‘size’ => ‘mini’,’onText’ => ‘<i class=»glyphicon glyphicon-ok»></i>’,’offText’=>'<i class=»glyphicon glyphicon-remove»></i>’,’onColor’ => ‘success’,’offColor’ => ‘danger’,],
           ]);
       }
     }
     ?>
    </td>

Для выбора вашей доступности в определенном месте мы используем режим флажка входа переключателя, т.е. это место работает для вас (включено) или нет (выключено).

value   свойство устанавливает переключатель на нагрузку. Идентификатор, соответствующий MeetingPlaceChoice->id , используется для AJAX ниже для идентификации этого конкретного переключателя.

Вы также можете заметить, что мы используем глифы для да и нет вместо меток.

Код для участника реализует переключатели трех состояний. то есть это место работает для вас (включено), не работает (выключено) или вы еще не указали (неопределенно):

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
<td style>
   <?
 foreach ($model->meetingPlaceChoices as $mpc) {
   if (count($model->meeting->participants)==0) break;
   if ($mpc->user_id == $model->meeting->participants[0]->participant_id) {
       if ($mpc->status == $mpc::STATUS_YES)
         $value = 1;
       else if ($mpc->status == $mpc::STATUS_NO)
         $value =0;
       else if ($mpc->status == $mpc::STATUS_UNKNOWN)
         $value =-1;
       echo SwitchInput::widget([
         ‘type’=>SwitchInput::CHECKBOX,
         ‘name’ => ‘meeting-place-choice’,
         ‘id’=>’mpc-‘.$mpc->id,
         ‘tristate’=>true,
         ‘indeterminateValue’=>-1,
         ‘indeterminateToggle’=>false,
         ‘disabled’=>true,
         ‘value’ => $value,
         ‘pluginOptions’ => [‘size’ => ‘mini’,’onText’ => ‘<i class=»glyphicon glyphicon-ok»></i>’,’offText’=>'<i class=»glyphicon glyphicon-remove»></i>’,’onColor’ => ‘success’,’offColor’ => ‘danger’],
     ]);
   }
 }
   ?>
 </td>

Когда мы добавим поддержку собраний, которая позволяет участнику предлагать места и время, мы добавим виджеты с тремя состояниями и в столбец организатора.

Если организатор просматривает собрание, мы позволим им выбрать окончательное место встречи и дату. Вскоре мы также добавим поддержку собраний, чтобы участники могли выбирать их.

В этом случае пользователь делает выбор между строками (выбирая одно из перечисленных мест). Для этого необходимо использовать переключатель переключателя в режиме переключателя. Для событий AJAX для селекторов мы можем просто прослушивать свойство name — идентификатор не требуется, поскольку для панели возможен только один выбор.

Я также хотел, чтобы переключатели выбора выглядели иначе, чем переключатели доступности, поэтому я сделал их шире и использовал разные цвета.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<td style>
      <?
      if ($placeCount>1) {
        if ($model->status == $model::STATUS_SELECTED) {
            $value = $model->id;
        } else {
          $value = 0;
        }
        echo SwitchInput::widget([
          ‘type’ => SwitchInput::RADIO,
          ‘name’ => ‘place-chooser’,
            ‘items’ => [
                [ ‘value’ => $model->id],
            ],
            ‘value’ => $value,
            ‘pluginOptions’ => [ ‘size’ => ‘mini’,’handleWidth’=>60,’onText’ => ‘<i class=»glyphicon glyphicon-ok»></i>’,’offText’=>'<i class=»glyphicon glyphicon-remove»></i>’],
            ‘labelOptions’ => [‘style’ => ‘font-size: 12px’],
        ]);
      }
      ?>
  </td>
</tr>

Теперь я расскажу, как мы реализовали поддержку AJAX для всех этих селекторов.

Очевидно, я хотел не требовать от пользователей сохранения изменений в этих формах. Вместо этого я хотел, чтобы переключатели изменили состояние через AJAX без обновления страницы.

Код разделен между настройкой прослушивателей событий для реагирования на изменения состояния и действиями контроллера для записи изменений в нашей базе данных. Это также немного отличается для переключателей флажка по сравнению с переключателями радио.

Мы создаем прослушиватели событий для выполнения кода при каждом изменении состояния кнопки. Событие listen — это код JavaScript, который генерируется PHP в виде панели (для всей таблицы параметров).

Вот код внизу \frontend\views\meeting-place\_panel.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<?php
$script = <<< JS
   
// allows user to set the final place
$(‘input[name=»place-chooser»]’).on(‘switchChange.bootstrapSwitch’, function(e, s) {
// console.log(e.target.value);
  $.ajax({
     url: ‘/mp/meetingplace/choose’,
     data: {id: $model->id, ‘val’: e.target.value},
     // e.target.value is selected MeetingPlaceChoice model
     success: function(data) {
       return true;
     }
  });
});
JS;
$position = \yii\web\View::POS_READY;
$this->registerJs($script, $position);
?>

Кстати, если кто-то может сказать мне сокращенное название блока JS для PHP, опубликуйте его в разделе комментариев. Я хотел бы знать. Некоторые вещи трудно найти.

Функция registerJs в Yii отображает скрипт для определенной $position на странице. В этом случае это готовое событие.

Приведенный выше код устанавливает события прослушивателя для всех переключателей выбора мест для всех мест с помощью свойства name. Целевое значение события будет представлять выбранный идентификатор места встречи. Я расскажу больше о функции AJAX через минуту.

Другими словами, радио-события коммутатора отвечают на то, что организатор (как правило) выбирает место или дату для завершения встречи, передает идентификатор места встречи или идентификатор времени встречи.

Вот код для прослушивания изменений доступности с помощью переключателей ввода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// users can say if a place is an option for them
$(‘input[name=»meeting-place-choice»]’).on(‘switchChange.bootstrapSwitch’, function(e, s) {
  //console.log(e.target.id,s);
  // set intval to pass via AJAX from boolean state
  if (s)
    state = 1;
  else
    state =0;
  $.ajax({
     url: ‘/mp/meetingplacechoice/set’,
     data: {id: e.target.id, ‘state’: state},
     success: function(data) {
       return true;
     }
  });
});

Приемник настроен для всех свойств имени meeting-place-choice но он должен передать идентификатор, чтобы точно указать, какой MeetingPlaceChoice изменяется.

Чтобы уточнить, прослушиватели событий для переключателей ввода флажки позволяют пользователям сказать, что они доступны или не для места или даты времени. Они отправляют идентификатор места meeting-place-time или идентификатор места meeting-place-time .

Теперь давайте более подробно рассмотрим, как события AJAX вызывают действия нашего контроллера на основе PHP для записи изменений состояния в базе данных.

Вот еще раз код для выбора переключателя meeting-place :

1
2
3
4
5
6
7
8
$.ajax({
    url: ‘/mp/meetingplace/choose’,
    data: {id: $model->id, ‘val’: e.target.value},
    // e.target.value is selected MeetingPlaceChoice model
    success: function(data) {
      return true;
    }
 });

URL указывает путь к действию выбора контроллера MeetingPlace :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public function actionChoose($id,$val) {
     // meeting_place_id needs to be set active
     // other meeting_place_id for this meeting need to be set inactive
     $meeting_id = intval($id);
     $mtg=Meeting::find()->where([‘id’=>$meeting_id])->one();
     if (Yii::$app->user->getId()!=$mtg->owner_id) return false;
     // to do — also check participant id if participants allowed to choose
     foreach ($mtg->meetingPlaces as $mp) {
       if ($mp->id == intval($val)) {
         $mp->status = MeetingPlace::STATUS_SELECTED;
       }
       else {
         $mp->status = MeetingPlace::STATUS_SUGGESTED;
       }
       $mp->save();
     }
     return true;
   }

meeting_id $id представляет $id meeting_id . Значение представляет выбранный идентификатор MeetingPlace . STATUS_SELECTED указывает, что место было выбрано, тогда как STATUS_SUGGESTED указывает только то, что было предложено (не выбрано).

Этот код перебирает места встречи каждого собрания и обновляет состояние выбранного места.

Давайте снова посмотрим на код переключателей ввода, которые определяют, доступен ли кто-то для определенного места:

1
2
3
4
5
6
7
$.ajax({
    url: ‘/mp/meetingplacechoice/set’,
    data: {id: e.target.id, ‘state’: state},
    success: function(data) {
      return true;
    }
 });

Эти события вызывают действие set контроллера MeetingPlaceChoice со строкой, суффикс которой содержит идентификатор записи MeetingPlaceChoice которую необходимо обновить:

01
02
03
04
05
06
07
08
09
10
11
12
13
public function actionSet($id,$state)
   {
     // caution — incoming AJAX type issues with val
     $id=str_replace(‘mpc-‘,»,$id);
     $mpc = $this->findModel($id);
     if (Yii::$app->user->getId()!=$mpc->user_id) return false;
     if (intval($state) == 0 or $state==’false’)
       $mpc->status = MeetingPlaceChoice::STATUS_NO;
     else
       $mpc->status = MeetingPlaceChoice::STATUS_YES;
     $mpc->save();
     return $mpc->id;
   }

По соображениям безопасности нам необходимо убедиться, что запрос AJAX был инициирован реальным пользователем, который может внести эти изменения. Этот код делает это:

1
if (Yii::$app->user->getId()!=$mtg->owner_id) return false;

и

1
if (Yii::$app->user->getId()!=$mpc->user_id) return false;

Без этих проверок хакеру было бы легко написать скрипт для изменения настроек собрания для всех и каждого.

Код AJAX для указания доступности для даты и времени выбора практически идентичен.

Для поддержки всех вышеперечисленных функций нам также необходимо добавить код, который добавляет записи в таблицы MeetingPlaceChoice и MeetingTimeChoice всякий раз, когда MeetingTimeChoice участники, места и время. Для этого мы используем события YS afterSave .

Когда добавляется участник, нам нужно добавить новые строки MeetingPlaceChoice для каждого MeetingPlace и новые строки MeetingTimeChoice для каждого MeetingTime . Вот код в модели участия, который обрабатывает это автоматически для нас:

01
02
03
04
05
06
07
08
09
10
11
12
public function afterSave($insert,$changedAttributes)
   {
       parent::afterSave($insert,$changedAttributes);
       if ($insert) {
         // if Participant is added
         // add MeetingPlaceChoice & MeetingTimeChoice this participant
         $mt = new MeetingTime;
         $mt->addChoices($this->meeting_id,$this->participant_id);
         $mp = new MeetingPlace;
         $mp->addChoices($this->meeting_id,$this->participant_id);
       }
   }

Когда добавляется новое место, новые MeetingPlaceChoices необходимы для каждого участника:

01
02
03
04
05
06
07
08
09
10
public function afterSave($insert,$changedAttributes)
   {
       parent::afterSave($insert,$changedAttributes);
       if ($insert) {
         // if MeetingPlace is added
         // add MeetingPlaceChoice for owner and participants
         $mpc = new MeetingPlaceChoice;
         $mpc->addForNewMeetingPlace($this->meeting_id,$this->suggested_by,$this->id);
       }
   }

Точно так же, когда добавляется новое время даты, для MeetingTimeChoice необходимы новые записи для каждого участника:

01
02
03
04
05
06
07
08
09
10
public function afterSave($insert,$changedAttributes)
   {
       parent::afterSave($insert,$changedAttributes);
       if ($insert) {
         // if MeetingTime is added
         // add MeetingTimeChoice for owner and participants
         $mtc = new MeetingTimeChoice;
         $mtc->addForNewMeetingTime($this->meeting_id,$this->suggested_by,$this->id);
       }
   }

Предполагается, что, когда организатор собрания добавляет место или дату, это работает для них изначально.

Если есть хотя бы один приглашенный участник, одно место и один раз, организатор собрания может завершить собрание. В будущем мы также позволим участникам завершить встречу.

Хотя этот код немного изменится в будущем, в модели Meeting есть функция, которая сообщает представлению, нужно ли включать кнопку « Finalize :

01
02
03
04
05
06
07
08
09
10
11
12
13
public function canFinalize() {
       // check if meeting can be finalized by viewer
       if ($this->canSend()) {
         // organizer can always finalize
         if ($this->viewer == Meeting::VIEWER_ORGANIZER) {
           $this->isReadyToFinalize = true;
         } else {
           // viewer is a participant
           // has participant responded to one time or is there only one time
           // has participant responded to one place or is there only one place
            
         }
       }

Вот код вида:

1
<?= Html::a(Yii::t(‘frontend’, ‘Finalize’), [‘finalize’, ‘id’ => $model->id], [‘class’ => ‘btn btn-success ‘.(!$model->isReadyToFinalize?’disabled’:»)]) ?>

Как только встреча будет завершена, MeetingPlanner изменит режим с поддержки планирования на облегчение участия участников с помощью множества интересных функций, которые мы расскажем в будущих уроках.

Я хотел упомянуть несколько проблем, с которыми я столкнулся при написании кода для этого довольно сложного раздела.

Состояния SwitchInput были отправлены через JavaScript в виде логических типов, например, true или false, но мне нужно было преобразовать их в целочисленные значения, чтобы успешно передать их через AJAX на контроллеры.

1
2
3
4
5
6
7
8
// users can say if a place is an option for them
$(‘input[name=»meeting-place-choice»]’).on(‘switchChange.bootstrapSwitch’, function(e, s) {
  //console.log(e.target.id,s);
  // set intval to pass via AJAX from boolean state
  if (s)
    state = 1;
  else
    state =0;

MeetingTimeChoice виджетов MeetingPlaceChoice и MeetingTimeChoice перекрываются. Мне потребовалось некоторое время, чтобы понять, почему виджеты переключателей перестали правильно отображаться для меня, когда я добавил возможности выбора. Поскольку были перекрывающиеся идентификаторы, виджеты переключателей отображались только для первого объекта.

Необходимо было добавить префиксы, такие как mpc- или mtc- к идентификаторам и удалить их в действиях контроллера.

1
2
3
4
5
echo SwitchInput::widget([
         ‘type’=>SwitchInput::CHECKBOX,
         ‘name’ => ‘meeting-place-choice’,
         ‘id’=>’mpc-‘.$mpc->id,
         ‘tristate’=>true,

Вот где мы удаляем этот префикс в контроллере для загрузки модели:

1
2
3
4
5
public function actionSet($id,$state)
   {
     // caution — incoming AJAX type issues with val
     $id=str_replace(‘mpc-‘,»,$id);
     $mpc = $this->findModel($id);

Мне потребовалось некоторое время, чтобы узнать, как установить начальное состояние / значение загрузки для виджета ввода переключателя в режиме переключателя. Там не было документации, показывающей, как это сделать. Наконец-то я написал здесь объяснение для других: настройка переключателя состояния виджета Input Input .

Теперь, когда все AJAX установлены и работают, пришло время завершить некоторые из оставшихся областей представления планирования собраний, чтобы подготовить приглашения, которые были доставлены и должны быть просмотрены участниками.

Например, представление расписания собраний, которое видят участники, будет отличаться по макету от организатора, и оно будет отличаться в зависимости от того, какие полномочия делегированы организатору.

Например, столбцы «вы» и «они» необходимо будет изменить по сравнению с их текущей реализацией. Необходимо будет расширить параметры модели собрания, чтобы определить, могут ли участники предлагать места и время и завершить собрание.

В будущем я, возможно, захочу разрешить нескольким участникам и мне нужно отобразить больше столбцов доступности для организационного представления — эта функциональность не является частью нашего минимально жизнеспособного продукта (MVP).

Мне также нужно завершить внедрение MeetingLog котором будут записываться все изменения, внесенные в собрание в процессе планирования. Это обеспечит своего рода историю планирования каждой встречи. Я также могу использовать события afterSave() для этого.

Следите за будущими уроками в нашей серии «Построение стартапа с помощью PHP» — список предстоящих тем теперь опубликован в нашем оглавлении .

Пожалуйста, не стесняйтесь добавлять свои вопросы и комментарии ниже; Я обычно участвую в обсуждениях. Вы также можете связаться со мной в Twitter @reifman или написать мне напрямую.