http://phing.info/

Source Code Coverage

Designed for use with PHPUnit2, Xdebug and Phing.

Methods: 26 LOC: 688 Statements: 206

Source file Statements Methods Total coverage
Abstract.php 94.7% 92.3% 94.4%
   
1
<?php
2
/**
3
 * Xyster Framework
4
 *
5
 * This source file is subject to the new BSD license that is bundled
6
 * with this package in the file LICENSE.txt.
7
 * It is also available through the world-wide-web at this URL:
8
 * http://www.opensource.org/licenses/bsd-license.php
9
 *
10
 * @category  Xyster
11
 * @package   Xyster_Orm
12
 * @copyright Copyright (c) 2007-2008 Irrational Logic (http://irrationallogic.net)
13
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
14
 * @version   $Id: Abstract.php 200 2008-01-20 15:43:42Z doublecompile $
15
 */
16
/**
17
 * @see Xyster_Orm_Loader
18
 */
19 1
require_once 'Xyster/Orm/Loader.php';
20
/**
21
 * @see Xyster_Orm_Mapper_Interface
22
 */
23 1
require_once 'Xyster/Orm/Mapper/Interface.php';
24
/**
25
 * @see Xyster_Orm_Entity
26
 */
27 1
require_once 'Xyster/Orm/Entity.php';
28
/**
29
 * @see Xyster_Orm_Entity_Meta
30
 */
31 1
require_once 'Xyster/Orm/Entity/Meta.php';
32
/**
33
 * Zend_Filter
34
 */
35 1
require_once 'Zend/Filter.php';
36
/**
37
 * An abstract implementation of the mapper interface
38
 *
39
 * This class allows for a more simple implementation of the mapper interface,
40
 * taking care of common logic.
41
 *
42
 * @category  Xyster
43
 * @package   Xyster_Orm
44
 * @copyright Copyright (c) 2007-2008 Irrational Logic (http://irrationallogic.net)
45
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
46
 */
