1: <?php
2:
3: namespace RedBeanPHP;
4:
5: use RedBeanPHP\ToolBox as ToolBox;
6: use RedBeanPHP\AssociationManager as AssociationManager;
7: use RedBeanPHP\OODB as OODB;
8: use RedBeanPHP\OODBBean as OODBBean;
9: use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
10:
11: /**
12: * Duplication Manager.
13: * Creates deep copies of beans.
14: *
15: * @file RedBeanPHP/DuplicationManager.php
16: * @author Gabor de Mooij and the RedBeanPHP Community
17: * @license BSD/GPLv2
18: *
19: * @copyright
20: * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
21: * This source file is subject to the BSD/GPLv2 License that is bundled
22: * with this source code in the file license.txt.
23: */
24: class DuplicationManager
25: {
26: /**
27: * @var ToolBox
28: */
29: protected $toolbox;
30:
31: /**
32: * @var AssociationManager
33: */
34: protected $associationManager;
35:
36: /**
37: * @var OODB
38: */
39: protected $redbean;
40:
41: /**
42: * @var array
43: */
44: protected $tables = array();
45:
46: /**
47: * @var array
48: */
49: protected $columns = array();
50:
51: /**
52: * @var array
53: */
54: protected $filters = array();
55:
56: /**
57: * @var array
58: */
59: protected $cacheTables = FALSE;
60:
61: /**
62: * Copies the shared beans in a bean, i.e. all the sharedBean-lists.
63: *
64: * @param OODBBean $copy target bean to copy lists to
65: * @param string $shared name of the shared list
66: * @param array $beans array with shared beans to copy
67: *
68: * @return void
69: */
70: private function copySharedBeans( OODBBean $copy, $shared, $beans )
71: {
72: $copy->$shared = array();
73:
74: foreach ( $beans as $subBean ) {
75: array_push( $copy->$shared, $subBean );
76: }
77: }
78:
79: /**
80: * Copies the own beans in a bean, i.e. all the ownBean-lists.
81: * Each bean in the own-list belongs exclusively to its owner so
82: * we need to invoke the duplicate method again to duplicate each bean here.
83: *
84: * @param OODBBean $copy target bean to copy lists to
85: * @param string $owned name of the own list
86: * @param array $beans array with shared beans to copy
87: * @param array $trail array with former beans to detect recursion
88: * @param boolean $preserveIDs TRUE means preserve IDs, for export only
89: *
90: * @return void
91: */
92: private function copyOwnBeans( OODBBean $copy, $owned, $beans, $trail, $preserveIDs )
93: {
94: $copy->$owned = array();
95: foreach ( $beans as $subBean ) {
96: array_push( $copy->$owned, $this->duplicate( $subBean, $trail, $preserveIDs ) );
97: }
98: }
99:
100: /**
101: * Creates a copy of bean $bean and copies all primitive properties (not lists)
102: * and the parents beans to the newly created bean. Also sets the ID of the bean
103: * to 0.
104: *
105: * @param OODBBean $bean bean to copy
106: *
107: * @return OODBBean
108: */
109: private function createCopy( OODBBean $bean )
110: {
111: $type = $bean->getMeta( 'type' );
112:
113: $copy = $this->redbean->dispense( $type );
114: $copy->setMeta( 'sys.dup-from-id', $bean->id );
115: $copy->setMeta( 'sys.old-id', $bean->id );
116: $copy->importFrom( $bean );
117: $copy->id = 0;
118:
119: return $copy;
120: }
121:
122: /**
123: * Generates a key from the bean type and its ID and determines if the bean
124: * occurs in the trail, if not the bean will be added to the trail.
125: * Returns TRUE if the bean occurs in the trail and FALSE otherwise.
126: *
127: * @param array $trail list of former beans
128: * @param OODBBean $bean currently selected bean
129: *
130: * @return boolean
131: */
132: private function inTrailOrAdd( &$trail, OODBBean $bean )
133: {
134: $type = $bean->getMeta( 'type' );
135: $key = $type . $bean->getID();
136:
137: if ( isset( $trail[$key] ) ) {
138: return TRUE;
139: }
140:
141: $trail[$key] = $bean;
142:
143: return FALSE;
144: }
145:
146: /**
147: * Given the type name of a bean this method returns the canonical names
148: * of the own-list and the shared-list properties respectively.
149: * Returns a list with two elements: name of the own-list, and name
150: * of the shared list.
151: *
152: * @param string $typeName bean type name
153: *
154: * @return array
155: */
156: private function getListNames( $typeName )
157: {
158: $owned = 'own' . ucfirst( $typeName );
159: $shared = 'shared' . ucfirst( $typeName );
160:
161: return array( $owned, $shared );
162: }
163:
164: /**
165: * Determines whether the bean has an own list based on
166: * schema inspection from realtime schema or cache.
167: *
168: * @param string $type bean type to get list for
169: * @param string $target type of list you want to detect
170: *
171: * @return boolean
172: */
173: protected function hasOwnList( $type, $target )
174: {
175: return isset( $this->columns[$target][$type . '_id'] );
176: }
177:
178: /**
179: * Determines whether the bea has a shared list based on
180: * schema inspection from realtime schema or cache.
181: *
182: * @param string $type bean type to get list for
183: * @param string $target type of list you are looking for
184: *
185: * @return boolean
186: */
187: protected function hasSharedList( $type, $target )
188: {
189: return in_array( AQueryWriter::getAssocTableFormat( array( $type, $target ) ), $this->tables );
190: }
191:
192: /**
193: * @see DuplicationManager::dup
194: *
195: * @param OODBBean $bean bean to be copied
196: * @param array $trail trail to prevent infinite loops
197: * @param boolean $preserveIDs preserve IDs
198: *
199: * @return OODBBean
200: */
201: protected function duplicate( OODBBean $bean, $trail = array(), $preserveIDs = FALSE )
202: {
203: if ( $this->inTrailOrAdd( $trail, $bean ) ) return $bean;
204:
205: $type = $bean->getMeta( 'type' );
206:
207: $copy = $this->createCopy( $bean );
208: foreach ( $this->tables as $table ) {
209:
210: if ( !empty( $this->filters ) ) {
211: if ( !in_array( $table, $this->filters ) ) continue;
212: }
213:
214: list( $owned, $shared ) = $this->getListNames( $table );
215:
216: if ( $this->hasSharedList( $type, $table ) ) {
217: if ( $beans = $bean->$shared ) {
218: $this->copySharedBeans( $copy, $shared, $beans );
219: }
220: } elseif ( $this->hasOwnList( $type, $table ) ) {
221: if ( $beans = $bean->$owned ) {
222: $this->copyOwnBeans( $copy, $owned, $beans, $trail, $preserveIDs );
223: }
224:
225: $copy->setMeta( 'sys.shadow.' . $owned, NULL );
226: }
227:
228: $copy->setMeta( 'sys.shadow.' . $shared, NULL );
229: }
230:
231: $copy->id = ( $preserveIDs ) ? $bean->id : $copy->id;
232:
233: return $copy;
234: }
235:
236: /**
237: * Constructor,
238: * creates a new instance of DupManager.
239: *
240: * @param ToolBox $toolbox
241: */
242: public function __construct( ToolBox $toolbox )
243: {
244: $this->toolbox = $toolbox;
245: $this->redbean = $toolbox->getRedBean();
246: $this->associationManager = $this->redbean->getAssociationManager();
247: }
248:
249: /**
250: * Recursively turns the keys of an array into
251: * camelCase.
252: *
253: * @param array $array array to camelize
254: * @param boolean $dolphinMode whether you want the exception for IDs.
255: *
256: * @return array
257: */
258: public function camelfy( $array, $dolphinMode = false ) {
259: $newArray = array();
260: foreach( $array as $key => $element ) {
261: $newKey = preg_replace_callback( '/_(\w)/', function( &$matches ){
262: return strtoupper( $matches[1] );
263: }, $key);
264:
265: if ( $dolphinMode ) {
266: $newKey = preg_replace( '/(\w)Id$/', '$1ID', $newKey );
267: }
268:
269: $newArray[$newKey] = ( is_array($element) ) ? $this->camelfy( $element, $dolphinMode ) : $element;
270: }
271: return $newArray;
272: }
273:
274: /**
275: * For better performance you can pass the tables in an array to this method.
276: * If the tables are available the duplication manager will not query them so
277: * this might be beneficial for performance.
278: *
279: * This method allows two array formats:
280: *
281: * <code>
282: * array( TABLE1, TABLE2 ... )
283: * </code>
284: *
285: * or
286: *
287: * <code>
288: * array( TABLE1 => array( COLUMN1, COLUMN2 ... ) ... )
289: * </code>
290: *
291: * @param array $tables a table cache array
292: *
293: * @return void
294: */
295: public function setTables( $tables )
296: {
297: foreach ( $tables as $key => $value ) {
298: if ( is_numeric( $key ) ) {
299: $this->tables[] = $value;
300: } else {
301: $this->tables[] = $key;
302: $this->columns[$key] = $value;
303: }
304: }
305:
306: $this->cacheTables = TRUE;
307: }
308:
309: /**
310: * Returns a schema array for cache.
311: * You can use the return value of this method as a cache,
312: * store it in RAM or on disk and pass it to setTables later.
313: *
314: * @return array
315: */
316: public function getSchema()
317: {
318: return $this->columns;
319: }
320:
321: /**
322: * Indicates whether you want the duplication manager to cache the database schema.
323: * If this flag is set to TRUE the duplication manager will query the database schema
324: * only once. Otherwise the duplicationmanager will, by default, query the schema
325: * every time a duplication action is performed (dup()).
326: *
327: * @param boolean $yesNo TRUE to use caching, FALSE otherwise
328: */
329: public function setCacheTables( $yesNo )
330: {
331: $this->cacheTables = $yesNo;
332: }
333:
334: /**
335: * A filter array is an array with table names.
336: * By setting a table filter you can make the duplication manager only take into account
337: * certain bean types. Other bean types will be ignored when exporting or making a
338: * deep copy. If no filters are set all types will be taking into account, this is
339: * the default behavior.
340: *
341: * @param array $filters list of tables to be filtered
342: *
343: * @return void
344: */
345: public function setFilters( $filters )
346: {
347: if ( !is_array( $filters ) ) {
348: $filters = array( $filters );
349: }
350:
351: $this->filters = $filters;
352: }
353:
354: /**
355: * Makes a copy of a bean. This method makes a deep copy
356: * of the bean.The copy will have the following features.
357: * - All beans in own-lists will be duplicated as well
358: * - All references to shared beans will be copied but not the shared beans themselves
359: * - All references to parent objects (_id fields) will be copied but not the parents themselves
360: * In most cases this is the desired scenario for copying beans.
361: * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found
362: * (i.e. one that already has been processed) the ID of the bean will be returned.
363: * This should not happen though.
364: *
365: * Note:
366: * This function does a reflectional database query so it may be slow.
367: *
368: * Note:
369: * this function actually passes the arguments to a protected function called
370: * duplicate() that does all the work. This method takes care of creating a clone
371: * of the bean to avoid the bean getting tainted (triggering saving when storing it).
372: *
373: * @param OODBBean $bean bean to be copied
374: * @param array $trail for internal usage, pass array()
375: * @param boolean $preserveIDs for internal usage
376: *
377: * @return OODBBean
378: */
379: public function dup( OODBBean $bean, $trail = array(), $preserveIDs = FALSE )
380: {
381: if ( !count( $this->tables ) ) {
382: $this->tables = $this->toolbox->getWriter()->getTables();
383: }
384:
385: if ( !count( $this->columns ) ) {
386: foreach ( $this->tables as $table ) {
387: $this->columns[$table] = $this->toolbox->getWriter()->getColumns( $table );
388: }
389: }
390:
391: $rs = $this->duplicate( ( clone $bean ), $trail, $preserveIDs );
392:
393: if ( !$this->cacheTables ) {
394: $this->tables = array();
395: $this->columns = array();
396: }
397:
398: return $rs;
399: }
400:
401: /**
402: * Exports a collection of beans recursively.
403: * This method will export an array of beans in the first argument to a
404: * set of arrays. This can be used to send JSON or XML representations
405: * of bean hierarchies to the client.
406: *
407: * For every bean in the array this method will export:
408: *
409: * - contents of the bean
410: * - all own bean lists (recursively)
411: * - all shared beans (but not THEIR own lists)
412: *
413: * If the second parameter is set to TRUE the parents of the beans in the
414: * array will be exported as well (but not THEIR parents).
415: *
416: * The third parameter can be used to provide a white-list array
417: * for filtering. This is an array of strings representing type names,
418: * only the type names in the filter list will be exported.
419: *
420: * The fourth parameter can be used to change the keys of the resulting
421: * export arrays. The default mode is 'snake case' but this leaves the
422: * keys as-is, because 'snake' is the default case style used by
423: * RedBeanPHP in the database. You can set this to 'camel' for
424: * camel cased keys or 'dolphin' (same as camelcase but id will be
425: * converted to ID instead of Id).
426: *
427: * @param array|OODBBean $beans beans to be exported
428: * @param boolean $parents also export parents
429: * @param array $filters only these types (whitelist)
430: * @param string $caseStyle case style identifier
431: *
432: * @return array
433: */
434: public function exportAll( $beans, $parents = FALSE, $filters = array(), $caseStyle = 'snake')
435: {
436: $array = array();
437:
438: if ( !is_array( $beans ) ) {
439: $beans = array( $beans );
440: }
441:
442: foreach ( $beans as $bean ) {
443: $this->setFilters( $filters );
444:
445: $duplicate = $this->dup( $bean, array(), TRUE );
446:
447: $array[] = $duplicate->export( FALSE, $parents, FALSE, $filters );
448: }
449:
450: if ( $caseStyle === 'camel' ) $array = $this->camelfy( $array );
451: if ( $caseStyle === 'dolphin' ) $array = $this->camelfy( $array, true );
452:
453: return $array;
454: }
455: }
456: