summaryrefslogtreecommitdiff
path: root/framework/Data/ActiveRecord/TActiveRecordStateRegistry.php
blob: 59222e4462c168a2218df0506887ca966916904a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<?php
/**
 * TActiveRecordStateRegistry class file.
 *
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005-2007 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Id$
 * @package System.Data.ActiveRecord
 */

/**
 * Active record Unit of Work class and Identity Map.
 *
 * Maintains a list of objects affected by a business transaction and
 * coordinates the writing out of changes and the resolution of concurrency problems.
 *
 * This registry keeps track of everything you do during a business transaction
 * that can affect the database. When you're done, it figures out everything that
 * needs to be done to alter the database as a result of your work.
 *
 * The object can only be in one of the four states: "new", "clean", "dirty" or "removed".
 * A "new" object is one that is created not by loading the record from database.
 * A "clean" object is one that is created by loading the record from datase.
 * A "dirty" object is one that is marked as dirty or a "clean" object that has
 * its internal state altered (done by using == object comparision).
 * A "removed" object is one that is marked for deletion.
 *
 * See the "Active Record Object States.png" in the docs directory for state
 * transition diagram.
 *
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
 * @version $Id$
 * @package System.Data.ActiveRecord
 * @since 3.1
 */
class TActiveRecordStateRegistry
{
	private $_cleanObjects=array();
	private $_removedObjects;
	private $_cachedObjects=array();
	/**
	 * Initialize the registry.
	 */
	public function __construct()
	{
		$this->_removedObjects = new TList;
	}

	/**
	 * Get the cached object for given type and row data. The cached object
	 * must also be clean.
	 * @param mixed row data fetched
	 * @return TActiveRecord cached object if found, null otherwise.
	 */
	public function getCachedInstance($data)
	{
		$key = $this->getObjectDataKey($data);
		if(isset($this->_cachedObjects[$key]))
		{
			$obj = $this->_cachedObjects[$key];
			if($this->getIsCleanObject($obj))
				return $obj;
		}
	}

	/**
	 * Cache the object that corresponding to the fetched row data.
	 * @param mixed row data fetched.
	 * @param TActiveRecord object to be cached.
	 * @return TActiveRecord cached object.
	 */
	public function addCachedInstance($data,$obj)
	{
		$key = $this->getObjectDataKey($data);
		$this->registerClean($obj);
		$this->_cachedObjects[$key]=$obj;
		return $obj;
	}

	/**
	 * @return string hash of the data.
	 */
	protected function getObjectDataKey($data)
	{
		return sprintf('%x',crc32(serialize($data)));
	}

	/**
	 * Ensure that object is not null.
	 */
	protected function assertNotNull($obj)
	{
		if(is_null($obj))
			throw new TActiveRecordException('ar_object_must_not_be_null');
	}

	/**
	 * Register the object for deletion, when the object invokes its delete() method
	 * the corresponding row in the database is deleted.
	 * @param TActiveRecord existing active record.
	 * @throws TActiveRecordException if object is null.
	 */
	public function registerRemoved($obj)
	{
		$this->assertNotNull($obj);
		$found=false;
		foreach($this->_cleanObjects as $i=>$cache)
		{
			if($cache[0]===$obj)
			{
				unset($this->_cleanObjects[$i]);
				$found=true;
			}
		}
		if(!$found)
			throw new TActiveRecordException('ar_object_must_be_retrieved_before_delete');
		if(!$this->_removedObjects->contains($obj))
			$this->_removedObjects->add($obj);
	}

	/**
	 * Register a clean object attached to a specific data that was used to
	 * populate the object. This acts as an object cache.
	 * @param TActiveRecord new clean object.
	 */
	public function registerClean($obj)
	{
		$this->removeCleanOrDirty($obj);
		if($this->getIsRemovedObject($obj))
			throw new TActiveRecordException('ar_object_marked_for_removal');
		$this->_cleanObjects[] = array($obj, clone($obj));
	}

	/**
	 * Remove the object from dirty state.
	 * @param TActiveRecord object to remove.
	 */
	protected function removeDirty($obj)
	{
		$this->assertNotNull($obj);
		foreach($this->_cleanObjects as $i=>$cache)
			if($cache[0]===$obj && $obj != $cache[1])
				unset($this->_cleanObjects[$i]);
	}

	/**
	 * Remove object from clean state.
	 * @param TActiveRecord object to remove.
	 */
	protected function removeClean($obj)
	{
		$this->assertNotNull($obj);
		foreach($this->_cleanObjects as $i=>$cache)
			if($cache[0]===$obj && $obj == $cache[1])
				unset($this->_cleanObjects[$i]);
	}

	/**
	 * Remove object from dirty and clean state.
	 * @param TActiveRecord object to remove.
	 */
	protected function removeCleanOrDirty($obj)
	{
		$this->assertNotNull($obj);
		foreach($this->_cleanObjects as $i=>$cache)
			if($cache[0]===$obj)
				unset($this->_cleanObjects[$i]);
	}

	/**
	 * Remove object from removed state.
	 * @param TActiveRecord object to remove.
	 */
	protected function removeRemovedObject($obj)
	{
		$this->_removedObjects->remove($obj);
	}

	/**
	 * Test whether an object is dirty or has been modified.
	 * @param TActiveRecord object to test.
	 * @return boolean true if the object is dirty, false otherwise.
	 */
	public function getIsDirtyObject($obj)
	{
		foreach($this->_cleanObjects as $cache)
			if($cache[0] === $obj)
				return $obj != $cache[1];
		return false;
	}

	/**
	 * Test whether an object is in the clean state.
	 * @param TActiveRecord object to test.
	 * @return boolean true if object is clean, false otherwise.
	 */
	public function getIsCleanObject($obj)
	{
		foreach($this->_cleanObjects as $cache)
			if($cache[0] === $obj)
				return $obj == $cache[1];
		return false;
	}

	/**
	 * Test whether an object is a new instance.
	 * @param TActiveRecord object to test.
	 * @return boolean true if object is newly created, false otherwise.
	 */
	public function getIsNewObject($obj)
	{
		if($this->getIsRemovedObject($obj)) return false;
		foreach($this->_cleanObjects as $cache)
			if($cache[0] === $obj)
				return false;
		return true;
	}

	/**
	 * Test whether an object is marked for deletion.
	 * @param TActiveRecord object to test.
	 * @return boolean true if object is marked for deletion, false otherwise.
	 */
	public function getIsRemovedObject($obj)
	{
		return $this->_removedObjects->contains($obj);
	}

	/**
	 * Commit the object to database:
	 *   * a new record is inserted if the object is new, object becomes clean.
	 *   * the record is updated if the object is dirty, object becomes clean.
	 *   * the record is deleted if the object is marked for removal.
	 *
	 * @param TActiveRecord record object.
	 * @param TActiveRecordGateway database gateway
	 * @return boolean true if commit was successful, false otherwise.
	 */
	public function commit($record,$gateway)
	{
		$rowsAffected=0;

		if($this->getIsRemovedObject($record))
		{
			$rowsAffected = $gateway->delete($record);
			if($rowsAffected===1)
				$this->removeRemovedObject($record);
		}
		else
		{
			if($this->getIsDirtyObject($record))
				$rowsAffected = $gateway->update($record);
			else if($this->getIsNewObject($record))
				$rowsAffected = $gateway->insert($record);

			if($rowsAffected===1)
				$this->registerClean($record);
		}
		return $rowsAffected===1;
	}
}

?>