47
abstract class Xyster_Orm_Mapper_Abstract implements Xyster_Orm_Mapper_Interface
48
{
49
    /**
50
     * The domain associated with the entity
51
     *
52
     * @var string
53
     */
54
    protected $_domain = "";
55
56
    /**
57
     * The factory that created this mapper
58
     *
59
     * @var Xyster_Orm_Mapper_Factory_Interface
60
     */
61
    protected $_factory;
62
63
    /**
64
     * An array of properties used to index the entity by value
65
     *
66
     * The array consists of index names as keys and arrays of the columns
67
     * contained within as values.
68
     *
69
     * <code>array(
70
     *     'name_index' => array( 'name' ),
71
     *  'multi_index' => array( 'transactionNumber', 'transactionDate' )
72
     * );</code>
73
     *
74
     * @var array
75
     */
76
    protected $_index = array();
77
78
    /**
79
     * The period of time entities should persist in the secondary cache
80
     *
81
     * A value of -1 means the entity shouldn't be added to secondary cache. A
82
     * value of 0 means the entity should be stored indefinitely.
83
     *
84
     * @var int
85
     */
86
    protected $_lifetime = 60;
87
88
    /**
89
     * The class meta data
90
     *
91
     * @var Xyster_Orm_Entity_Meta
92
     */
93
    private $_meta;
94
95
    /**
96
     * Any additional options
97
     *
98
     * <dl>
99
     * <dt>metadataCache</dt><dd>The name of the Zend_Registry key to find a
100
     * Zend_Cache_Core object for caching metadata information.  If not
101
     * specified, the mapper will use the defaultMetadataCache.</dd>
102
     * <dt>doNotRefreshAfterSave</dt><dd>This will cause the mapper not to
103
     * refresh the entity after it's inserted or updated.</dd>
104
     * <dt>locking</dt><dd>The name of the field which holds an integer
105
     * version number of the record (used to avoid concurrent changes)</dd>
106
     * </dl>
107
     *
108
     * @var array
109
     */
110
    protected $_options = array();
111
112
    /**
113
     * The name of the table, defaults to entity name
114
     *
115
     * @var string
116
     */
117
    protected $_table = "";
118
119
    /**
120
     * Creates a new mapper
121
     *
122
     * Class authors can overwrite this, but <em>be sure to call the parent</em>
123
     *
124
     * @param Xyster_Orm_Mapper_Factory_Interface $factory
125
     */
126
    public function __construct( Xyster_Orm_Mapper_Factory_Interface $factory )
127
    {
128 286
        $this->_factory = $factory;
129
130 286
        $this->getEntityMeta(); // to assign the meta data to the entity class
131 286
        $this->getSet(); // to make sure the class is defined
132
    }
133
134
    /**
135
     * Allows for subclassing without overwriting constructor
136
     *
137
     * The mapper factory calls this method.  This is necessary because the init
138
     * method should contain the setup of relations, which might depend on the
139
     * mapper that's still being instantiated in the factory.
140
     */
141
    public function init()
142
    {
143
    }
144
145
    /**
146
     * Deletes an entity
147
     *
148
     * @param Xyster_Orm_Entity $entity The entity to delete
149
     */
150
    public function delete( Xyster_Orm_Entity $entity )
151
    {
152 13
        $this->_assertThisEntityName($entity);
153
154 13
        $manager = $this->_factory->getManager();
155 13
        $broker = $manager->getPluginBroker();
156 13
        $broker->preDelete($entity);
157 13
        $this->_delete($entity->getPrimaryKeyAsCriterion());
158 13
        $broker->postDelete($entity);
159
160 13
        $relations = $this->getEntityMeta()->getRelations();
161 13
        foreach( $relations as $relation ) { /* @var $relation Xyster_Orm_Relation */
162 12
            if ( $relation->getType() == 'many' ) {
163 5
                $onDelete = $relation->getOnDelete();
164 5
                $map = $this->_factory->get($relation->getTo());
165 5
                $name = $relation->getName();
166 5
                $reverseName = $relation->hasBelongsTo() ?
167 5
                    $relation->getReverse()->getName() : null;
168 5
                $related = $entity->$name; /* @var $related Xyster_Orm_Set */
169
170
                // just remove the association
171 5
                if ( $onDelete == Xyster_Orm_Relation::ACTION_SET_NULL && $reverseName ) {
172 4
                    foreach( $related as $relatedEntity ) {
173 4
                        $relatedEntity->$reverseName = null;
174 4
                        $map->save($relatedEntity);
175 4
                    }
176
                // rely on the database to cascade the delete
177 5
                } else if ( $onDelete == Xyster_Orm_Relation::ACTION_CASCADE ) {
178 1
                    $manager->getRepository()->removeAll($related);
179
                // we have to delete every last one ourselves
180 5
                } else if ( $onDelete == Xyster_Orm_Relation::ACTION_REMOVE ) {
181 1
                    $manager->getRepository()->removeAll($related);
182 1
                    foreach( $related as $relatedEntity ) {
183 1
                        $map->delete($relatedEntity);
184 1
                    }
185 1
                }
186 5
            }
187 12
        }
188
    }
189
190
    /**
191
     * Gets an entity with the supplied identifier
192
     *
193
     * @param mixed $id  The id of the entity to get
194
     * @return Xyster_Orm_Entity  The data entity found, or null if none
195
     */
196
    final public function get( $id )
197
    {
198 65
        $keyNames = $this->getEntityMeta()->getPrimary();
199 65
        $keyValues = array();
200
201 65
	    if ( count($keyNames) > 1 ) {
202
203 5
    	    $this->_checkPrimaryKey($id);
204 3
    	    $keyValues = $id;
205
206 63
	    } else if ( is_array($id) ) {
207
208 25
	        $keyValues = array( current($keyNames) => current($id) );
209
210 25
	    } else {
211
212 35
	        $keyValues = array( $keyNames[0] => $id );
213
214
	    }
215
216 63
	    return $this->find($keyValues);
217
    }
218
219
    /**
220
     * Gets the name of the domain to which this mapper belongs
221
     *
222
     * @return string  The domain
223
     */
224
    final public function getDomain()
225
    {
226 105
        return $this->_domain;
227
    }
228
229
    /**
230
     * Gets the entity metadata
231
     *
232
     * @return Xyster_Orm_Entity_Meta
233
     */
234
    final public function getEntityMeta()
235
    {
236 286
        if ( !$this->_meta ) {
237 286
            $this->_meta = new Xyster_Orm_Entity_Meta($this);
238 286
            Xyster_Orm_Entity::setMeta($this->_meta);
239 286
        }
240 286
        return $this->_meta;
241
    }
242
243
    /**
244
     * Gets the class name of the entity
245
     *
246
     * Class authors should overwrite this method if their entity name isn't
247
     * the same as the mapper name.
248
     *
249
     * @return string  The class name of the entity
250
     */
251
    public function getEntityName()
252
    {
253 286
        return substr(get_class($this),0,-6);
254
    }
255
256
    /**
257
     * Gets the factory that created this mapper
258
     *
259
     * @return Xyster_Orm_Mapper_Factory_Interface
260
     */
261
    public function getFactory()
262
    {
263 286
        return $this->_factory;
264
    }
265
266
    /**
267
     * Gets the columns that should be used to index the entity
268
     *
269
     * The array consists of index names as keys and arrays of the columns
270
     * contained within as values.
271
     *
272
     * @return array
273
     */
274
    final public function getIndex()
275
    {
276 80
        return $this->_index;
277
    }
278
279
	/**
280
	 * Gets the time in seconds an entity should be cached
281
	 *
282
	 * @return int
283
	 */
284
	final public function getLifetime()
285
	{
286 46
		return $this->_lifetime;
287
	}
288
289
    /**
290
     * Gets the value of an option
291
     *
292
     * @param string $name The name of the option
293
     * @return mixed The option value
294
     */
295
    final public function getOption( $name )
296
    {
297 39
        return array_key_exists($name, $this->_options) ?
298 39
            $this->_options[$name] : null;
299
    }
300
301
    /**
302
     * Gets the options for this mapper
303
     *
304
     * @return array
305
     */
306
    final public function getOptions()
307
    {
308 1
        return $this->_options;
309
    }
310
311
    /**
312
     * Gets an empty entity set for the mapper's entity type
313
     *
314
     * @return Xyster_Orm_Set An empty set
315
     */
316
    public function getSet( Xyster_Collection_Interface $entities = null )
317
    {
318 286
        $set = Xyster_Orm_Loader::loadSetClass($this->getEntityName());
319
320 286
        return new $set($entities);
321
    }
322
323
    /**
324
     * Gets the table from which an entity comes
325
     *
326
     * It is up to the Xyster_Orm_Backend to do something with this value.
327
     *
328
     * @return string The table name
329
     */
330
    public function getTable()
331
    {
332 91
        if ( !$this->_table ) {
333 1
            $this->_table = strtolower(Zend_Filter::get($this->getEntityName(),
334 1
                'Word_CamelCaseToUnderscore'));
335 1
        }
336 91
        return $this->_table;
337
    }
338
339
    /**
340
     * Saves an entity (insert or update)
341
     *
342
     * @param Xyster_Orm_Entity $entity  The entity to save
343
     */
344
    public function save( Xyster_Orm_Entity $entity )
345
    {
346 38
        $this->_assertThisEntityName($entity);
347
348
        /*
349
		 * Step 1: Sets ids for any single-entity relationships
350
		 */
351 38
		foreach( $this->getEntityMeta()->getRelations() as $k=>$v ) {
352
		    /* @var $v Xyster_Orm_Relation */
353 32
			if ( !$v->isCollection() && $entity->isLoaded($k) && $entity->$k !== null ) {
354 10
				$linked = $entity->$k; /* @var $linked Xyster_Orm_Entity */
355
				// get the original primary key, in case it's not auto-generated
356 10
				$key = $linked->getPrimaryKey(true);
357 10
				if ( !$linked->getBase() ) {
358 5
					$this->_factory->get($v->getTo())->save($linked);
359 5
					$key = $linked->getPrimaryKey();
360 10
				} else if ( $key != $linked->getPrimaryKey() ) {
361 1
				    $key = $linked->getPrimaryKey();
362 1
				}
363 10
				$keyNames = array_keys($key);
364 10
				$foreignKey = $v->getId();
365 10
				for( $i=0; $i<count($key); $i++ ) {
366 10
				    $field = $foreignKey[$i];
367 10
				    $keyName = $keyNames[$i];
368 10
				    $entity->$field = $linked->$keyName;
369 10
				}
370 10
			}
371 32
		}
372
373 38
		$broker = $this->_factory->getManager()->getPluginBroker();
374 38
		$updatedKey = false;
375
		/*
376
		 * Step 2: Save actual entity
377
		 */
378 38
        if ( !$entity->getBase() ) {
379 16
            $broker->preInsert($entity);
380 16
            $this->_insert($entity);
381 16
            $broker->postInsert($entity);
382
383 16
        } else {
384 29
            $updatedKey = $entity->getPrimaryKey() != $entity->getPrimaryKey(true);
385 29
            $broker->preUpdate($entity);
386 29
            $this->_update($entity);
387 29
            $broker->postUpdate($entity);
388
        }
389
390
        // this is in case any triggers in the db, etc. have changed the record
391 38
        if ( !$this->getOption('doNotRefreshAfterSave') ) {
392 36
    	   $this->refresh($entity);
393 36
        }
394 38
    	$entity->setDirty(false);
395
396
        /*
397
		 * Step 3: work with many and joined relationships
398
		 */
399 38
		foreach( $this->getEntityMeta()->getRelations() as $k=>$relation ) {
400
		    /* @var $relation Xyster_Orm_Relation */
401 32
            if ( $relation->isCollection() && ( $updatedKey
402 32
			    || $entity->isLoaded($k) ) ) {
403
404 18
				$set = $entity->$k;
405 18
				$cascadeUpdate = $updatedKey &&
406 18
				    $relation->getOnUpdate() == Xyster_Orm_Relation::ACTION_CASCADE;
407
408 18
				$added = $set->getDiffAdded();
409 18
				$removed = $set->getDiffRemoved();
410 18
				if ( !$added && !$removed && !$cascadeUpdate ) {
411 4
					continue;
412 0
				}
413
414 14
				if ( $relation->getType() == 'joined' ) {
415
416 12
				    $this->_joinedInsert($set);
417 12
				    $this->_joinedDelete($set);
418
419 14
				} else if ( $relation->getType() == 'many' ) {
420
421 2
    				$map = $this->_factory->get($relation->getTo());
422 2
    				if ( $cascadeUpdate ) {
423
    				    // if we should cascade changed primary keys
424 1
    				    foreach( $set as $setEntity ) {
425 1
    				        $map->save($setEntity);
426 1
    				    }
427 1
       				} else {
428
    				    // no cascade, just save newly added to set
429 1
    				    array_walk($added, array($map, 'save'));
430
    				}
431 2
    				array_walk($removed, array($map, 'delete'));
432
433 2
				}
434
435 14
				$set->baseline();
436 14
			}
437 32
		}
438
    }
439
440
    /**
441
     *
442
     * @param string $field
443
     * @return string
444
     */
445
    public function translateField( $field )
446
    {
447 286
        $field = Zend_Filter::get($field, 'Word_UnderscoreToCamelCase');
448 286
        return strtolower($field[0]) . substr($field, 1);
449
    }
450
451
    /**
452
     *
453
     * @param string $field
454
     * @return string
455
     */
456
    public function untranslateField( $field )
457
    {
458 267
        return strtolower(Zend_Filter::get($field, 'Word_CamelCaseToUnderscore'));
459
    }
460
461
    /**
462
     * Removes entities from the backend
463
     *
464
     * @param Xyster_Data_Criterion $where  The criteria on which to remove entities
465
     * @return int The number of rows deleted
466
     */
467
    abstract protected function _delete( Xyster_Data_Criterion $where );
468
469
    /**
470
     * Saves a new entity into the backend
471
     *
472
     * @param Xyster_Orm_Entity $entity  The entity to insert
473
     * @return mixed  The new primary key
474
     */
475
    abstract protected function _insert( Xyster_Orm_Entity $entity );
476
477
    /**
478
     * Adds the entity to the many-to-many join
479
     *
480
     * @param Xyster_Orm_Set $set
481
     */
482
    abstract protected function _joinedInsert( Xyster_Orm_Set $set );
483
484
    /**
485
     * Removes the entity from the many-to-many join
486
     *
487
     * @param Xyster_Orm_Set $set
488
     */
489
    abstract protected function _joinedDelete( Xyster_Orm_Set $set );
490
491
    /**
492
     * Updates the values of an entity in the backend
493
     *
494
     * Class authors must remember to implement optimistic offline locking in
495
     * this method.  See the {@link Xyster_Orm_Mapper::_update} method for an
496
     * example.
497
     *
498
     * @param Xyster_Orm_Entity $entity The entity to update
499
     * @throws Xyster_Orm_Mapper_Exception if the record was modified or deleted
500
     */
501
    abstract protected function _update( Xyster_Orm_Entity $entity );
502
503
    /**
504
     * A convenience method to assert entity type
505
     *
506
     * @param Xyster_Orm_Entity $entity
507
     * @throws Xyster_Orm_Mapper_Exception if the entity supplied is of the wrong type
508
     */
509
    final protected function _assertThisEntityName( Xyster_Orm_Entity $entity )
510
    {
511 44
        $name = $this->getEntityName();
512 44
        if ( ! $entity instanceof $name ) {
513 0
            require_once 'Xyster/Orm/Mapper/Exception.php';
514 0
            throw new Xyster_Orm_Mapper_Exception('This mapper only accepts entities of type ' . $name);
515 0
        }
516
    }
517
518
    /**
519
     * Convenience method to create a 'belongs' relationship
520
     *
521
     * @see Xyster_Orm_Entity_Meta::belongsTo
522
     * @param string $name The name of the relationship
523
     * @param array $options An array of options
524
     * @return Xyster_Orm_Mapper_Abstract provides a fluent interface
525
     */
526
    final protected function _belongsTo( $name, array $options = array() )
527
    {
528 266
        $this->getEntityMeta()->belongsTo($name, $options);
529 266
        return $this;
530
    }
531
532
    /**
533
     * Ensures the parameter passed is a Criterion
534
     *
535
     * @param Xyster_Data_Criterion|array $criteria
536
     * @return Xyster_Data_Criterion
537
     */
538
    protected function _buildCriteria( $criteria )
539
    {
540 87
        if ( !is_array($criteria) &&
541 16
            ! $criteria instanceof Xyster_Data_Criterion &&
542 87
            $criteria !== null ) {
543 2
            require_once 'Xyster/Orm/Mapper/Exception.php';
544 2
            throw new Xyster_Orm_Mapper_Exception('Invalid criteria: ' . gettype($criteria));
545 0
        }
546
547 85
        $_criteria = null;
548
549 85
        if ( is_array($criteria) ) {
550 84
            $this->_checkPropertyNames($criteria);
551 83
            foreach( $criteria as $name => $value ) {
552 83
                require_once 'Xyster/Data/Expression.php';
553 83
                $thiskey = Xyster_Data_Expression::eq($name,$value);
554 83
                if ( !$_criteria ) {
555 83
                    $_criteria = $thiskey;
556 83
                } else if ( $_criteria instanceof Xyster_Data_Expression ) {
557 4
                    require_once 'Xyster/Data/Junction.php';
558 4
                    $_criteria = Xyster_Data_Junction::all( $_criteria, $thiskey );
559 4
                } else if ( $_criteria instanceof Xyster_Data_Junction ) {
560 4
                    $_criteria->add($thiskey);
561 4
                }
562 83
            }
563 83
        } else {
564 14
            $_criteria = $criteria;
565
        }
566
567 84
        return $_criteria;
568
    }
569
570
    /**
571
     * Checks an array for correct primary key names
572
     *
573
     * @param mixed $key
574
     */
575
    protected function _checkPrimaryKey( $id )
576
    {
577 23
        $keyNames = $this->getEntityMeta()->getPrimary();
578
579 23
        if ( (!is_array($id) && count($keyNames) > 1) || count($id) != count($keyNames)) {
580 1
            require_once 'Xyster/Orm/Mapper/Exception.php';
581 1
            throw new Xyster_Orm_Mapper_Exception("Missing value(s) for the primary key");
582 0
        }
583
584 22
        if ( !is_array($id) ) {
585 14
            $id = array( $keyNames[0] => $id );
586 14
        }
587
588 22
        foreach( array_keys($id) as $name ) {
589 22
            if ( !in_array($name, $keyNames) ) {
590 1
                require_once 'Xyster/Orm/Mapper/Exception.php';
591 1
                throw new Xyster_Orm_Mapper_Exception("'$name' is not a primary key");
592 0
            }
593 22
        }
594
595 21
        return $id;
596
    }
597
598
    /**
599
     * Asserts the correct property names in a criteria array
600
     *
601
     * @param array $criteria
602
     * @throws Xyster_Orm_Mapper_Exception if one of the field names is invalid
603
     */
604
    protected function _checkPropertyNames( array $criteria )
605
    {
606
        // get the array of Xyster_Orm_Entity_Field objects
607 93
        $fields = $this->getEntityMeta()->getFieldNames();
608
609 93
        foreach( $criteria as $k => $v ) {
610 93
            if ( !in_array($k, $fields) ) {
611 1
                require_once 'Xyster/Orm/Mapper/Exception.php';
612 1
                throw new Xyster_Orm_Mapper_Exception("'" . $k
613 1
                    . "' is not a valid field for "
614 1
                    . $this->getEntityName() );
615 0
            }
616 92
        }
617
    }
618
619
    /**
620
     * Creates an entity from the row supplied
621
     *
622
     * If the row has already been loaded and the entity that represents the row
623
     * is in the repository, this method will return that exact instance instead
624
     * of creating a new one.
625
     *
626
     * @param array $row
627
     * @return Xyster_Orm_Entity  The entity created
628
     */
629
    protected function _create( $row )
630
    {
631 103
        $entityName = $this->getEntityName();
632
        // this class should already be loaded by the class' mapper
633
634 103
        $manager = $this->_factory->getManager();
635 103
        $primary = array_intersect_key($row,
636 103
            array_flip($this->getEntityMeta()->getPrimary()));
637 103
        $loaded = $manager->getFromCache($entityName, $primary);
638 103
        if ( $loaded instanceof Xyster_Orm_Entity ) {
639 37
            return $loaded;
640 0
        }
641
642 103
        $entity = new $entityName($row);
643 103
        $manager->getPluginBroker()->postLoad($entity);
644 103
        return $entity;
645
    }
646
647
    /**
648
     * Convenience method to create a 'many to many' relationship
649
     *
650
     * @see Xyster_Orm_Entity_Meta::hasJoined
651
     * @param string $name The name of the relationship
652
     * @param array $options An array of options
653
     * @return Xyster_Orm_Mapper_Abstract provides a fluent interface
654
     */
655
    final protected function _hasJoined( $name, array $options = array() )
656
    {
657 266
        $this->getEntityMeta()->hasJoined($name, $options);
658 266
        return $this;
659
    }
660
661
    /**
662
     * Convenience method to create a 'one to many' relationship
663
     *
664
     * @see Xyster_Orm_Entity_Meta::hasMany
665
     * @param string $name The name of the relationship
666
     * @param array $options An array of options
667
     * @return Xyster_Orm_Mapper_Abstract provides a fluent interface
668
     */
669
    final protected function _hasMany( $name, array $options = array() )
670
    {
671 52
        $this->getEntityMeta()->hasMany($name, $options);
672 52
        return $this;
673
    }
674
675
    /**
676
     * Convenience method to create a 'one to one' relationship
677
     *
678
     * @see Xyster_Orm_Entity_Meta::hasOne
679
     * @param string $name The name of the relationship
680
     * @param array $options An array of options
681
     * @return Xyster_Orm_Mapper_Abstract provides a fluent interface
682
     */
683
    final protected function _hasOne( $name, array $options = array() )
684
    {
685 0
        $this->getEntityMeta()->hasOne($name, $options);
686 0
        return $this;
687
    }
688
}


Report generated at 2008-03-05T18:27:43-05:00