1: <?php
2:
3: /**
4: * This file is part of the Nette Framework (https://nette.org)
5: *
6: * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
7: *
8: * For the full copyright and license information, please view
9: * the file license.txt that was distributed with this source code.
10: */
11:
12: namespace Nette\Collections;
13:
14: use Nette,
15: Nette\ObjectMixin;
16:
17:
18:
19: /**
20: * SPL ArrayObject customization.
21: *
22: * @author David Grudl
23: *
24: * @property-read bool $frozen
25: */
26: abstract class Collection extends \ArrayObject implements ICollection
27: {
28: /** @var string type (class, interface, PHP type) */
29: private $itemType;
30:
31: /** @var string function to verify type */
32: private $checkFunc;
33:
34: /** @var bool */
35: private $frozen = FALSE;
36:
37:
38:
39: /**
40: * @param array to wrap
41: * @param string class/interface name or ':type'
42: * @throws \InvalidArgumentException
43: */
44: public function __construct($arr = NULL, $type = NULL)
45: {
46: if (substr($type, 0, 1) === ':') {
47: $this->itemType = substr($type, 1);
48: $this->checkFunc = 'is_' . $this->itemType;
49: } else {
50: $this->itemType = $type;
51: }
52:
53: if ($arr !== NULL) {
54: $this->import($arr);
55: }
56: }
57:
58:
59:
60: /**
61: * Appends the specified element to the end of this collection.
62: * @param mixed
63: * @return void
64: * @throws \InvalidArgumentException
65: */
66: public function append($item)
67: {
68: $this->beforeAdd($item);
69: parent::append($item);
70: }
71:
72:
73:
74: /**
75: * Removes the first occurrence of the specified element.
76: * @param mixed
77: * @return bool true if this collection changed as a result of the call
78: * @throws \NotSupportedException
79: */
80: public function remove($item)
81: {
82: $this->updating();
83: $index = $this->search($item);
84: if ($index === FALSE) {
85: return FALSE;
86: } else {
87: parent::offsetUnset($index);
88: return TRUE;
89: }
90: }
91:
92:
93:
94: /**
95: * Returns the index of the first occurrence of the specified element,.
96: * or FALSE if this collection does not contain this element.
97: * @param mixed
98: * @return int|FALSE
99: */
100: protected function search($item)
101: {
102: return array_search($item, $this->getArrayCopy(), TRUE);
103: }
104:
105:
106:
107: /**
108: * Removes all of the elements from this collection.
109: * @return void
110: * @throws \NotSupportedException
111: */
112: public function clear()
113: {
114: $this->updating();
115: parent::exchangeArray(array());
116: }
117:
118:
119:
120: /**
121: * Returns true if this collection contains the specified item.
122: * @param mixed
123: * @return bool
124: */
125: public function contains($item)
126: {
127: return $this->search($item) !== FALSE;
128: }
129:
130:
131:
132: /**
133: * Import from array or any traversable object.
134: * @param array|\Traversable
135: * @return void
136: * @throws \InvalidArgumentException
137: */
138: public function import($arr)
139: {
140: if (!(is_array($arr) || $arr instanceof \Traversable)) {
141: throw new \InvalidArgumentException("Argument must be traversable.");
142: }
143:
144: $this->clear();
145: foreach ($arr as $item) {
146: $this->offsetSet(NULL, $item);
147: }
148: }
149:
150:
151:
152: /**
153: * Returns the item type.
154: * @return string
155: */
156: public function getItemType()
157: {
158: return $this->itemType;
159: }
160:
161:
162:
163: /**
164: * @deprecated
165: */
166: public function setReadOnly()
167: {
168: throw new \DeprecatedException(__METHOD__ . '() is deprecated; use freeze() instead.');
169: }
170:
171:
172:
173: /**
174: * @deprecated
175: */
176: public function isReadOnly()
177: {
178: throw new \DeprecatedException(__METHOD__ . '() is deprecated; use isFrozen() instead.');
179: }
180:
181:
182:
183: /********************* internal notifications ****************d*g**/
184:
185:
186:
187: /**
188: * Responds when the item is about to be added to the collection.
189: * @param mixed
190: * @return void
191: * @throws \InvalidArgumentException, \NotSupportedException
192: */
193: protected function beforeAdd($item)
194: {
195: $this->updating();
196:
197: if ($this->itemType !== NULL) {
198: if ($this->checkFunc === NULL) {
199: if (!($item instanceof $this->itemType)) {
200: throw new \InvalidArgumentException("Item must be '$this->itemType' object.");
201: }
202: } else {
203: $fnc = $this->checkFunc;
204: if (!$fnc($item)) {
205: throw new \InvalidArgumentException("Item must be $this->itemType type.");
206: }
207: }
208: }
209: }
210:
211:
212:
213: /********************* ArrayObject cooperation ****************d*g**/
214:
215:
216:
217: /**
218: * Returns the iterator.
219: * @return ArrayIterator
220: */
221: public function getIterator()
222: {
223: return new \ArrayIterator($this->getArrayCopy());
224: }
225:
226:
227:
228: /**
229: * Not supported. Use import().
230: */
231: public function exchangeArray($array)
232: {
233: throw new \NotSupportedException('Use ' . __CLASS__ . '::import()');
234: }
235:
236:
237:
238: /**
239: * Protected exchangeArray().
240: * @param array new array
241: * @return Collection provides a fluent interface
242: */
243: protected function setArray($array)
244: {
245: parent::exchangeArray($array);
246: return $this;
247: }
248:
249:
250:
251: /********************* Nette\Object behaviour ****************d*g**/
252:
253:
254:
255: /**
256: * @return Nette\Reflection\ClassReflection
257: */
258: public static function getReflection()
259: {
260: return new Nette\Reflection\ClassReflection(get_called_class());
261: }
262:
263:
264:
265: /**
266: * Call to undefined method.
267: * @throws \MemberAccessException
268: */
269: public function __call($name, $args)
270: {
271: return ObjectMixin::call($this, $name, $args);
272: }
273:
274:
275:
276: /**
277: * Call to undefined static method.
278: * @throws \MemberAccessException
279: */
280: public static function __callStatic($name, $args)
281: {
282: $class = get_called_class();
283: throw new \MemberAccessException("Call to undefined static method $class::$name().");
284: }
285:
286:
287:
288: /**
289: * Returns property value. Do not call directly.
290: * @throws \MemberAccessException if the property is not defined.
291: */
292: public function &__get($name)
293: {
294: return ObjectMixin::get($this, $name);
295: }
296:
297:
298:
299: /**
300: * Sets value of a property. Do not call directly.
301: * @throws \MemberAccessException if the property is not defined or is read-only
302: */
303: public function __set($name, $value)
304: {
305: return ObjectMixin::set($this, $name, $value);
306: }
307:
308:
309:
310: /**
311: * Is property defined?
312: * @param string property name
313: * @return bool
314: */
315: public function __isset($name)
316: {
317: return ObjectMixin::has($this, $name);
318: }
319:
320:
321:
322: /**
323: * Access to undeclared property.
324: * @throws \MemberAccessException
325: */
326: public function __unset($name)
327: {
328: throw new \MemberAccessException("Cannot unset the property {$this->reflection->name}::\$$name.");
329: }
330:
331:
332:
333: /********************* Nette\FreezableObject behaviour ****************d*g**/
334:
335:
336:
337: /**
338: * Makes the object unmodifiable.
339: * @return void
340: */
341: public function freeze()
342: {
343: $this->frozen = TRUE;
344: }
345:
346:
347:
348: /**
349: * Is the object unmodifiable?
350: * @return bool
351: */
352: final public function isFrozen()
353: {
354: return $this->frozen;
355: }
356:
357:
358:
359: /**
360: * Creates a modifiable clone of the object.
361: * @return void
362: */
363: public function __clone()
364: {
365: $this->frozen = FALSE;
366: }
367:
368:
369:
370: /**
371: * @return void
372: */
373: protected function updating()
374: {
375: if ($this->frozen) {
376: $class = get_class($this);
377: throw new \InvalidStateException("Cannot modify a frozen object '$class'.");
378: }
379: }
380:
381: }
382: