1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\ORM\Behavior;
16:
17: use Cake\Database\Type;
18: use Cake\Datasource\EntityInterface;
19: use Cake\Event\Event;
20: use Cake\I18n\Time;
21: use Cake\ORM\Behavior;
22: use DateTime;
23: use UnexpectedValueException;
24:
25: /**
26: * Class TimestampBehavior
27: */
28: class TimestampBehavior extends Behavior
29: {
30:
31: /**
32: * Default config
33: *
34: * These are merged with user-provided config when the behavior is used.
35: *
36: * events - an event-name keyed array of which fields to update, and when, for a given event
37: * possible values for when a field will be updated are "always", "new" or "existing", to set
38: * the field value always, only when a new record or only when an existing record.
39: *
40: * refreshTimestamp - if true (the default) the timestamp used will be the current time when
41: * the code is executed, to set to an explicit date time value - set refreshTimetamp to false
42: * and call setTimestamp() on the behavior class before use.
43: *
44: * @var array
45: */
46: protected $_defaultConfig = [
47: 'implementedFinders' => [],
48: 'implementedMethods' => [
49: 'timestamp' => 'timestamp',
50: 'touch' => 'touch'
51: ],
52: 'events' => [
53: 'Model.beforeSave' => [
54: 'created' => 'new',
55: 'modified' => 'always'
56: ]
57: ],
58: 'refreshTimestamp' => true
59: ];
60:
61: /**
62: * Current timestamp
63: *
64: * @var \Cake\I18n\Time
65: */
66: protected $_ts;
67:
68: /**
69: * Initialize hook
70: *
71: * If events are specified - do *not* merge them with existing events,
72: * overwrite the events to listen on
73: *
74: * @param array $config The config for this behavior.
75: * @return void
76: */
77: public function initialize(array $config)
78: {
79: if (isset($config['events'])) {
80: $this->setConfig('events', $config['events'], false);
81: }
82: }
83:
84: /**
85: * There is only one event handler, it can be configured to be called for any event
86: *
87: * @param \Cake\Event\Event $event Event instance.
88: * @param \Cake\Datasource\EntityInterface $entity Entity instance.
89: * @throws \UnexpectedValueException if a field's when value is misdefined
90: * @return bool Returns true irrespective of the behavior logic, the save will not be prevented.
91: * @throws \UnexpectedValueException When the value for an event is not 'always', 'new' or 'existing'
92: */
93: public function handleEvent(Event $event, EntityInterface $entity)
94: {
95: $eventName = $event->getName();
96: $events = $this->_config['events'];
97:
98: $new = $entity->isNew() !== false;
99: $refresh = $this->_config['refreshTimestamp'];
100:
101: foreach ($events[$eventName] as $field => $when) {
102: if (!in_array($when, ['always', 'new', 'existing'])) {
103: throw new UnexpectedValueException(
104: sprintf('When should be one of "always", "new" or "existing". The passed value "%s" is invalid', $when)
105: );
106: }
107: if ($when === 'always' ||
108: ($when === 'new' && $new) ||
109: ($when === 'existing' && !$new)
110: ) {
111: $this->_updateField($entity, $field, $refresh);
112: }
113: }
114:
115: return true;
116: }
117:
118: /**
119: * implementedEvents
120: *
121: * The implemented events of this behavior depend on configuration
122: *
123: * @return array
124: */
125: public function implementedEvents()
126: {
127: return array_fill_keys(array_keys($this->_config['events']), 'handleEvent');
128: }
129:
130: /**
131: * Get or set the timestamp to be used
132: *
133: * Set the timestamp to the given DateTime object, or if not passed a new DateTime object
134: * If an explicit date time is passed, the config option `refreshTimestamp` is
135: * automatically set to false.
136: *
137: * @param \DateTime|null $ts Timestamp
138: * @param bool $refreshTimestamp If true timestamp is refreshed.
139: * @return \Cake\I18n\Time
140: */
141: public function timestamp(DateTime $ts = null, $refreshTimestamp = false)
142: {
143: if ($ts) {
144: if ($this->_config['refreshTimestamp']) {
145: $this->_config['refreshTimestamp'] = false;
146: }
147: $this->_ts = new Time($ts);
148: } elseif ($this->_ts === null || $refreshTimestamp) {
149: $this->_ts = new Time();
150: }
151:
152: return $this->_ts;
153: }
154:
155: /**
156: * Touch an entity
157: *
158: * Bumps timestamp fields for an entity. For any fields configured to be updated
159: * "always" or "existing", update the timestamp value. This method will overwrite
160: * any pre-existing value.
161: *
162: * @param \Cake\Datasource\EntityInterface $entity Entity instance.
163: * @param string $eventName Event name.
164: * @return bool true if a field is updated, false if no action performed
165: */
166: public function touch(EntityInterface $entity, $eventName = 'Model.beforeSave')
167: {
168: $events = $this->_config['events'];
169: if (empty($events[$eventName])) {
170: return false;
171: }
172:
173: $return = false;
174: $refresh = $this->_config['refreshTimestamp'];
175:
176: foreach ($events[$eventName] as $field => $when) {
177: if (in_array($when, ['always', 'existing'])) {
178: $return = true;
179: $entity->setDirty($field, false);
180: $this->_updateField($entity, $field, $refresh);
181: }
182: }
183:
184: return $return;
185: }
186:
187: /**
188: * Update a field, if it hasn't been updated already
189: *
190: * @param \Cake\Datasource\EntityInterface $entity Entity instance.
191: * @param string $field Field name
192: * @param bool $refreshTimestamp Whether to refresh timestamp.
193: * @return void
194: */
195: protected function _updateField($entity, $field, $refreshTimestamp)
196: {
197: if ($entity->isDirty($field)) {
198: return;
199: }
200:
201: $ts = $this->timestamp(null, $refreshTimestamp);
202:
203: $columnType = $this->getTable()->getSchema()->getColumnType($field);
204: if (!$columnType) {
205: return;
206: }
207:
208: /** @var \Cake\Database\Type\DateTimeType $type */
209: $type = Type::build($columnType);
210:
211: if (!$type instanceof Type\DateTimeType) {
212: deprecationWarning('TimestampBehavior support for column types other than DateTimeType will be removed in 4.0.');
213: $entity->set($field, (string)$ts);
214:
215: return;
216: }
217:
218: $class = $type->getDateTimeClassName();
219:
220: $entity->set($field, new $class($ts));
221: }
222: }
223: