Skip to content

Commit 2e01e08

Browse files
committed
Implement timestamp behavior
closes #2215
1 parent 984d0ac commit 2e01e08

File tree

2 files changed

+391
-0
lines changed

2 files changed

+391
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
/**
3+
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4+
* Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
11+
* @link http://cakephp.org CakePHP(tm) Project
12+
* @since CakePHP(tm) v 3.0.0
13+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
14+
*/
15+
namespace Cake\Model\Behavior;
16+
17+
use Cake\Event\Event;
18+
use Cake\ORM\Behavior;
19+
use Cake\ORM\Entity;
20+
use Cake\ORM\Table;
21+
22+
class TimestampBehavior extends Behavior {
23+
24+
/**
25+
* Default settings
26+
*
27+
* These are merged with user-provided settings when the behavior is used.
28+
*
29+
* refreshTimestamp - if true (the default) the timestamp used will be the current time when
30+
* the code is executed, to set to an explicit date time value - set refreshTimetamp to false
31+
* and call setTimestamp() on the behavior class before use.
32+
*
33+
* @var array
34+
*/
35+
protected $_defaultSettings = [
36+
'fields' => [
37+
'created' => 'created',
38+
'updated' => 'modified'
39+
],
40+
'refreshTimestamp' => true
41+
];
42+
43+
/**
44+
* Current timestamp
45+
*
46+
* @var \DateTime
47+
*/
48+
protected $_ts;
49+
50+
/**
51+
* Constructor
52+
*
53+
* Merge settings with the default and store in the settings property
54+
*
55+
* @param Table $table The table this behavior is attached to.
56+
* @param array $settings The settings for this behavior.
57+
*/
58+
public function __construct(Table $table, array $settings = []) {
59+
$this->_settings = $settings + $this->_defaultSettings;
60+
}
61+
62+
/**
63+
* beforeSave
64+
*
65+
* There is only one event handler, it can be configured to be called for any event
66+
*
67+
* @param Event $event
68+
* @param Entity $entity
69+
* @return true (irrespective of the behavior logic, the save will not be prevented)
70+
*/
71+
public function beforeSave(Event $event, Entity $entity) {
72+
$settings = $this->settings();
73+
$new = $entity->isNew() !== false;
74+
75+
if ($new) {
76+
$this->_updateField($entity, $settings['fields']['created'], $settings['refreshTimestamp']);
77+
}
78+
$this->_updateField($entity, $settings['fields']['updated'], $settings['refreshTimestamp']);
79+
80+
return true;
81+
}
82+
83+
/**
84+
* getTimestamp
85+
*
86+
* Gets the current timestamp. If $refreshTimestamp is not truthy, the existing timestamp will be
87+
* returned
88+
*
89+
* @return \DateTime
90+
*/
91+
public function getTimestamp($refreshTimestamp = null) {
92+
if ($this->_ts === null || $refreshTimestamp) {
93+
$this->setTimestamp();
94+
}
95+
96+
return $this->_ts;
97+
}
98+
99+
/**
100+
* setTimestamp
101+
*
102+
* Set the timestamp to the given DateTime object, or if not passed a new DateTime object
103+
*
104+
* @param int $ts
105+
* @return void
106+
*/
107+
public function setTimestamp(\DateTime $ts = null) {
108+
if ($ts === null) {
109+
$ts = new \DateTime();
110+
}
111+
$this->_ts = $ts;
112+
}
113+
114+
/**
115+
* Update a field, if it hasn't been updated already
116+
*
117+
* @param Entity $entity
118+
* @param string $field
119+
* @param bool $refreshTimestamp
120+
* @return void
121+
*/
122+
protected function _updateField(Entity $entity, $field, $refreshTimestamp) {
123+
if ($entity->dirty($field)) {
124+
return;
125+
}
126+
$entity->set($field, $this->getTimestamp($refreshTimestamp));
127+
}
128+
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<?php
2+
/**
3+
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4+
* Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
11+
* @link http://cakephp.org CakePHP(tm) Project
12+
* @since CakePHP(tm) v 3.0.0
13+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
14+
*/
15+
namespace Cake\Test\TestCase\Model\Behavior;
16+
17+
use Cake\Event\Event;
18+
use Cake\Model\Behavior\TimestampBehavior;
19+
use Cake\ORM\Entity;
20+
use Cake\TestSuite\TestCase;
21+
22+
/**
23+
* Behavior test case
24+
*/
25+
class TimestampBehaviorTest extends TestCase {
26+
27+
/**
28+
* Sanity check Implemented events
29+
*
30+
* @return void
31+
*/
32+
public function testImplementedEvents() {
33+
$table = $this->getMock('Cake\ORM\Table');
34+
$this->Behavior = new TimestampBehavior($table);
35+
36+
$expected = [
37+
'Model.beforeSave' => 'beforeSave'
38+
];
39+
$this->assertEquals($expected, $this->Behavior->implementedEvents());
40+
}
41+
42+
/**
43+
* testCreatedAbsent
44+
*
45+
* @return void
46+
*/
47+
public function testCreatedAbsent() {
48+
$table = $this->getMock('Cake\ORM\Table');
49+
$this->Behavior = new TimestampBehavior($table, ['refreshTimestamp' => false]);
50+
$ts = new \DateTime('2000-01-01');
51+
$this->Behavior->setTimestamp($ts);
52+
53+
$event = new Event('Model.beforeSave');
54+
$entity = new Entity(['name' => 'Foo']);
55+
56+
$return = $this->Behavior->beforeSave($event, $entity);
57+
$this->assertTrue($return, 'Handle Event is expected to always return true');
58+
$this->assertSame($ts, $entity->created, 'Created timestamp is expected to be the mocked value');
59+
}
60+
61+
/**
62+
* testCreatedPresent
63+
*
64+
* @return void
65+
*/
66+
public function testCreatedPresent() {
67+
$table = $this->getMock('Cake\ORM\Table');
68+
$this->Behavior = new TimestampBehavior($table, ['refreshTimestamp' => false]);
69+
$ts = new \DateTime('2000-01-01');
70+
$this->Behavior->setTimestamp($ts);
71+
72+
$event = new Event('Model.beforeSave');
73+
$existingValue = new \DateTime('2011-11-11');
74+
$entity = new Entity(['name' => 'Foo', 'created' => $existingValue]);
75+
76+
$return = $this->Behavior->beforeSave($event, $entity);
77+
$this->assertTrue($return, 'Handle Event is expected to always return true');
78+
$this->assertSame($existingValue, $entity->created, 'Created timestamp is expected to be unchanged');
79+
}
80+
81+
/**
82+
* testCreatedNotNew
83+
*
84+
* @return void
85+
*/
86+
public function testCreatedNotNew() {
87+
$table = $this->getMock('Cake\ORM\Table');
88+
$this->Behavior = new TimestampBehavior($table, ['refreshTimestamp' => false]);
89+
$ts = new \DateTime('2000-01-01');
90+
$this->Behavior->setTimestamp($ts);
91+
92+
$event = new Event('Model.beforeSave');
93+
$entity = new Entity(['name' => 'Foo']);
94+
$entity->isNew(false);
95+
96+
$return = $this->Behavior->beforeSave($event, $entity);
97+
$this->assertTrue($return, 'Handle Event is expected to always return true');
98+
$this->assertNull($entity->created, 'Created timestamp is expected to be untouched if the entity is not new');
99+
}
100+
101+
/**
102+
* testModifiedAbsent
103+
*
104+
* @return void
105+
*/
106+
public function testModifiedAbsent() {
107+
$table = $this->getMock('Cake\ORM\Table');
108+
$this->Behavior = new TimestampBehavior($table, ['refreshTimestamp' => false]);
109+
$ts = new \DateTime('2000-01-01');
110+
$this->Behavior->setTimestamp($ts);
111+
112+
$event = new Event('Model.beforeSave');
113+
$entity = new Entity(['name' => 'Foo']);
114+
$entity->isNew(false);
115+
116+
$return = $this->Behavior->beforeSave($event, $entity);
117+
$this->assertTrue($return, 'Handle Event is expected to always return true');
118+
$this->assertSame($ts, $entity->modified, 'Modified timestamp is expected to be the mocked value');
119+
}
120+
121+
/**
122+
* testModifiedPresent
123+
*
124+
* @return void
125+
*/
126+
public function testModifiedPresent() {
127+
$table = $this->getMock('Cake\ORM\Table');
128+
$this->Behavior = new TimestampBehavior($table, ['refreshTimestamp' => false]);
129+
$ts = new \DateTime('2000-01-01');
130+
$this->Behavior->setTimestamp($ts);
131+
132+
$event = new Event('Model.beforeSave');
133+
$existingValue = new \DateTime('2011-11-11');
134+
$entity = new Entity(['name' => 'Foo', 'modified' => $existingValue]);
135+
$entity->clean();
136+
$entity->isNew(false);
137+
138+
$return = $this->Behavior->beforeSave($event, $entity);
139+
$this->assertTrue($return, 'Handle Event is expected to always return true');
140+
$this->assertSame($ts, $entity->modified, 'Modified timestamp is expected to be updated');
141+
}
142+
143+
/**
144+
* testGetTimestamp
145+
*
146+
* @return void
147+
*/
148+
public function testGetTimestamp() {
149+
$table = $this->getMock('Cake\ORM\Table');
150+
$this->Behavior = new TimestampBehavior($table);
151+
152+
$property = new \ReflectionProperty('Cake\Model\Behavior\TimestampBehavior', '_ts');
153+
$property->setAccessible(true);
154+
155+
$this->assertNull($property->getValue($this->Behavior), 'Should be null be default');
156+
157+
$return = $this->Behavior->getTimestamp();
158+
$this->assertInstanceOf(
159+
'DateTime',
160+
$return,
161+
'After calling for the first time, should be a date time object'
162+
);
163+
164+
return $this->Behavior;
165+
}
166+
167+
/**
168+
* testGetTimestampPersists
169+
*
170+
* @depends testGetTimestamp
171+
* @return void
172+
*/
173+
public function testGetTimestampPersists($behavior) {
174+
$this->Behavior = $behavior;
175+
176+
$property = new \ReflectionProperty('Cake\Model\Behavior\TimestampBehavior', '_ts');
177+
$property->setAccessible(true);
178+
179+
$initialValue = $property->getValue($this->Behavior);
180+
$this->Behavior->getTimestamp();
181+
$postValue = $property->getValue($this->Behavior);
182+
183+
$this->assertSame(
184+
$initialValue,
185+
$postValue,
186+
'The timestamp should be exactly the same value'
187+
);
188+
}
189+
190+
/**
191+
* testGetTimestampRefreshes
192+
*
193+
* @depends testGetTimestamp
194+
* @return void
195+
*/
196+
public function testGetTimestampRefreshes($behavior) {
197+
$this->Behavior = $behavior;
198+
199+
$property = new \ReflectionProperty('Cake\Model\Behavior\TimestampBehavior', '_ts');
200+
$property->setAccessible(true);
201+
202+
$initialValue = $property->getValue($this->Behavior);
203+
$this->Behavior->getTimestamp(true);
204+
$postValue = $property->getValue($this->Behavior);
205+
206+
$this->assertNotSame(
207+
$initialValue,
208+
$postValue,
209+
'The timestamp should be a different object if refreshTimestamp is truthy'
210+
);
211+
}
212+
213+
/**
214+
* testSetTimestampDefault
215+
*
216+
* @return void
217+
*/
218+
public function testSetTimestampDefault() {
219+
$table = $this->getMock('Cake\ORM\Table');
220+
$this->Behavior = new TimestampBehavior($table);
221+
222+
$this->Behavior->setTimestamp();
223+
224+
$property = new \ReflectionProperty('Cake\Model\Behavior\TimestampBehavior', '_ts');
225+
$property->setAccessible(true);
226+
$set = $property->getValue($this->Behavior);
227+
228+
$this->assertInstanceOf(
229+
'DateTime',
230+
$set,
231+
'After calling for the first time, should be a date time object'
232+
);
233+
}
234+
235+
/**
236+
* testSetTimestampExplicit
237+
*
238+
* @return void
239+
*/
240+
public function testSetTimestampExplicit() {
241+
$table = $this->getMock('Cake\ORM\Table');
242+
$this->Behavior = new TimestampBehavior($table);
243+
244+
$ts = new \DateTime();
245+
$this->Behavior->setTimestamp($ts);
246+
247+
$property = new \ReflectionProperty('Cake\Model\Behavior\TimestampBehavior', '_ts');
248+
$property->setAccessible(true);
249+
$set = $property->getValue($this->Behavior);
250+
251+
$this->assertInstanceOf(
252+
'DateTime',
253+
$set,
254+
'After calling for the first time, should be a date time object'
255+
);
256+
257+
$this->assertSame(
258+
$ts,
259+
$set,
260+
'Should have set the same object passed in'
261+
);
262+
}
263+
}

0 commit comments

Comments
 (0)