У меня есть свойство state (значение конечного автомата) для объекта, и я устанавливаю поля datetime в зависимости от того, когда объект переходит в заданное состояние.
Состояния задачи: (упрощенное)
Задача может перейти обратно в in_progress после того, как она будет установлена на complete, но я хочу использовать метку времени для установки свойства $dateStarted только при первом переходе на in_progress.
Этот код всегда устанавливает $dateStarted на последний раз, когда $state переходит на in_progress.
Есть ли способ настроить его так, чтобы $dateStarted устанавливался только при первом переходе задачи на in_progress?
/**
* @var \DateTime
*
* @ORM\Column(type = "datetime", nullable=true)
* @Gedmo\Timestampable(on = "change", field = "state", value = "in_progress")
*/
private $dateStarted;
@fyrye Ocramius из команды Doctrine дал советы, как избегать обратных вызовов жизненного цикла (ocramius.github.io/doctrine-best-practices/#/58ocramius.github.io/doctrine-best-practices/#/59)
@Franckgamess Timestampable - это обратный вызов жизненного цикла, он использует onFlush .. Однако я согласен, это делает код менее управляемым, добавляет ненужные накладные расходы, и то же самое можно сделать в методе setState.




Как говорит Фрай, вы можете использовать обратный вызов жизненного цикла с функцией, чтобы установить дату только один раз. Я использую это так:
/**
* @ORM\PrePersist
* @ORM\PreUpdate
*/
public function setTheStartDate()
{
if ($this->getState() === 'in_progress' && $this->getDateStarted === null) {
$this->setDateStarted(new \DateTime('now'));
}
}
Также убедитесь, что у вас есть следующая аннотация для вашего класса, чтобы включить обратные вызовы жизненного цикла:
* @ORM\HasLifecycleCallbacks()
Как я упоминал в комментарии, Timestampable не может использовать выражения или метод обратного вызова для оценки измененного значения. Хотя вы можете использовать обратный вызов жизненного цикла для достижения желаемого результата. Обратные вызовы жизненного цикла действительно добавляют дополнительный уровень накладных расходов и сложности для вашей сущности, что затрудняет отслеживание возникающих из-за них проблем, а расширение кода в будущем более утомительно.
Один пример использования обратных вызовов Timestampable или Lifecycle; dateStarted не будет установлен до тех пор, пока ПОСЛЕ вы не сбросите свои изменения в базу данных. Это приводит к тому, что ссылка на значение dateStarted в другой бизнес-логике становится невозможной, пока вы не отправите их в базу данных.
Поэтому рекомендуется вместо этого поместить вашу бизнес-логику в метод setState. Это приведет к принудительному применению желаемых правил назначения dateStarted. Это также позволяет вам использовать дополнительную бизнес-логику, например, гарантировать, что состояние не перескакивает с new на complete, без предварительной установки на in_progress и т. д.
Пример: https://3v4l.org/s9PJu
class MyEntity
{
const STATE_NEW = 'new';
const STATE_IN_PROGRESS = 'in_progress';
const STATE_COMPLETE = 'complete';
const CONFIG_STATES = array(
self::STATE_NEW,
self::STATE_IN_PROGRESS,
self::STATE_COMPLETE
);
/**
* @var \DateTime|null
* @ORM\Column(type = "datetime", nullable=true)
*/
private $dateStarted;
/**
* @var string|null
* @ORM\Column(type = "string", nullable=true)
*/
private $state = self::STATE_NEW;
/**
* @var string|null $state
*/
public function setState($state = null)
{
$this->validateState($state);
$this->updateStateDate($state);
$this->state = $state;
return $this;
}
public function getState()
{
return $this->state;
}
public function getDateStarted()
{
return $this->dateStarted;
}
/**
* used to update the started date when the state changes to in_progress
* @var string $state
*/
protected function updateStateDate($state)
{
if ($this->state !== $state && $state === self::STATE_IN_PROGRESS && $this->dateStarted === null) {
$this->dateStarted = new \DateTime();
}
return $this;
}
protected function validateState($state)
{
if (!in_array($state, self::CONFIG_STATES, true)) {
throw new \InvalidArgumentException(
sprintf('Unknown state "%s". Expected one of: "%s"',
$state,
implode('", "', self::CONFIG_STATES)
)
);
}
if ($this->state === self::STATE_NEW && $state !== self::STATE_IN_PROGRESS) {
throw new \InvalidArgumentException(
sprintf('Invalid state "%s" specified, Expected: %s',
$state,
self::STATE_IN_PROGRESS
)
);
}
return $this;
}
}
Применение:
//default state = "new", dateStarted = null
$entity = new \MyEntity;
//state = "in_progress", dateStarted = \DateTime('now')
$entity->setState(MyEntity::STATE_IN_PROGRESS);
//NOTE - $entity->getDateStarted() will return a value now without first using $em->flush();
//only changes state to "complete", leaves the previously assigned date
$entity->setState(MyEntity::STATE_COMPLETE);
//only changes state to "in_progress", leaves the previously assigned date
$entity->setState(MyEntity::STATE_IN_PROGRESS);
Я также предлагаю, если это еще не сделано, преобразовать ваше поле state из строки в связанный объект. Затем вы можете удалить константы STATE и проверить в validateState, поскольку ограничение внешнего ключа будет обеспечивать правильное присвоение состояния.
В заключение, поскольку вы используете фреймворк Symfony, вы можете альтернативно использовать Ограничения проверки формы и переместить свою бизнес-логику в эти ограничения / обратные вызовы, которые будут проверяться при отправке форм и изменят вашу сущность и соответственно отобразят дружественные сообщения.
Timestampableне может использовать выражения или обратные вызовы, вместо этого используйте Обратный вызов жизненного циклаpreUpdate. Затем вы можете добавить условное событие для обновления поля по желанию.if ($this->state === 'in_progress' && null === $this->dateStarted) { $this->dateStarted = new \DateTime(); }