1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Extbase\Persistence\Generic;
17
18use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
19use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
20use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
21use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
22use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
23
24/**
25 * A proxy that can replace any object and replaces itself in it's parent on
26 * first access (call, get, set, isset, unset).
27 * @internal only to be used within Extbase, not part of TYPO3 Core API.
28 */
29class LazyObjectStorage extends ObjectStorage implements LoadingStrategyInterface
30{
31    /**
32     * This field is only needed to make debugging easier:
33     * If you call current() on a class that implements Iterator, PHP will return the first field of the object
34     * instead of calling the current() method of the interface.
35     * We use this unusual behavior of PHP to return the warning below in this case.
36     *
37     * @var string
38     */
39    private $warning = 'You should never see this warning. If you do, you probably used PHP array functions like current() on the TYPO3\\CMS\\Extbase\\Persistence\\Generic\\LazyObjectStorage. To retrieve the first result, you can use the rewind() and current() methods.';
40
41    /**
42     * @var DataMapper|null
43     */
44    protected $dataMapper;
45
46    /**
47     * The object this property is contained in.
48     *
49     * @var \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface
50     */
51    protected $parentObject;
52
53    /**
54     * The name of the property represented by this proxy.
55     *
56     * @var string
57     */
58    protected $propertyName;
59
60    /**
61     * The raw field value.
62     *
63     * @var mixed
64     */
65    protected $fieldValue;
66
67    /**
68     * @var bool
69     */
70    protected $isInitialized = false;
71
72    /**
73     * @var ObjectManagerInterface
74     */
75    protected $objectManager;
76
77    /**
78     * @param ObjectManagerInterface $objectManager
79     */
80    public function injectObjectManager(ObjectManagerInterface $objectManager)
81    {
82        $this->objectManager = $objectManager;
83    }
84
85    /**
86     * Returns the state of the initialization
87     *
88     * @return bool
89     */
90    public function isInitialized()
91    {
92        return $this->isInitialized;
93    }
94
95    /**
96     * Constructs this proxy instance.
97     *
98     * @param DomainObjectInterface $parentObject The object instance this proxy is part of
99     * @param string $propertyName The name of the proxied property in it's parent
100     * @param mixed $fieldValue The raw field value.
101     * @param ?DataMapper $dataMapper
102     */
103    public function __construct($parentObject, $propertyName, $fieldValue, ?DataMapper $dataMapper = null)
104    {
105        $this->parentObject = $parentObject;
106        $this->propertyName = $propertyName;
107        $this->fieldValue = $fieldValue;
108        reset($this->storage);
109        $this->dataMapper = $dataMapper;
110    }
111
112    /**
113     * Object initialization called when object is created with ObjectManager, after constructor
114     */
115    public function initializeObject()
116    {
117        if (!$this->dataMapper) {
118            $this->dataMapper = $this->objectManager->get(DataMapper::class);
119        }
120    }
121
122    /**
123     * This is a function lazy load implementation.
124     */
125    protected function initialize()
126    {
127        if (!$this->isInitialized) {
128            $this->isInitialized = true;
129            $objects = $this->dataMapper->fetchRelated($this->parentObject, $this->propertyName, $this->fieldValue, false);
130            foreach ($objects as $object) {
131                parent::attach($object);
132            }
133            $this->_memorizeCleanState();
134            if (!$this->isStorageAlreadyMemorizedInParentCleanState()) {
135                $this->parentObject->_memorizeCleanState($this->propertyName);
136            }
137        }
138    }
139
140    /**
141     * @return bool
142     */
143    protected function isStorageAlreadyMemorizedInParentCleanState()
144    {
145        return $this->parentObject->_getCleanProperty($this->propertyName) === $this;
146    }
147
148    // Delegation to the ObjectStorage methods below
149    /**
150     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $storage
151     *
152     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::addAll
153     */
154    public function addAll(ObjectStorage $storage)
155    {
156        $this->initialize();
157        parent::addAll($storage);
158    }
159
160    /**
161     * @param DomainObjectInterface $object The object to add.
162     * @param mixed $data The data to associate with the object.
163     *
164     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::attach
165     */
166    public function attach($object, $data = null)
167    {
168        $this->initialize();
169        parent::attach($object, $data);
170    }
171
172    /**
173     * @param DomainObjectInterface $object The object to look for.
174     * @return bool
175     *
176     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::contains
177     */
178    public function contains($object)
179    {
180        $this->initialize();
181        return parent::contains($object);
182    }
183
184    /**
185     * Counts the elements in the storage array
186     *
187     * @throws Exception
188     * @return int The number of elements in the ObjectStorage
189     */
190    public function count()
191    {
192        $columnMap = $this->dataMapper->getDataMap(get_class($this->parentObject))->getColumnMap($this->propertyName);
193        $numberOfElements = null;
194        if (!$this->isInitialized && $columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
195            $numberOfElements = $this->dataMapper->countRelated($this->parentObject, $this->propertyName, $this->fieldValue);
196        } else {
197            $this->initialize();
198            $numberOfElements = count($this->storage);
199        }
200        if ($numberOfElements === null) {
201            throw new Exception('The number of elements could not be determined.', 1252514486);
202        }
203        return $numberOfElements;
204    }
205
206    /**
207     * @return DomainObjectInterface The object at the current iterator position.
208     *
209     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::current
210     */
211    public function current()
212    {
213        $this->initialize();
214        return parent::current();
215    }
216
217    /**
218     * @param DomainObjectInterface $object The object to remove.
219     *
220     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::detach
221     */
222    public function detach($object)
223    {
224        $this->initialize();
225        parent::detach($object);
226    }
227
228    /**
229     * @return string The index corresponding to the position of the iterator.
230     *
231     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::key
232     */
233    public function key()
234    {
235        $this->initialize();
236        return parent::key();
237    }
238
239    /**
240     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::next
241     */
242    public function next()
243    {
244        $this->initialize();
245        parent::next();
246    }
247
248    /**
249     * @param DomainObjectInterface $value The object to look for, or the key in the storage.
250     * @return bool
251     *
252     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::offsetExists
253     */
254    public function offsetExists($value)
255    {
256        $this->initialize();
257        return parent::offsetExists($value);
258    }
259
260    /**
261     * @param DomainObjectInterface $value The object to look for, or its key in the storage.
262     * @return mixed
263     *
264     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::offsetGet
265     */
266    public function offsetGet($value)
267    {
268        $this->initialize();
269        return parent::offsetGet($value);
270    }
271
272    /**
273     * @param DomainObjectInterface $object The object to add.
274     * @param mixed $info The data to associate with the object.
275     *
276     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::offsetSet
277     */
278    public function offsetSet($object, $info)
279    {
280        $this->initialize();
281        parent::offsetSet($object, $info);
282    }
283
284    /**
285     * @param DomainObjectInterface $value The object to remove, or its key in the storage.
286     *
287     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::offsetUnset
288     */
289    public function offsetUnset($value)
290    {
291        $this->initialize();
292        parent::offsetUnset($value);
293    }
294
295    /**
296     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $storage The storage containing the elements to remove.
297     *
298     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::removeAll
299     */
300    public function removeAll(ObjectStorage $storage)
301    {
302        $this->initialize();
303        parent::removeAll($storage);
304    }
305
306    /**
307     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::rewind
308     */
309    public function rewind()
310    {
311        $this->initialize();
312        parent::rewind();
313    }
314
315    /**
316     * @return bool
317     *
318     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::valid
319     */
320    public function valid()
321    {
322        $this->initialize();
323        return parent::valid();
324    }
325
326    /**
327     * @return array
328     *
329     * @see \TYPO3\CMS\Extbase\Persistence\ObjectStorage::toArray
330     */
331    public function toArray()
332    {
333        $this->initialize();
334        return parent::toArray();
335    }
336
337    /**
338     * @param mixed $object
339     * @return int|null
340     */
341    public function getPosition($object)
342    {
343        $this->initialize();
344        return parent::getPosition($object);
345    }
346}
347