По умолчанию в VT при сохранении объектов действует правило: «кто последний нажал сохранить, того и форма :)». Попробуем исправить это, поставив следующую задачу:
В VT при редактировании объекта давать сохранять именно ту версию объекта, которая была открыта на тот момент.
Если страница редактирования одновременно была открыта два раза, то при нажатии кнопки «сохранить» во второй раз система не должна позволить сохранить изменения, так как в первый раз уже было произведено сохранение.
Эта схема называется "Optimistic Concurrency Control"
Решение данной задачи можно рассматривать в различных вариантах, но мы, как программисты, определим свои ограничения заранее. У объекта могут быть связанные объекты (листы), обычно они редактируются на одной странице, поэтому нам важен сам факт нажатия кнопки «Сохранить и закрыть» или «Применить», а не то, что изменилось в объекте. Если в объекте ничего не поменялось, мы не учитываем это. По своей сути задача сводится к записи времени последнего редактирования объекта.
Попытаемся решить задачу в общем виде.
Создадим табличку objectHistory
, в которую будут записываться все изменения.
CREATE TABLE "objectHistory" ( "objectHistoryId" Serial NOT NULL, "objectId" INTEGER NOT NULL, "objectType" VARCHAR(64) NOT NULL, "userId" INTEGER NOT NULL, "userType" VARCHAR(64) NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY ("objectHistoryId") ) WITHOUT Oids; CREATE INDEX "IX_objectHistory_objectIdType" ON "objectHistory" USING btree ("objectId","objectType"); CREATE INDEX "IX_objectHistory_createdAt_desc" ON "objectHistory" USING btree ("createdAt" DESC);
Эта таблица универсальна: мы можем хранить как хронологию изменений, так и только последнее изменение.
Утилита ObjectHistoryUtility.phps будет работать только с теми объектами, у которых уже есть Factory.
Переменная $InsertsOnly
отвечает за режим работы. Если значение true
, то в objectHistory
будет хронология всех изменений, иначе - только последнее изменение.
Перейдем к интеграции с SaveAction.
/** * @var DateTime */ private $ohCreatedAt; /** * @var User */ private $user; /** * Before Action */ protected function beforeAction() { $this->user = AuthUtility::GetCurrentUser( 'User' ); $this->ohCreatedAt = Request::getDateTime( 'ohCreatedAt' ); // initialize ohCreatedAt if ( !$this->action && !$this->ohCreatedAt && $this->originalObject ) { $this->ohCreatedAt = DateTimeWrapper::Now(); } else if ( $this->action == BaseSaveAction::DeleteAction ) { $this->ohCreatedAt = DateTimeWrapper::Now(); } Response::setParameter( 'ohCreatedAt', $this->ohCreatedAt ); }
В данном куске кода мы определили переменные user
(текущий пользователь в VT) , ohCreatedAt
(дата открытия страницы редактирования, инициализируем её при первом открытии).
Не забудьте добавить в массив опций options
: WithReturningKeys ⇒ true
в конструкторе.
Добавим в validate()
следующую проверку перед return $errors
:
ObjectHistoryUtility::ValidateObject( $this->originalObject, $this->ohCreatedAt, $errors );
Метод ObjectHistoryUtility::ValidateObject
модифицирует массив $errors
в случае необходимости, проверка идет при заполненном поле $this→originalObject
.
protected function add( $object ) { ConnectionFactory::BeginTransaction(); $result = parent::$factory->Add( $object, $this->options ); $result = $result && ObjectHistoryUtility::Save( $object, $this->user ); ConnectionFactory::CommitTransaction( $result ); return $result; }
protected function update( $object ) { ConnectionFactory::BeginTransaction(); $result = parent::$factory->Update( $object ); $result = $result && ObjectHistoryUtility::Save( $object, $this->user ); if ( $result ) { Response::setParameter( 'ohCreatedAt', ObjectHistoryUtility::$LastCreatedAt ); } ConnectionFactory::CommitTransaction( $result ); return $result; }
Обновление ohCreatedAt
необходимо для кнопки «Применить».
code php
Перед подключением шаблона data.tmpl.php
добавьте:
<?= FormHelper::FormHidden( 'ohCreatedAt', !empty( $ohCreatedAt ) ? $ohCreatedAt->format('c') : '' ); ?>
После $errors[«fatal»]
добавьте:
// Render Errors from Object History echo ObjectHistoryUtility::RenderErrors( $errors );
Перед закрытием тега errors
добавьте:
<objectHistory> <alreadyUpdated><![CDATA[Объект уже был обновлен ранее в %s. Вы не можете применить свои изменения. Снова <a href="%s">переоткройте</a> эту страницу.]]></alreadyUpdated> <empty><![CDATA[Не удалось определить время открытия страницы. Вы не можете применить свои изменения. Снова <a href="%s">переоткройте</a> эту страницу.]]></empty> </objectHistory>
objectHistory
поле changes
, в котором в сериализованном виде хранить изменившиеся значения полей у объекта (и его листов). После создания такой функциональности берегитесь своей бурной фантазии :).