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