Статьи

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

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

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

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

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

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

Для создания функциональности этого урока потребовалась тонна кода — некоторые из них были сгенерированы автоматически Gii из Yii, а многие — вручную. Из-за сложности доставки начальных частей этой функции, я сосредоточился на довольно элементарном пользовательском интерфейсе, который я буду отлаживать итеративно.

Создание этой функции затрагивает многие аспекты программирования в среде Yii2: миграции, отношения и проверки Active Record, генерацию кода Gii, Bootstrap, расширения и виджеты пользовательского интерфейса Yii2 JQuery, AJAX, рендеринг частичных представлений, методы кодирования DRY и т. Д. Это было сложно выбрать и покрыть то, что покрыть здесь. Вы заметите множество изменений в репозитории из предыдущих серий учебников.

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

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

Страница «Встречи» с вкладками для предстоящих и отмененных

Реализация этого — еще один пример того, насколько хорош Bootstrap и насколько надежна интеграция Yii2 с Bootstrap 3.x. Bootstrap имеет встроенные вкладки .

В MeetingController мы предварительно загружаем запросы для каждого из типов собраний и отображаем представление индекса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public function actionIndex()
{
  // add filter for upcoming or past
    $upcomingProvider = new ActiveDataProvider([
        ‘query’ => Meeting::find()->joinWith(‘participants’)->where([‘owner_id’=>Yii::$app->user->getId()])->orWhere([‘participant_id’=>Yii::$app->user->getId()])->andWhere([‘Meeting.status’=>[Meeting::STATUS_PLANNING,Meeting::STATUS_CONFIRMED]]),
    ]);
    $pastProvider = new ActiveDataProvider([
        ‘query’ => Meeting::find()->joinWith(‘participants’)->where([‘owner_id’=>Yii::$app->user->getId()])->orWhere([‘participant_id’=>Yii::$app->user->getId()])->andWhere([‘Meeting.status’=>Meeting::STATUS_COMPLETED]),
    ]);
    $canceledProvider = new ActiveDataProvider([
        ‘query’ => Meeting::find()->joinWith(‘participants’)->where([‘owner_id’=>Yii::$app->user->getId()])->orWhere([‘participant_id’=>Yii::$app->user->getId()])->andWhere([‘Meeting.status’=>Meeting::STATUS_CANCELED]),
    ]);
 
    return $this->render(‘index’, [
        ‘upcomingProvider’ => $upcomingProvider,
        ‘pastProvider’ => $pastProvider,
        ‘canceledProvider’ => $canceledProvider,
    ]);
}

Затем в представлении индекса мы реализуем наш код начальной загрузки с панелями вкладок:

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
<h1><?= $this->title ?></h1>
 
<!— Nav tabs —>
<ul class=»nav nav-tabs» role=»tablist»>
  <li class=»active»><a href=»#upcoming» role=»tab» data-toggle=»tab»>Upcoming</a></li>
  <li><a href=»#past» role=»tab» data-toggle=»tab»>Past</a></li>
  <li><a href=»#canceled» role=»tab» data-toggle=»tab»>Canceled</a></li>
</ul>
 
<!— Tab panes —>
<div class=»tab-content»>
  <div class=»tab-pane active» id=»upcoming»>
    <div class=»meeting-index»>
       
      <?= $this->render(‘_grid’, [
          ‘dataProvider’ => $upcomingProvider,
      ]) ?>
 
      </div> <!— end of upcoming meetings tab —>
  </div>
  <div class=»tab-pane» id=»past»>
 
    <?= $this->render(‘_grid’, [
        ‘dataProvider’ => $pastProvider,
    ]) ?>
  </div> <!— end of past meetings tab —>
  <div class=»tab-pane» id=»canceled»>
    <?= $this->render(‘_grid’, [
        ‘dataProvider’ => $canceledProvider,
    ]) ?>
     
  </div> <!— end of canceled meetings tab —>
   
</div>

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

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

Система отслеживания проблем Lighthouse AJAXify

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

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

Собрания состоят из нескольких моделей данных ActiveRecord, например, «Участник», «MeetingTime», «MeetingPlace», «MeetingNote» и т. Д. Сначала я просто хотел использовать генерацию кода Yii для построения CRUD для каждой из этих моделей, а затем интегрировать его в одну страницу планирования. ,

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

Для многих моделей я начал с процесса, описанного в предыдущих руководствах по использованию генератора кода Yii, Gii, для построения CRUD. Затем я настроил их по мере необходимости. На данный момент я придерживаюсь очень простой формы создания встречи — она ​​даже не включает адрес электронной почты участника. Это позволяет мне быстро создать основную запись собрания и работать на странице планирования.

Создать форму встречи

После отправки формы вы можете просмотреть страницу «Встреча». Очевидно, я изменю эту форму и начальный процесс вовремя.

Помните мой макет для первого урока в этой серии:

Оригинальный Макет Планировщика Встреч для Планирования Встреч

Вот ранний взгляд на форму, в которой я работаю:

Текущая форма планировщиков собраний.

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

Хотя вряд ли окончательный дизайн, я решил использовать Bootstrap Panels для организации страницы между свойствами, местами, датами, временем и заметками. Сама страница обрабатывается действием View контроллера Meeting и вызывает частичные представления для конкретных моделей для каждой из них.

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

Вот как работает действие View контроллера Meeting. Он загружает ActiveDataProviders для каждой из моделей, а затем отображает файл представления собрания:

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
/**
    * Displays a single Meeting model.
    * @param integer $id
    * @return mixed
    */
   public function actionView($id)
   {
     $timeProvider = new ActiveDataProvider([
         ‘query’ => MeetingTime::find()->where([‘meeting_id’=>$id]),
     ]);
 
     $noteProvider = new ActiveDataProvider([
         ‘query’ => MeetingNote::find()->where([‘meeting_id’=>$id]),
     ]);
 
     $placeProvider = new ActiveDataProvider([
         ‘query’ => MeetingPlace::find()->where([‘meeting_id’=>$id]),
     ]);
 
     $participantProvider = new ActiveDataProvider([
         ‘query’ => Participant::find()->where([‘meeting_id’=>$id]),
     ]);
     $model = $this->findModel($id);
     $model->prepareView();
       return $this->render(‘view’, [
           ‘model’ => $model,
           ‘participantProvider’ => $participantProvider,
           ‘timeProvider’ => $timeProvider,
           ‘noteProvider’ => $noteProvider,
           ‘placeProvider’ => $placeProvider,
       ]);
   }

Используя все представления в каждой из связанных моделей, довольно просто отобразить всю страницу расписания с частичными представлениями MVC. В представлении «Встреча» отображаются все виды _panel для других моделей. Вы можете просмотреть документацию по методу рендеринга в Yii2 здесь .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?= $this->render(‘../participant/_panel’, [
           ‘model’=>$model,
           ‘participantProvider’ => $participantProvider,
       ]) ?>
 
       <?= $this->render(‘../meeting-place/_panel’, [
           ‘model’=>$model,
           ‘placeProvider’ => $placeProvider,
       ]) ?>
                
       <?= $this->render(‘../meeting-time/_panel’, [
           ‘model’=>$model,
           ‘timeProvider’ => $timeProvider,
       ]) ?>
 
       <?= $this->render(‘../meeting-note/_panel’, [
           ‘model’=>$model,
           ‘noteProvider’ => $noteProvider,
       ]) ?>

MeetingTimeChoice эту функциональность, я понял, что пренебрег парой необходимых моделей: MeetingPlaceChoice и MeetingTimeChoice . Они необходимы для хранения доступности организаторов и участников для определенных MeetingPlaces и MeetingTimes .

Вот миграция для MeetingPlaceChoice :

