Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
ObjectManager
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 17
2256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 clear
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 merge
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 cachePurge
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 put
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 multiPut
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 remove
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 multiRemove
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 serializeOffset
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 insert
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 update
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 updateSingle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 load
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 arrayEquals
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 makeArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 calcUpdatesWithoutValidation
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 splitFromRow
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Flow\Data;
4
5use Flow\DbFactory;
6use Flow\Exception\DataModelException;
7use Flow\Exception\FlowException;
8use Flow\Model\UUID;
9use SplObjectStorage;
10
11/**
12 * ObjectManager orchestrates the storage of a single type of objects.
13 * Where ObjectLocator handles querying, ObjectManager extends that to
14 * add persistence.
15 *
16 * The ObjectManager has two required constructor dependencies:
17 * * An ObjectMapper instance that can convert back and forth from domain
18 *   objects to database rows
19 * * An ObjectStorage implementation that implements persistence.
20 *
21 * Additionally there are two optional constructor arguments:
22 * * A set of Index objects that listen to life cycle events and maintain
23 *   an up-to date cache of all objects. Individual Index objects typically
24 *   answer a single set of query arguments.
25 * * A set of LifecycleHandler implementations that are notified about
26 *   insert, update, remove and load events.
27 *
28 * A simple ObjectManager instances might be created as such:
29 *
30 *   $om = new Flow\Data\ObjectManager(
31 *        Flow\Data\Mapper\BasicObjectMapper::model( 'MyModelClass' ),
32 *        new Flow\Data\Storage\BasicDbStorage(
33 *            $dbFactory,
34 *            'my_model_table',
35 *            [ 'my_primary_key' ]
36 *        )
37 *   );
38 *
39 * Objects of MyModelClass can be stored:
40 *
41 *   $om->put( $object );
42 *
43 * Objects can be retrieved via my_primary_key
44 *
45 *   $object = $om->get( $pk );
46 *
47 * The object can be updated by calling ObjectManager:put at
48 * any time.  If the object is to be deleted:
49 *
50 *   $om->remove( $object );
51 *
52 * The data cached in the indexes about this object can be cleared
53 * with:
54 *
55 *   $om->cachePurge( $object );
56 *
57 * In addition to the single-use put, get and remove there are also multi
58 * variants named multiPut, mulltiGet and multiRemove.  They perform the
59 * same operation as their namesake but with fewer network operations when
60 * dealing with multiple objects of the same type.
61 *
62 * @todo Information about Indexes and LifecycleHandlers
63 */
64class ObjectManager extends ObjectLocator {
65    /**
66     * @var SplObjectStorage Maps from a php object to the database
67     *  row that was used to create it. One use of this is to toggle between
68     *  self::insert and self::update when self::put is called.
69     */
70    protected $loaded;
71
72    /**
73     * @param ObjectMapper $mapper Convert to/from database rows/domain objects.
74     * @param ObjectStorage $storage Implements persistence(typically sql)
75     * @param DbFactory $dbFactory
76     * @param Index[] $indexes Specialized listeners that cache rows and can respond
77     *  to queries
78     * @param LifecycleHandler[] $lifecycleHandlers Listeners for insert, update,
79     *  remove and load events.
80     */
81    public function __construct(
82        ObjectMapper $mapper,
83        ObjectStorage $storage,
84        DbFactory $dbFactory,
85        array $indexes = [],
86        array $lifecycleHandlers = []
87    ) {
88        parent::__construct( $mapper, $storage, $dbFactory, $indexes, $lifecycleHandlers );
89
90        // This needs to be SplObjectStorage rather than using spl_object_hash for keys
91        // in a normal array because if the object gets GC'd spl_object_hash can reuse
92        // the value.  Stuffing the object as well into SplObjectStorage prevents GC.
93        $this->loaded = new SplObjectStorage;
94    }
95
96    /**
97     * Clear the internal cache of which objects have been loaded so far.
98     *
99     * Objects that were loaded prior to clearing the object manager must
100     * not use self::put until they have been merged via self::merge or
101     * an insert operation will be performed.
102     */
103    public function clear() {
104        $this->loaded = new SplObjectStorage;
105        $this->mapper->clear();
106        foreach ( $this->lifecycleHandlers as $handler ) {
107            $handler->onAfterClear();
108        }
109    }
110
111    /**
112     * Merge an object loaded from outside the object manager for update.
113     * Without merge using self::put will trigger an insert operation.
114     *
115     * @param object $object
116     */
117    public function merge( $object ) {
118        if ( !isset( $this->loaded[$object] ) ) {
119            $this->loaded[$object] = $this->mapper->toStorageRow( $object );
120        }
121    }
122
123    /**
124     * Purge all cached data related to this object.
125     *
126     * @param object $object
127     */
128    public function cachePurge( $object ) {
129        if ( !isset( $this->loaded[$object] ) ) {
130            throw new FlowException( 'Object was not loaded through this object manager, use ObjectManager::merge if necessary' );
131        }
132        $row = $this->loaded[$object];
133        foreach ( $this->indexes as $index ) {
134            $index->cachePurge( $object, $row );
135        }
136    }
137
138    /**
139     * Persist a single object to storage.
140     *
141     * @param object $object
142     * @param array $metadata Additional information about the object for
143     *  listeners to operate on.
144     */
145    public function put( $object, array $metadata = [] ) {
146        $this->multiPut( [ $object ], $metadata );
147    }
148
149    /**
150     * Persist multiple objects to storage.
151     *
152     * @param object[] $objects
153     * @param array $metadata Additional information about the object for
154     *  listeners to operate on.
155     */
156    public function multiPut( array $objects, array $metadata = [] ) {
157        $updateObjects = [];
158        $insertObjects = [];
159
160        foreach ( $objects as $object ) {
161            if ( isset( $this->loaded[$object] ) ) {
162                $updateObjects[] = $object;
163            } else {
164                $insertObjects[] = $object;
165            }
166        }
167
168        if ( count( $updateObjects ) ) {
169            $this->update( $updateObjects, $metadata );
170        }
171
172        if ( count( $insertObjects ) ) {
173            $this->insert( $insertObjects, $metadata );
174        }
175    }
176
177    /**
178     * Remove an object from persistent storage.
179     *
180     * @param object $object
181     * @param array $metadata Additional information about the object for
182     *  listeners to operate on.
183     */
184    public function remove( $object, array $metadata = [] ) {
185        if ( !isset( $this->loaded[$object] ) ) {
186            throw new FlowException( 'Object was not loaded through this object manager, use ObjectManager::merge if necessary' );
187        }
188        $old = $this->loaded[$object];
189        $old = $this->mapper->normalizeRow( $old );
190        $this->storage->remove( $old );
191        foreach ( $this->lifecycleHandlers as $handler ) {
192            $handler->onAfterRemove( $object, $old, $metadata );
193        }
194        unset( $this->loaded[$object] );
195    }
196
197    /**
198     * Remove multiple objects from persistent storage.
199     *
200     * @param object[] $objects
201     * @param array $metadata
202     */
203    public function multiRemove( $objects, array $metadata ) {
204        foreach ( $objects as $obj ) {
205            $this->remove( $obj, $metadata );
206        }
207    }
208
209    /**
210     * Return a string value that can be provided to self::find or self::findMulti
211     * as the offset-id option to facilitate pagination.
212     *
213     * @param object $object
214     * @param array $sortFields
215     * @return string
216     */
217    public function serializeOffset( $object, array $sortFields ) {
218        $offsetFields = [];
219        // @todo $row = $this->loaded[$object] ?
220        $row = $this->mapper->toStorageRow( $object );
221        // @todo Why not self::splitFromRow?
222        foreach ( $sortFields as $field ) {
223            $value = $row[$field];
224
225            if ( is_string( $value )
226                && strlen( $value ) === UUID::BIN_LEN
227                && substr( $field, -3 ) === '_id'
228            ) {
229                $value = UUID::create( $value );
230            }
231            if ( $value instanceof UUID ) {
232                $value = $value->getAlphadecimal();
233            }
234            $offsetFields[] = $value;
235        }
236
237        return implode( '|', $offsetFields );
238    }
239
240    /**
241     * Insert new objects into storage.
242     *
243     * @param object[] $objects
244     * @param array $metadata
245     */
246    protected function insert( array $objects, array $metadata ) {
247        $rows = array_map( [ $this->mapper, 'toStorageRow' ], $objects );
248        $storedRows = $this->storage->insert( $rows );
249        if ( !$storedRows ) {
250            throw new DataModelException( 'failed insert', 'process-data' );
251        }
252
253        $numObjects = count( $objects );
254        for ( $i = 0; $i < $numObjects; ++$i ) {
255            $object = $objects[$i];
256            $stored = $storedRows[$i];
257
258            // Propagate stuff that was added to the row by storage back
259            // into the object. Currently intended for storage URLs etc,
260            // but may in the future also bring in auto-ids and so on.
261            $this->mapper->fromStorageRow( $stored, $object );
262
263            foreach ( $this->lifecycleHandlers as $handler ) {
264                $handler->onAfterInsert( $object, $stored, $metadata );
265            }
266
267            $this->loaded[$object] = $stored;
268        }
269    }
270
271    /**
272     * Update the set of objects representation within storage.
273     *
274     * @param object[] $objects
275     * @param array $metadata
276     */
277    protected function update( array $objects, array $metadata ) {
278        foreach ( $objects as $object ) {
279            $this->updateSingle( $object, $metadata );
280        }
281    }
282
283    /**
284     * Update a single objects representation within storage.
285     *
286     * @param object $object
287     * @param array $metadata
288     */
289    protected function updateSingle( $object, array $metadata ) {
290        $old = $this->loaded[$object];
291        $old = $this->mapper->normalizeRow( $old );
292        $new = $this->mapper->toStorageRow( $object );
293        if ( self::arrayEquals( $old, $new ) ) {
294            return;
295        }
296        $this->storage->update( $old, $new );
297        foreach ( $this->lifecycleHandlers as $handler ) {
298            $handler->onAfterUpdate( $object, $old, $new, $metadata );
299        }
300        $this->loaded[$object] = $new;
301    }
302
303    /**
304     * @inheritDoc
305     */
306    protected function load( array $row ) {
307        $object = parent::load( $row );
308        $this->loaded[$object] = $row;
309        return $object;
310    }
311
312    /**
313     * Compare two arrays for equality.
314     * @todo why not $x === $y ?
315     *
316     * @param array $old
317     * @param array $new
318     * @return bool
319     */
320    public static function arrayEquals( array $old, array $new ) {
321        return array_diff_assoc( $old, $new ) === []
322            && array_diff_assoc( $new, $old ) === [];
323    }
324
325    /**
326     * Convert the input argument into an array. This is preferred
327     * over casting with (array)$value because that will cast an
328     * object to an array rather than wrap it.
329     *
330     * @param mixed $input
331     *
332     * @return array
333     */
334    public static function makeArray( $input ) {
335        if ( is_array( $input ) ) {
336            return $input;
337        } else {
338            return [ $input ];
339        }
340    }
341
342    /**
343     * Return an array containing all the top level changes between
344     * $old and $new. Expects $old and $new to be representations of
345     * database rows and contain only strings and numbers.
346     *
347     * It does not validate that it is a legal update (See DbStorage->calcUpdates).
348     *
349     * @param array $old
350     * @param array $new
351     * @return array
352     */
353    public static function calcUpdatesWithoutValidation( array $old, array $new ) {
354        $updates = [];
355        foreach ( $new as $key => $newValue ) {
356            /*
357             * $old[$key] and $new[$key] could both be the same value going into the same
358             * column, but represented as different data type here: one could be a string
359             * and another an int, of even an object (e.g. Blob)
360             * What we should be comparing is their "value", regardless of the data type
361             * (different between them doesn't matter here, both are for the same database
362             * column), so I'm casting them to string before performing comparison.
363             */
364            if ( !array_key_exists( $key, $old ) || (string)$old[$key] !== (string)$newValue ) {
365                $updates[$key] = $newValue;
366            }
367            unset( $old[$key] );
368        }
369        // These keys don't exist in $new
370        foreach ( array_keys( $old ) as $key ) {
371            $updates[$key] = null;
372        }
373        return $updates;
374    }
375
376    /**
377     * Separate a set of keys from an array. Returns null if not
378     * all keys are set.
379     *
380     * @param array $row
381     * @param string[] $keys
382     * @return array|null
383     */
384    public static function splitFromRow( array $row, array $keys ) {
385        $split = [];
386        foreach ( $keys as $key ) {
387            if ( !isset( $row[$key] ) ) {
388                return null;
389            }
390            $split[$key] = $row[$key];
391        }
392
393        return $split;
394    }
395}