01
02
03
04
05
06
07
08
09
10
$this->createTable(‘{{%meeting_place_choice}}’, [
         ‘id’ => Schema::TYPE_PK,
         ‘meeting_place_id’ => Schema::TYPE_INTEGER.’
         ‘user_id’ => Schema::TYPE_BIGINT.’
         ‘status’ => Schema::TYPE_SMALLINT .
         ‘created_at’ => Schema::TYPE_INTEGER .
         ‘updated_at’ => Schema::TYPE_INTEGER .
     ], $tableOptions);
     $this->addForeignKey(‘fk_mpc_meeting_place’, ‘{{%meeting_place_choice}}’, ‘meeting_place_id’, ‘{{%meeting_place}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);
     $this->addForeignKey(‘fk_mpc_user_id’, ‘{{%meeting_place_choice}}’, ‘user_id’, ‘{{%user}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);

Вот миграция для MeetingTimeChoice :

01
02
03
04
05
06
07
08
09
10
$this->createTable(‘{{%meeting_time_choice}}’, [
         ‘id’ => Schema::TYPE_PK,
         ‘meeting_time_id’ => Schema::TYPE_INTEGER.’
         ‘user_id’ => Schema::TYPE_BIGINT.’
         ‘status’ => Schema::TYPE_SMALLINT .
         ‘created_at’ => Schema::TYPE_INTEGER .
         ‘updated_at’ => Schema::TYPE_INTEGER .
     ], $tableOptions);
     $this->addForeignKey(‘fk_mtc_meeting_time’, ‘{{%meeting_time_choice}}’, ‘meeting_time_id’, ‘{{%meeting_time}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);
     $this->addForeignKey(‘fk_mtc_user_id’, ‘{{%meeting_time_choice}}’, ‘user_id’, ‘{{%user}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);

Миграции ActiveGecord от Yii позволяют программно расширять схему базы данных по мере продвижения вашего продукта.

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

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

01
02
03
04
05
06
07
08
09
10
11
public function prepareView() {
       $this->setViewer();
       $this->canSend();
       $this->canFinalize();
       // has invitation been sent
        if ($this->canSend()) {
          Yii::$app->session->setFlash(‘warning’, Yii::t(‘frontend’,’This invitation has not yet been sent.’));
     }
       // to do — if sent, has invitation been opened
       // to do — if not finalized, is it within 72 hrs, 48 hrs
     }

Yii имеет встроенную поддержку для отображения предупреждений Bootstrap , называемых вспышками:

Планировщик собраний setFlash Bootstrap alert

Вот код для примера контейнера представления собрания с командными кнопками, показанными выше:

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
<div class=»panel panel-default»>
   <!— Default panel contents —>
   <div class=»panel-heading»>
     <div class=»row»>
       <div class=»col-lg-12″><h1><?= Html::encode($this->title) ?></h1></div>
     </div>
   </div>
   <div class=»panel-body»>
   <?= $model->message ?>
   </div>
   <div class=»panel-footer»>
     <div class=»row»>
       <div class=»col-lg-6″></div>
       <div class=»col-lg-6″ >
         <div style=»float:right;»>
         <?= Html::a(Yii::t(‘frontend’, ‘Send’), [‘finalize’, ‘id’ => $model->id], [‘class’ => ‘btn btn-primary ‘.(!$model->isReadyToSend?’disabled’:»)]) ?>
 
         <?= Html::a(Yii::t(‘frontend’, ‘Finalize’), [‘finalize’, ‘id’ => $model->id], [‘class’ => ‘btn btn-success ‘.(!$model->isReadyToFinalize?’disabled’:»)]) ?>
         <?= Html::a(», [‘cancel’, ‘id’ => $model->id], [‘class’ => ‘btn btn-primary glyphicon glyphicon-remove btn-danger’,’title’=>Yii::t(‘frontend’,’Cancel’)]) ?>
 
         <?= Html::a(», [‘update’, ‘id’ => $model->id], [‘class’ => ‘btn btn-primary glyphicon glyphicon-pencil’,’title’=>’Edit’]) ?>
         </div>
       </div>
   </div> <!— end row —>
   </div>
  </div>

Каждая кнопка создается с помощью HTML-помощника Yii и стилей кнопок Bootstrap:

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

Для кнопок отмены и редактирования свойств я использовал Glyphicons . Глификоны красивы и свободно включены в Bootstrap и интегрированы с Yii2.

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

Кнопка Завершить позволяет организатору (или участнику) изменить состояние собрания с планирования на предстоящее. Идея состоит в том, что после выбора места и даты, собрание может быть «завершено». До этого у участника будет возможность по желанию предложить другие места и время, а организатор (или оба из них) будет иметь возможность выбрать конечное место и дату.

Кнопка « Отмена» отменяет собрание и перемещает его на вкладку отмены на странице «Собрания».

Далее пользователь добавит людей.

Панель участников Планировщика встреч

Примечание: в моем минимально жизнеспособном продукте разрешен только один участник, но мы могли бы добавить больше позже.

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

Meeting Planner приглашает участника с автозаполнением

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
/**
    * Creates a new Participant model.
    * If creation is successful, the browser will be redirected to the ‘view’ page.
    * @return mixed
    */
   public function actionCreate($meeting_id)
   {
     $mtg = new Meeting();
     $title = $mtg->getMeetingTitle($meeting_id);
       $model = new Participant();
       $model->meeting_id= $meeting_id;
       $model->invited_by= Yii::$app->user->getId();
       // load friends for auto complete field
       $friends = Friend::getFriendList(Yii::$app->user->getId());

Вот _form.php в \frontend\views\participant _form.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<div class=»participant-form»>
   <?php $form = ActiveForm::begin();
    
   <?= $form->errorSummary($model);
    
   <p>Email address:</p>
   <?php
     // preload friends into array
     echo yii\jui\AutoComplete::widget([
         ‘model’ => $model,
         ‘attribute’ => ’email’,
         ‘clientOptions’ => [
         ‘source’ => $friends,
          ],
         ]);
   ?>

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

Когда пользователь приглашает кого-то неизвестного системе (новый адрес электронной почты), мы пассивно регистрируем его в таблице User.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
if ($model->load(Yii::$app->request->post())) {
         if (!User::find()->where( [ ’email’ => $model->email ] )->exists()) {
           // if email not already registered
           // create new user with temporary username & password
           $temp_email_arr[] = $model->email;
           $model->username = Inflector::slug(implode(‘-‘, $temp_email_arr));
           $model->password = Yii::$app->security->generateRandomString(12);
           $model->participant_id = $model->addUser();
         } else {
           // add participant from user record
           $usr = User::find()->where( [ ’email’ => $model->email ] )->one();
           $model->participant_id = $usr->id;
         }
         // validate the form against model rules
         if ($model->validate()) {
             // all inputs are valid
             $model->save();
             return $this->redirect([‘/meeting/view’, ‘id’ => $meeting_id]);
         }

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

Давайте перейдем к добавлению мест.

Существует большое преимущество использования MVC Yii для каждого контроллера и модели, а не кодирование всей этой функциональности в контроллере Meeting. Это делает понимание и управление кодом намного проще и более организованным.

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

Добавить место встречи хлебные крошки

На самом деле мы используем модель MeetingPlace для добавления мест на собрания. В \frontend\views\meeting-place\create.php мне пришлось просто настроить ссылки в области breadcrumbs :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?php
 
use yii\helpers\Html;
 
/* @var $this yii\web\View */
/* @var $model frontend\models\MeetingPlace */
 
$this->title = Yii::t(‘frontend’, ‘Add a {modelClass}’, [
    ‘modelClass’ => ‘Meeting Place’,
]);
$this->params[‘breadcrumbs’][] = [‘label’ => Yii::t(‘frontend’, ‘Meetings’), ‘url’ => [‘/meeting/index’]];
 
$this->params[‘breadcrumbs’][] = [‘label’=>$title,’url’ => [‘/meeting/view’, ‘id’ => $model->meeting_id]];
$this->params[‘breadcrumbs’][] = $this->title;
 
?>
Добавить место встречи из ваших мест или через Google Places Autocomplete

Я не только хотел настроить форму создания места, чтобы пользователи могли добавлять ранее использовавшиеся места, но и добавлять новые Google Места на лету.

В основном мне пришлось повторить ту поддержку, которую мы создали в учебнике Google Мест на карте здесь при создании MeetingPlace :

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
<?php
use yii\helpers\ArrayHelper;
use yii\helpers\BaseHtml;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use frontend\models\UserPlace;
 
use frontend\assets\MapAsset;
MapAsset::register($this);
 
/* @var $this yii\web\View */
/* @var $model frontend\models\MeetingPlace */
/* @var $form yii\widgets\ActiveForm */
?>
 
<div class=»meeting-place-form»>
 
    <?php $form = ActiveForm::begin();
 
    <?= $form->errorSummary($model);
 
    <h3>Choose one of your places</h3>
    <div class=»row»>
      <div class=»col-md-6″>
    <?= Html::activeDropDownList($model, ‘place_id’,
          ArrayHelper::map(UserPlace::find()->all(), ‘place.id’, ‘place.name’),[‘prompt’=>Yii::t(‘frontend’,’— select one of your places below —‘)] ) ?>
    <h3>- or -</h3>
    <h3>Choose from Google Places</h3>
      <p>Type in a place or business known to Google Places:</p>
        <?= $form->field($model, ‘searchbox’)->textInput([‘maxlength’ => 255])->label(‘Place’) ?>
      </div>
      <div class=»col-md-6″>
        <div id=»map-canvas»>
          <article></article>
        </div>
      </div>
      </div> <!— end row —>
        <?= BaseHtml::activeHiddenInput($model, ‘name’);
        <?= BaseHtml::activeHiddenInput($model, ‘google_place_id’);
        <?= BaseHtml::activeHiddenInput($model, ‘location’);
        <?= BaseHtml::activeHiddenInput($model, ‘website’);
        <?= BaseHtml::activeHiddenInput($model, ‘vicinity’);
        <?= BaseHtml::activeHiddenInput($model, ‘full_address’);
    <div class=»clearfix»></div>
    <div class=»row vertical-pad»>
      <div class=»form-group»>
          <?= Html::submitButton($model->isNewRecord ? Yii::t(‘frontend’, ‘Add Place’) : Yii::t(‘frontend’, ‘Update’), [‘class’ => $model->isNewRecord ? ‘btn btn-success’ : ‘btn btn-primary’]) ?>
      </div>
    </div>
 
    <?php ActiveForm::end();
 
</div>

Мне также нужно было больше использовать сложную поддержку валидации Yii2 . В приведенной ниже модели MeetingPlace мы используем уникальную проверку Yii2, чтобы сообщить об ошибке, если кто-то пытается найти место встречи, предложенное ранее:

1
2
3
4
5
6
public function rules()
   {
       return [
           [[‘meeting_id’, ‘place_id’, ‘suggested_by’], ‘required’],
           [[‘meeting_id’, ‘place_id’, ‘suggested_by’, ‘status’, ‘created_at’, ‘updated_at’], ‘integer’],
           [[‘place_id’], ‘unique’, ‘targetAttribute’ => [‘place_id’,’meeting_id’], ‘message’=>Yii::t(‘frontend’,’This place has already been suggested.’)],

Я также добавил пользовательское условие ошибки в действие создания MeetingPlaceController если пользователь выбирает места из своего списка, а также Google Place — хотя, возможно, это будет дополнительная функция, которую следует оставить (есть мнение? Пост в комментариях ниже):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public function actionCreate($meeting_id)
    {
      $mtg = new Meeting();
      $title = $mtg->getMeetingTitle($meeting_id);
        $model = new MeetingPlace();
        $model->meeting_id= $meeting_id;
        $model->suggested_by= Yii::$app->user->getId();
        $model->status = MeetingPlace::STATUS_SUGGESTED;
        $posted_form = Yii::$app->request->post();
        if ($model->load($posted_form)) {
         // check if both are chosen and return an error
          if ($model->place_id<>» and $posted_form[‘MeetingPlace’][‘google_place_id’]<>») {
            $model->addErrors([‘place_id’=>Yii::t(‘frontend’,’Please choose one or the other’)]);
            return $this->render(‘create’, [
                 ‘model’ => $model,
                  ‘title’ => $title,
             ]);
          }

Я использовал метод addErrors в Yii2 .

Я также исправил ошибку в третьем эпизоде, которая создавала несколько блоков карт всякий раз, когда кто-то изменял выбор Google Place. Проверка количества дочерних элементов в селекторе article исправила это:

1
2
3
4
5
6
7
8
9
function loadMap(gps,name) {
 if (document.querySelector(‘article’).children.length==0) {
   var mapcanvas = document.createElement(‘div’);
   mapcanvas.id = ‘mapcanvas’;
   mapcanvas.style.height = ‘300px’;
   mapcanvas.style.width = ‘300px’;
   mapcanvas.style.border = ‘1px solid black’;
   document.querySelector(‘article’).appendChild(mapcanvas);
 }

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

  • Разрешить пользователям добавлять свои текущие геолокации.
  • Предлагайте частые места, предложенные пользователем.
  • Обеспечить быстрый доступ к любимым местам пользователя.
  • Предложить места поблизости (равноудаленные) от пользователя и участников.
  • Предложить спонсорские места от платных рекламодателей.
  • Разрешить организатору удалять места и время дат — возможно, при условии, что они не были просмотрены или на них не ответил участник.

Также может быть полезно разрешить пользователям делать заметки о конкретных местах и ​​дате. Например, я мог бы указать, что «это место будет хорошо работать для меня в четверг утром, но не в пятницу днем» или «если вы выберете это время, можем ли мы сделать это в Caffe Vita на Капитолийском холме». Если у вас есть мнение по поводу этой функции (что может добавить сложность), пожалуйста, оставьте комментарий ниже.

Для каждой из моделей мы используем похожую иерархию представлений и компонентов Yii2. Контроллер собрания отображает представление для _panel.php в \frontend\views\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>

Схема таблицы, совместимой с Bootstrap, находится в _panel.php . Затем мы используем виджет Yii2 Listview для отображения каждой строки данных в виде таблицы. Часть itemView находится в _list.php .

Обратите внимание, что мы передаем пользовательскую переменную с именем placeCount через viewParams . Это удобно для настройки кнопок в таблице.

Вот представление _list.php которое я расскажу более подробно в следующем уроке, включая переключение виджетов ввода и реализацию 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
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
76
77
78
79
<?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>
  <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>
  <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>
  <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>

Чтобы добавить даты и время, мы интегрируем средство выбора даты Bootstrap JQuery через расширение 2Amigos Yii2 Date Time

MeetingPlanner Предложить время встречи
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\ActiveForm;
use dosamigos\datetimepicker\DateTimePicker;
 
/* @var $this yii\web\View */
/* @var $model frontend\models\MeetingTime */
/* @var $form yii\widgets\ActiveForm */
?>
 
<div class=»meeting-time-form»>
 
  <div class=»row»>
    <div class=»col-md-4″>
    <?php $form = ActiveForm::begin();
 
    <?= DateTimePicker::widget([
        ‘model’ => $model,
        ‘attribute’ => ‘start’,
        ‘language’ => ‘en’,
        ‘size’ => ‘ms’,
        ‘clientOptions’ => [
            ‘autoclose’ => true,
            ‘format’ => ‘MM dd, yyyy HH:ii P’,
            ‘todayBtn’ => true,
            ‘minuteStep’=> 15,
            ‘pickerPosition’ => ‘bottom-left’,
        ]
    ]);?>
    </div>
  </div>
  <div class=»clearfix»><p></div>
  <div class=»row»>
      <div class=»col-md-4″>
     <div class=»form-group»>
        <?= Html::submitButton($model->isNewRecord ? Yii::t(‘frontend’, ‘Add’) : Yii::t(‘frontend’, ‘Update’), [‘class’ => $model->isNewRecord ? ‘btn btn-success’ : ‘btn btn-primary’]) ?>
    </div>
    </div>
  </div>
    <?php ActiveForm::end();

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

Опять же, мы используем уникальный валидатор, чтобы гарантировать, что конкретная дата и время еще не были добавлены к собранию:

1
2
3
4
5
6
7
8
9
public function rules()
   {
       return [
           [[‘meeting_id’, ‘start’, ‘suggested_by’], ‘required’],
           [[‘meeting_id’, ‘start’, ‘suggested_by’, ‘status’, ‘created_at’, ‘updated_at’], ‘integer’],
           [[‘start’], ‘unique’, ‘targetAttribute’ => [‘start’,’meeting_id’], ‘message’=>Yii::t(‘frontend’,’This date and time has already been suggested.’)],
            
       ];
   }

На странице просмотра собрания панель «Даты и время» строится аналогично местам:

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
<?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’,’Dates &amp; Times’) ?></h4></div><div class=»col-lg-6″ ><div style=»float:right;»><?= Html::a(Yii::t(‘frontend’, »), [‘meeting-time/create’, ‘meeting_id’ => $model->id], [‘class’ => ‘btn btn-primary glyphicon glyphicon-plus’]) ?></div></div></div></div>
 
  <?php
   if ($timeProvider->count>0):
  ?>
  <!— Table —>
  <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 ($timeProvider->count>1) echo Yii::t(‘frontend’,’Choose’);
         ?>
        </td>
    </tr>
    </thead>
    <?= ListView::widget([
           ‘dataProvider’ => $timeProvider,
           ‘itemOptions’ => [‘class’ => ‘item’],
           ‘layout’ => ‘{items}’,
           ‘itemView’ => ‘_list’,
           ‘viewParams’ => [‘timeCount’=>$timeProvider->count],
       ]) ?>
  </table>
  <?php else: ?>
  <?php endif;
</div>

Вот представление _list.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
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
76
77
<?php
use yii\helpers\Html;
use frontend\models\Meeting;
use \kartik\switchinput\SwitchInput;
?>
 
<tr >
  <td style >
        <?= Meeting::friendlyDateFromTimestamp($model->start) ?>
  </td>
  <td style>
      <?
      foreach ($model->meetingTimeChoices as $mtc) {
        if ($mtc->user_id == $model->meeting->owner_id) {
            if ($mtc->status == $mtc::STATUS_YES)
              $value = 1;
            else
              $value =0;
              echo SwitchInput::widget([
              ‘type’ => SwitchInput::CHECKBOX,
              ‘name’ => ‘meeting-time-choice’,
              ‘id’=>’mtc-‘.$mtc->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>
  <td style>
    <?
    foreach ($model->meetingTimeChoices as $mtc) {
      if (count($model->meeting->participants)==0) break;
      if ($mtc->user_id == $model->meeting->participants[0]->participant_id) {
          if ($mtc->status == $mtc::STATUS_YES)
            $value = 1;
          else if ($mtc->status == $mtc::STATUS_NO)
            $value =0;
          else if ($mtc->status == $mtc::STATUS_UNKNOWN)
            $value =-1;
          echo SwitchInput::widget([
            ‘type’ => SwitchInput::CHECKBOX,
            ‘name’ => ‘meeting-time-choice’,
            ‘id’=>’mtc-‘.$mtc->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>
  <td style>
      <?
      if ($timeCount>1) {
        if ($model->status == $model::STATUS_SELECTED) {
            $value = $model->id;
        } else {
          $value = 0;
        }
        echo SwitchInput::widget([
            ‘type’ => SwitchInput::RADIO,
            ‘name’ => ‘time-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>

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

Добавить заметку о встрече

Вот как выглядят заметки на странице собрания:

Страница просмотра собрания с заметкой о встрече

Реализация заметок практически идентична приведенной выше реализации Places и Date Times. Вы можете просмотреть контроллер MeetingNote и MeetingNote \frontend\views\meeting-note view для получения дополнительной информации.

Я надеюсь, что вы узнали что-то новое с этим уроком. Следите за будущими уроками в моей серии «Построение стартапа с помощью PHP» — впереди много интересных функций.

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

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