PageRenderTime 48ms CodeModel.GetById 15ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 1ms

/solar/source/solar/Solar/Sql/Model.php

https://bitbucket.org/Sanakan/noise
PHP | 2664 lines | 1023 code | 266 blank | 1375 comment | 145 complexity | ba94ff6e5dd45cce2a55ffdade4dcc48 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/**
   3 * 
   4 * An SQL-centric Model class based on TableDataGateway, using Collection and
   5 * Record objects for returns, with integrated caching of versioned result
   6 * data.
   7 * 
   8 * @category Solar
   9 * 
  10 * @package Solar_Sql_Model An SQL-oriented ORM system using TableDataGateway 
  11 * and DataMapper patterns.
  12 * 
  13 * @author Paul M. Jones <pmjones@solarphp.com>
  14 * 
  15 * @author Jeff Moore <jeff@procata.com>
  16 * 
  17 * @license http://opensource.org/licenses/bsd-license.php BSD
  18 * 
  19 * @version $Id: Model.php 4694 2010-09-06 14:33:18Z pmjones $
  20 * 
  21 */
  22abstract class Solar_Sql_Model extends Solar_Base
  23{
  24    /**
  25     * 
  26     * Default configuration values.
  27     * 
  28     * @config dependency sql A Solar_Sql dependency.
  29     * 
  30     * @config dependency cache A Solar_Cache dependency for the 
  31     * Solar_Sql_Model_Cache object.
  32     * 
  33     * @config dependency catalog A Solar_Sql_Model_Catalog to find other 
  34     * models with.
  35     * 
  36     * @config bool table_scan Connect to the database and scan the table for 
  37     * its column descriptions, creating the table and indexes if not already 
  38     * present.
  39     * 
  40     * @config bool auto_cache Automatically maintain the data cache.
  41     * 
  42     * @var array
  43     * 
  44     */
  45    protected $_Solar_Sql_Model = array(
  46        'catalog' => 'model_catalog',
  47        'sql'   => 'sql',
  48        'cache' => array(
  49            'adapter' => 'Solar_Cache_Adapter_None',
  50        ),
  51        'table_scan' => true,
  52        'auto_cache' => false,
  53    );
  54    
  55    /**
  56     * 
  57     * The number of rows affected by the last INSERT, UPDATE, or DELETE.
  58     * 
  59     * @var int
  60     * 
  61     * @see getAffectedRows()
  62     * 
  63     */
  64    protected $_affected_rows;
  65    
  66    /**
  67     * 
  68     * A Solar_Sql_Model_Catalog dependency object.
  69     * 
  70     * @var Solar_Sql_Model_Catalog
  71     * 
  72     */
  73    protected $_catalog = null;
  74    
  75    /**
  76     * 
  77     * A Solar_Sql dependency object.
  78     * 
  79     * @var Solar_Sql_Adapter
  80     * 
  81     */
  82    protected $_sql = null;
  83    
  84    /**
  85     * 
  86     * A Solar_Sql_Model_Cache object.
  87     * 
  88     * @var Solar_Sql_Model_Cache
  89     * 
  90     */
  91    protected $_cache = null;
  92    
  93    /**
  94     * 
  95     * The model name is the short form of the class name; this is generally
  96     * a plural.
  97     * 
  98     * When inheritance is enabled, the default is the $_inherit_name value,
  99     * otherwise, the default is the $_table_name.
 100     * 
 101     * @var string
 102     * 
 103     */
 104    protected $_model_name;
 105    
 106    /**
 107     * 
 108     * When a record from this model is part of an form element array, use
 109     * this name as the array key for it; by default, this is the singular
 110     * of the model name.
 111     * 
 112     * @var string
 113     * 
 114     */
 115    protected $_array_name;
 116    
 117    // -----------------------------------------------------------------
 118    //
 119    // Classes
 120    //
 121    // -----------------------------------------------------------------
 122    
 123    /**
 124     * 
 125     * A Solar_Class_Stack object for fallback hierarchy.
 126     * 
 127     * @var Solar_Class_Stack
 128     * 
 129     */
 130    protected $_stack;
 131    
 132    /**
 133     * 
 134     * The results of get_class($this) so we don't call get_class() all the 
 135     * time.
 136     * 
 137     * @var string
 138     * 
 139     */
 140    protected $_class;
 141    
 142    /**
 143     * 
 144     * The final fallback class for an individual record.
 145     * 
 146     * Default is Solar_Sql_Model_Record.
 147     * 
 148     * @var string
 149     * 
 150     */
 151    protected $_record_class = 'Solar_Sql_Model_Record';
 152    
 153    /**
 154     * 
 155     * A blank instance of the Record class for this model.
 156     * 
 157     * We keep this so we don't keep looking for a record class once we know
 158     * what the proper class is.  Not used when inheritance is in effect.
 159     * 
 160     * @var Solar_Sql_Model_Record
 161     * 
 162     */
 163    protected $_record_prototype;
 164    
 165    /**
 166     * 
 167     * The final fallback class for collections of records.
 168     * 
 169     * Default is Solar_Sql_Model_Collection.
 170     * 
 171     * @var string
 172     * 
 173     */
 174    protected $_collection_class = 'Solar_Sql_Model_Collection';
 175    
 176    /**
 177     * 
 178     * A blank instance of the Collection class for this model.
 179     * 
 180     * We keep this so we don't keep looking for a collection class once we
 181     * know what the proper class is.
 182     * 
 183     * @var Solar_Sql_Model_Record
 184     * 
 185     */
 186    protected $_collection_prototype;
 187    
 188    /**
 189     * 
 190     * The class to use for building SELECT statements.
 191     * 
 192     * @var string
 193     * 
 194     */
 195    protected $_select_class = 'Solar_Sql_Select';
 196    
 197    /**
 198     * 
 199     * The class to use for filter chains.
 200     * 
 201     * @var string
 202     * 
 203     */
 204    protected $_filter_class = null;
 205    
 206    /**
 207     * 
 208     * The class to use for the cache object.
 209     * 
 210     * @var string
 211     * 
 212     * @see $_cache
 213     * 
 214     */
 215    protected $_cache_class = 'Solar_Sql_Model_Cache';
 216    
 217    // -----------------------------------------------------------------
 218    //
 219    // Table and index definition
 220    //
 221    // -----------------------------------------------------------------
 222    
 223    /**
 224     * 
 225     * The table name.
 226     * 
 227     * @var string
 228     * 
 229     */
 230    protected $_table_name = null;
 231    
 232    /**
 233     * 
 234     * The column specification array for all columns in this table.
 235     * 
 236     * Used in auto-creation, and for sync-checks.
 237     * 
 238     * Will be overridden by _fixTableCols() when it reads the table info, so 
 239     * you don't *have* to enter anything here ... but if it's empty, you 
 240     * won't get auto-creation.
 241     * 
 242     * Each element in this array looks like this...
 243     * 
 244     * {{code: php
 245     *     $_table_cols = array(
 246     *         'col_name' => array(
 247     *             'name'    => (string) the col_name, same as the key
 248     *             'type'    => (string) char, varchar, date, etc
 249     *             'size'    => (int) column size
 250     *             'scope'   => (int) decimal places
 251     *             'default' => (string) default value
 252     *             'require' => (bool) is this a required (non-null) column?
 253     *             'primary' => (bool) is this part of the primary key?
 254     *             'autoinc' => (bool) auto-incremented?
 255     *          ),
 256     *     );
 257     * }}
 258     * 
 259     * @var array
 260     * 
 261     */
 262    protected $_table_cols = array();
 263    
 264    /**
 265     * 
 266     * The index specification array for all indexes on this table.
 267     * 
 268     * Used only in auto-creation.
 269     * 
 270     * The array should be in this format ...
 271     * 
 272     * {{code: php
 273     *     // the index type: 'normal' or 'unique'
 274     *     $type = 'normal';
 275     * 
 276     *     // index on a single column:
 277     *     // CREATE INDEX idx_name ON table_name (col_name)
 278     *     $this->_index_info['idx_name'] = array(
 279     *         'type' => $type,
 280     *         'cols' => 'col_name'
 281     *     );
 282     * 
 283     *     // index on multiple columns:
 284     *     // CREATE INDEX idx_name ON table_name (col_1, col_2, ... col_N)
 285     *     $this->_index_info['idx_name'] = array(
 286     *         'type' => $type,
 287     *         'cols' => array('col_1', 'col_2', ..., 'col_N')
 288     *     );
 289     * 
 290     *     // easy shorthand for an index on a single column,
 291     *     // giving the index the same name as the column:
 292     *     // CREATE INDEX col_name ON table_name (col_name)
 293     *     $this->_index_info['col_name'] = $type;
 294     * }}
 295     * 
 296     * The $type may be 'normal' or 'unique'.
 297     * 
 298     * @var array
 299     * 
 300     */
 301    protected $_index_info = array();
 302    
 303    // -----------------------------------------------------------------
 304    //
 305    // Special columns and column behaviors
 306    //
 307    // -----------------------------------------------------------------
 308    
 309    /**
 310     * 
 311     * A list of column names that don't exist in the table, but should be
 312     * calculated by the model as-needed.
 313     * 
 314     * @var array
 315     * 
 316     */
 317    protected $_calculate_cols = array();
 318    
 319    /**
 320     * 
 321     * A list of column names that use sequence values.
 322     * 
 323     * When the column is present in a data array, but its value is null,
 324     * a sequence value will automatically be added.
 325     * 
 326     * @var array
 327     * 
 328     */
 329    protected $_sequence_cols = array();
 330    
 331    /**
 332     * 
 333     * A list of column names on which to apply serialize() and unserialize()
 334     * automatically.
 335     * 
 336     * Will be unserialized by the Record class as the values are loaded,
 337     * then re-serialized just before insert/update in the Model class.
 338     * 
 339     * @var array
 340     * 
 341     * @see [[php::serialize() | ]]
 342     * 
 343     * @see [[php::unserialize() | ]]
 344     * 
 345     */
 346    protected $_serialize_cols = array();
 347    
 348    /**
 349     * 
 350     * A list of column names storing XML strings to convert back and forth to
 351     * Solar_Struct_Xml objects.
 352     * 
 353     * @var array
 354     * 
 355     * @see $_xmlstruct_class
 356     * 
 357     * @see Solar_Struct_Xml
 358     * 
 359     */
 360    protected $_xmlstruct_cols = array();
 361    
 362    /**
 363     * 
 364     * The class to use for $_xmlstruct_cols conversion objects.
 365     * 
 366     * @var string
 367     * 
 368     * @var array
 369     * 
 370     * @see $_xmlstruct_cols
 371     * 
 372     */
 373    protected $_xmlstruct_class = 'Solar_Struct_Xml';
 374    
 375    /**
 376     * 
 377     * The column name for the primary key.
 378     * 
 379     * @var string
 380     * 
 381     */
 382    protected $_primary_col = null;
 383    
 384    /**
 385     * 
 386     * The column name for 'created' timestamps; default is 'created'.
 387     * 
 388     * @var string
 389     * 
 390     */
 391    protected $_created_col = 'created';
 392    
 393    /**
 394     * 
 395     * The column name for 'updated' timestamps; default is 'updated'.
 396     * 
 397     * @var string
 398     * 
 399     */
 400    protected $_updated_col = 'updated';
 401    
 402    /**
 403     * 
 404     * Other models that relate to this model should use this as the 
 405     * foreign-key column name.
 406     * 
 407     * @var string
 408     * 
 409     */
 410    protected $_foreign_col = null;
 411    
 412    // -----------------------------------------------------------------
 413    //
 414    // Other/misc
 415    //
 416    // -----------------------------------------------------------------
 417    
 418    /**
 419     * 
 420     * Relationships to other Model classes.
 421     * 
 422     * Keyed on a "virtual" column name, which will be used as a property
 423     * name in returned records.
 424     * 
 425     * @var array
 426     * 
 427     */
 428    protected $_related = array();
 429    
 430    /**
 431     * 
 432     * Filters to validate and sanitize column data.
 433     * 
 434     * Default is to use validate*() and sanitize*() methods in the filter
 435     * class, but if the method exists locally, it will be used instead.
 436     * 
 437     * The filters apply only to Record objects from the model; if you use
 438     * the model insert() and update() methods directly, the filters are not
 439     * applied.
 440     * 
 441     * Example usage follows; note that "_validate" and "_sanitize" refer
 442     * to internal (protected) filtering methods that have access to the
 443     * entire data set being filtered.
 444     * 
 445     * {{code: php
 446     *     // filter 'col_1' to have only alpha chars, with a max length of
 447     *     // 32 chars
 448     *     $this->_filters['col_1'][] = 'sanitizeStringAlpha';
 449     *     $this->_filters['col_1'][] = array('validateMaxLength', 32);
 450     * 
 451     *     // filter 'col_2' to have only numeric chars, validate as an
 452     *     // integer, in a range of -10 to +10.
 453     *     $this->_filters['col_2'][] = 'sanitizeNumeric';
 454     *     $this->_filters['col_2'][] = 'validateInteger';
 455     *     $this->_filters['col_2'][] = array('validateRange', -10, +10);
 456     * 
 457     *     // filter 'handle' to have only alpha-numeric chars, with a length
 458     *     // of 6-14 chars, and unique in the table.
 459     *     $this->_filters['handle'][] = 'sanitizeStringAlnum';
 460     *     $this->_filters['handle'][] = array('validateRangeLength', 6, 14);
 461     *     $this->_filters['handle'][] = 'validateUnique';
 462     * 
 463     *     // filter 'email' to have only emails-allowed chars, validate as an
 464     *     // email address, and be unique in the table.
 465     *     $this->_filters['email'][] = 'sanitizeStringEmail';
 466     *     $this->_filters['email'][] = 'validateEmail';
 467     *     $this->_filters['email'][] = 'validateUnique';
 468     * 
 469     *     // filter 'passwd' to be not-blank, and should match any existing
 470     *     // 'passwd_confirm' value.
 471     *     $this->_filters['passwd'][] = 'validateNotBlank';
 472     *     $this->_filters['passwd'][] = 'validateConfirm';
 473     * }}
 474     * 
 475     * @var array
 476     * 
 477     * @see $_filter_class
 478     * 
 479     * @see _addFilter()
 480     * 
 481     */
 482    protected $_filters;
 483    
 484    // -----------------------------------------------------------------
 485    //
 486    // Single-table inheritance
 487    //
 488    // -----------------------------------------------------------------
 489    
 490    /**
 491     * 
 492     * The base model this class is inherited from, in single-table 
 493     * inheritance.
 494     * 
 495     * @var string
 496     * 
 497     */
 498    protected $_inherit_base = null;
 499    
 500    /**
 501     * 
 502     * When inheritance is turned on, the class name value for this class
 503     * in $_inherit_col.
 504     * 
 505     * @var string
 506     * 
 507     */
 508    protected $_inherit_name = false;
 509    
 510    /**
 511     * 
 512     * The column name that tracks single-table inheritance; default is
 513     * 'inherit'.
 514     * 
 515     * @var string
 516     * 
 517     */
 518    protected $_inherit_col = 'inherit';
 519    
 520    // -----------------------------------------------------------------
 521    //
 522    // Select options
 523    //
 524    // -----------------------------------------------------------------
 525    
 526    /**
 527     * 
 528     * Only fetch these columns from the table.
 529     * 
 530     * @var array
 531     * 
 532     */
 533    protected $_fetch_cols;
 534    
 535    /**
 536     * 
 537     * By default, order by this column when fetching rows.
 538     * 
 539     * @var array
 540     * 
 541     */
 542    protected $_order;
 543    
 544    /**
 545     * 
 546     * The default number of rows per page when selecting.
 547     * 
 548     * @var int
 549     * 
 550     */
 551    protected $_paging = 10;
 552    
 553    /**
 554     * 
 555     * The registered Solar_Inflect object.
 556     * 
 557     * @var Solar_Inflect
 558     * 
 559     */
 560    protected $_inflect;
 561    
 562    // -----------------------------------------------------------------
 563    //
 564    // Constructor and magic methods
 565    //
 566    // -----------------------------------------------------------------
 567    
 568    /**
 569     * 
 570     * Post-construction tasks to complete object construction.
 571     * 
 572     * @return void
 573     * 
 574     */
 575    protected function _postConstruct()
 576    {
 577        parent::_postConstruct();
 578        
 579        // Establish the state of this object before _setup
 580        $this->_preSetup();        
 581        
 582        // user-defined setup
 583        $this->_setup();
 584        
 585        // Complete the setup of this model
 586        $this->_postSetup();
 587    }
 588    
 589    /**
 590     * 
 591     * Call this before you unset the instance so that you release the memory
 592     * from all the internal child objects.
 593     * 
 594     * @return void
 595     * 
 596     */
 597    public function free()
 598    {
 599        foreach ($this->_related as $key => $val) {
 600            unset($this->_related[$key]);
 601        }
 602        
 603        unset($this->_cache);
 604    }
 605    
 606    /**
 607     * 
 608     * User-defined setup.
 609     * 
 610     * @return void
 611     * 
 612     */
 613    protected function _setup()
 614    {
 615    }
 616    
 617    // -----------------------------------------------------------------
 618    //
 619    // Getters and setters
 620    //
 621    // -----------------------------------------------------------------
 622    
 623    /**
 624     * 
 625     * Read-only access to protected model properties.
 626     * 
 627     * @param string $key The requested property; e.g., `'foo'` will read from
 628     * `$_foo`.
 629     * 
 630     * @return mixed
 631     * 
 632     */
 633    public function __get($key)
 634    {
 635        $var = "_$key";
 636        if (property_exists($this, $var)) {
 637            return $this->$var;
 638        } else {
 639            throw $this->_exception('ERR_NO_SUCH_PROPERTY', array(
 640                'class' => get_class($this),
 641                'property' => $key,
 642            ));
 643        }
 644    }
 645    
 646    /**
 647     * 
 648     * Gets the number of records per page.
 649     * 
 650     * @return int The number of records per page.
 651     * 
 652     */
 653    public function getPaging()
 654    {
 655        return $this->_paging;
 656    }
 657    
 658    /**
 659     * 
 660     * Sets the number of records per page.
 661     * 
 662     * @param int $paging The number of records per page.
 663     * 
 664     * @return void
 665     * 
 666     */
 667    public function setPaging($paging)
 668    {
 669        $this->_paging = (int) $paging;
 670    }
 671    
 672    /**
 673     * 
 674     * Returns the fully-qualified primary key name.
 675     * 
 676     * @return string
 677     * 
 678     */
 679    public function getPrimary()
 680    {
 681        return "{$this->_model_name}.{$this->_primary_col}";
 682    }
 683    
 684    /**
 685     * 
 686     * Returns the number of rows affected by the last INSERT, UPDATE, or
 687     * DELETE.
 688     * 
 689     * @return int
 690     * 
 691     */
 692    public function getAffectedRows()
 693    {
 694        return $this->_affected_rows;
 695    }
 696    
 697    // -----------------------------------------------------------------
 698    //
 699    // Fetch
 700    //
 701    // -----------------------------------------------------------------
 702    
 703    /**
 704     * 
 705     * Magic call implements "fetchOneBy...()" and "fetchAllBy...()" for
 706     * columns listed in the method name.
 707     * 
 708     * You *have* to specify params for all of the named columns.
 709     * 
 710     * Optionally, you can pass a final array for the "extra" paramters to the
 711     * fetch ('order', 'group', 'having', etc.)
 712     * 
 713     * Example:
 714     * 
 715     * {{code: php
 716     *     // fetches one record by status
 717     *     $model->fetchOneByStatus('draft');
 718     * 
 719     *     // fetches all records by area_id and owner_handle
 720     *     $model->fetchAllByAreaIdAndOwnerHandle($area_id, $owner_handle);
 721     * 
 722     *     // fetches all records by area_id and owner_handle,
 723     *     // with ordering and page-limiting
 724     *     $extra = array('order' => 'area_id DESC', 'page' => 2);
 725     *     $model->fetchAllByAreaIdAndOwnerHandle($area_id, $owner_handle, $extra);
 726     * }}
 727     * 
 728     * @param string $method The virtual method name, composed of "fetchOneBy"
 729     * or "fetchAllBy", with a series of column names joined by "And".
 730     * 
 731     * @param array $params Parameters to pass to the method: one for each
 732     * column, plus an optional one for extra fetch parameters.
 733     * 
 734     * @return mixed
 735     * 
 736     * @todo Expand to cover assoc, col, pairs, and value.
 737     * 
 738     */
 739    public function __call($method, $params)
 740    {
 741        // fetch a record, or a collection?
 742        if (substr($method, 0, 7) == 'fetchBy') {
 743            // fetch a record
 744            $fetch = 'fetchOne';
 745            $method = substr($method, 7);
 746        } elseif (substr($method, 0, 10) == 'fetchOneBy') {
 747            // fetch a record
 748            $fetch = 'fetchOne';
 749            $method = substr($method, 10);
 750        } elseif (substr($method, 0, 10) == 'fetchAllBy') {
 751            // fetch a collection
 752            $fetch = 'fetchAll';
 753            $method = substr($method, 10);
 754        } else {
 755            throw $this->_exception('ERR_METHOD_NOT_IMPLEMENTED', array(
 756                'method' => $method,
 757            ));
 758        }
 759        
 760        // get the list of columns from the remainder of the method name
 761        // e.g., fetchAllByParentIdAndAreaId => ParentId, AreaId
 762        $list = explode('And', $method);
 763        
 764        // build the fetch params
 765        $where = array();
 766        foreach ($list as $key => $col) {
 767            // convert from ColName to col_name
 768            $col = strtolower(
 769                $this->_inflect->camelToUnder($col)
 770            );
 771            $where["{$this->_model_name}.$col = ?"] = $params[$key];
 772        }
 773        
 774        // add the last param after last column name as the "extra" settings
 775        // (order, group, having, page, paging, etc).
 776        $k = count($list);
 777        if (count($params) > $k) {
 778            $opts = (array) $params[$k];
 779        } else {
 780            $opts = array();
 781        }
 782        
 783        // merge the where with the base fetch params
 784        $opts = array_merge($opts, array(
 785            'where' => $where,
 786        ));
 787        
 788        // do the fetch
 789        return $this->$fetch($opts);
 790    }
 791    
 792    /**
 793     * 
 794     * Fetches a record or collection by primary key value(s).
 795     * 
 796     * @param int|array $spec The primary key value for a single record, or an
 797     * array of primary key values for a collection of records.
 798     * 
 799     * @param array $fetch An array of parameters for the fetch, with keys
 800     * for 'cols', 'group', 'having', 'order', etc.  Note that the 'where'
 801     * and 'order' elements are overridden and have no effect.
 802     * 
 803     * @return Solar_Sql_Model_Record|Solar_Sql_Model_Collection A record or
 804     * record-set object.
 805     * 
 806     */
 807    public function fetch($spec, $fetch = null)
 808    {
 809        $col = "{$this->_model_name}.{$this->_primary_col}";
 810        if (is_array($spec)) {
 811            $fetch['where'] = array("$col IN (?)" => $spec);
 812            $fetch['order'] = $col;
 813            return $this->fetchAll($fetch);
 814        } else {
 815            $fetch['where'] = array("$col = ?" => $spec);
 816            $fetch['order'] = $col;
 817            return $this->fetchOne($fetch);
 818        }
 819    }
 820    
 821    /**
 822     * 
 823     * Fetches a collection of all records by arbitrary parameters.
 824     * 
 825     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
 826     * fetch.
 827     * 
 828     * @return Solar_Sql_Model_Collection A collection object.
 829     * 
 830     */
 831    public function fetchAll($fetch = null)
 832    {
 833        // fetch the result array and select object
 834        $fetch = $this->_fixFetchParams($fetch);
 835        list($result, $select) = $this->_fetchResultSelect('all', $fetch);
 836        if (! $result) {
 837            return array();
 838        }
 839        
 840        // create a collection from the result
 841        $coll = $this->newCollection($result);
 842        
 843        // add pager-info to the collection
 844        if ($fetch['count_pages']) {
 845            $this->_setCollectionPagerInfo($coll, $fetch);
 846        }
 847        
 848        // done
 849        return $coll;
 850    }
 851    
 852    /**
 853     * 
 854     * Fetches an array of rows by arbitrary parameters.
 855     * 
 856     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
 857     * fetch.
 858     * 
 859     * @return array
 860     * 
 861     */
 862    public function fetchAllAsArray($fetch = null)
 863    {
 864        // fetch the result array and select object
 865        $fetch = $this->_fixFetchParams($fetch);
 866        list($result, $select) = $this->_fetchResultSelect('all', $fetch);
 867        if (! $result) {
 868            return array();
 869        } else {
 870            return $result;
 871        }
 872    }
 873    
 874    /**
 875     * 
 876     * The same as fetchAll(), except the record collection is keyed on the
 877     * first column of the results (instead of being a strictly sequential
 878     * array.)
 879     * 
 880     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
 881     * fetch.
 882     * 
 883     * @return Solar_Sql_Model_Collection A collection object.
 884     * 
 885     */
 886    public function fetchAssoc($fetch = null)
 887    {
 888        // fetch the result array and select object
 889        $fetch = $this->_fixFetchParams($fetch);
 890        list($result, $select) = $this->_fetchResultSelect('assoc', $fetch);
 891        if (! $result) {
 892            return array();
 893        }
 894        
 895        // create a collection from the result
 896        $coll = $this->newCollection($result);
 897        
 898        // add pager-info to the collection
 899        if ($fetch['count_pages']) {
 900            $this->_setCollectionPagerInfo($coll, $fetch);
 901        }
 902        
 903        // done
 904        return $coll;
 905    }
 906    
 907    /**
 908     * 
 909     * The same as fetchAssoc(), except it returns an array, not a collection.
 910     * 
 911     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
 912     * fetch.
 913     * 
 914     * @return array An array of rows.
 915     * 
 916     */
 917    public function fetchAssocAsArray($fetch = null)
 918    {
 919        // fetch the result array and select object
 920        $fetch = $this->_fixFetchParams($fetch);
 921        list($result, $select) = $this->_fetchResultSelect('assoc', $fetch);
 922        if (! $result) {
 923            return array();
 924        } else {
 925            return $result;
 926        }
 927    }
 928    
 929    /**
 930     * 
 931     * Sets the pager info in a collection, calling countPages() along the
 932     * way.
 933     * 
 934     * @param Solar_Sql_Model_Collection $coll The record collection to set
 935     * pager info on.
 936     * 
 937     * @param array $fetch The params for the original fetchAll() or
 938     * fetchAssoc().
 939     * 
 940     * @return void
 941     */
 942    protected function _setCollectionPagerInfo($coll, $fetch)
 943    {
 944        $total = $this->countPages($fetch);
 945        
 946        $info = array(
 947            'count'  => (int) $total['count'],
 948            'pages'  => (int) $total['pages'],
 949            'paging' => (int) $fetch['paging'],
 950        );
 951        
 952        if (! $info['count']) {
 953            $info['page']  = 0;
 954            $info['begin'] = 0;
 955            $info['end']   = 0;
 956        } elseif (! $fetch['page']) {
 957            $info['page']  = 1;
 958            $info['begin'] = 1;
 959            $info['end']   = $info['count'];
 960        } else {
 961            $start         = (int) ($fetch['page'] - 1) * $fetch['paging'];
 962            $info['page']  = $fetch['page'];
 963            $info['begin'] = $start + 1;
 964            $info['end']   = $start + $info['count'];
 965        }
 966        
 967        $info['is_first'] = (bool) ($info['page'] == 1);
 968        $info['is_last']  = (bool) ($info['end'] == $info['count']);
 969        
 970        $coll->setPagerInfo($info);
 971    }
 972    
 973    /**
 974     * 
 975     * Fetches one record by arbitrary parameters.
 976     * 
 977     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
 978     * fetch.
 979     * 
 980     * @return Solar_Sql_Model_Record A record object.
 981     * 
 982     */
 983    public function fetchOne($fetch = null)
 984    {
 985        // fetch the result array and select object
 986        $fetch = $this->_fixFetchParams($fetch);
 987        list($result, $select) = $this->_fetchResultSelect('one', $fetch);
 988        if (! $result) {
 989            return null;
 990        }
 991        
 992        // get the main record, which sets the to-one data
 993        $record = $this->newRecord($result);
 994        
 995        // done
 996        return $record;
 997    }
 998    
 999    /**
1000     * 
1001     * The same as fetchOne(), but returns an array instead of a record object.
1002     * 
1003     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
1004     * fetch.
1005     * 
1006     * @return array
1007     * 
1008     */
1009    public function fetchOneAsArray($fetch = null)
1010    {
1011        // fetch the result array and select object
1012        $fetch = $this->_fixFetchParams($fetch);
1013        list($result, $select) = $this->_fetchResultSelect('one', $fetch);
1014        if (! $result) {
1015            return array();
1016        } else {
1017            return $result;
1018        }
1019    }
1020    
1021    /**
1022     * 
1023     * Fetches a sequential array of values from the model, using only the
1024     * first column of the results.
1025     * 
1026     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
1027     * fetch.
1028     * 
1029     * @return array
1030     * 
1031     */
1032    public function fetchCol($fetch = null)
1033    {
1034        // fetch the result array and select object
1035        $fetch = $this->_fixFetchParams($fetch);
1036        list($result, $select) = $this->_fetchResultSelect('col', $fetch);
1037        if ($result) {
1038            return $result;
1039        } else {
1040            return array();
1041        }
1042    }
1043    
1044    /**
1045     * 
1046     * Fetches an array of key-value pairs from the model, where the first
1047     * column is the key and the second column is the value.
1048     * 
1049     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
1050     * fetch.
1051     * 
1052     * @return array
1053     * 
1054     */
1055    public function fetchPairs($fetch = null)
1056    {
1057        // fetch the result array and select object
1058        $fetch = $this->_fixFetchParams($fetch);
1059        list($result, $select) = $this->_fetchResultSelect('pairs', $fetch);
1060        if ($result) {
1061            return $result;
1062        } else {
1063            return array();
1064        }
1065    }
1066    
1067    /**
1068     * 
1069     * Fetches a single value from the model (i.e., the first column of the 
1070     * first record of the returned page set).
1071     * 
1072     * @param array|Solar_Sql_Model_Params_Fetch $fetch Parameters for the
1073     * fetch.
1074     * 
1075     * @return mixed The single value from the model query, or null.
1076     * 
1077     */
1078    public function fetchValue($fetch = null)
1079    {
1080        // fetch the result array and select object
1081        $fetch = $this->_fixFetchParams($fetch);
1082        list($result, $select) = $this->_fetchResultSelect('value', $fetch);
1083        return $result;
1084    }
1085    
1086    /**
1087     * 
1088     * Returns a data result and the select used to fetch the data.
1089     * 
1090     * If caching is turned on, this will fetch from the cache (if available)
1091     * and save the result back to the cache (if needed).
1092     * 
1093     * @param string $type The type of fetch to perform: 'all', 'one', etc.
1094     * 
1095     * @param Solar_Sql_Model_Params_Fetch $fetch The params for the fetch.
1096     * 
1097     * @return array An array of two elements; element 0 is the result data,
1098     * element 1 is the Solar_Sql_Select object used to fetch the data.
1099     * 
1100     */
1101    protected function _fetchResultSelect($type, Solar_Sql_Model_Params_Fetch $fetch)
1102    {
1103        $select = $this->newSelect($fetch);
1104        
1105        // attempt to fetch from cache?
1106        if ($fetch['cache']) {
1107            $key = $this->_cache->entry($fetch);
1108            $result = $this->_cache->fetch($key);
1109            if ($result !== false) {
1110                // found some data!
1111                return array($result, $select);
1112            }
1113        }
1114        
1115        // attempt to fetch from database
1116        $result = $select->fetch($type);
1117        
1118        // now process the results through the eagers
1119        foreach ($fetch['eager'] as $name => $eager) {
1120            $related = $this->getRelated($name);
1121            $related->modEagerResult($eager, $result, $type, $fetch);
1122        }
1123        
1124        // add to cache?
1125        if ($fetch['cache']) {
1126            $this->_cache->add($key, $result);
1127        }
1128        
1129        // done
1130        return array($result, $select);
1131    }
1132    
1133    /**
1134     * 
1135     * Returns a new record with default values.
1136     * 
1137     * @param array $spec An array of user-specified data to place into the
1138     * new record, if any.
1139     * 
1140     * @return Solar_Sql_Model_Record A record object.
1141     * 
1142     */
1143    public function fetchNew($spec = null)
1144    {
1145        $record = $this->_newRecord();
1146        $data   = $this->_fetchNewData($spec);
1147        $record->initNew($this, $data);
1148        return $record;
1149    }
1150    
1151    /**
1152     * 
1153     * Support method to generate the data for a new, blank record.
1154     * 
1155     * @param array $spec An array of user-specified data to place into the
1156     * new record, if any.
1157     * 
1158     * @return array An array of data for loading into a a new, blank record.
1159     * 
1160     */
1161    protected function _fetchNewData($spec = null)
1162    {
1163        // the user-specifed data
1164        settype($spec, 'array');
1165        
1166        // the array of data for the record
1167        $data = array();
1168        
1169        // loop through each table column and collect default data
1170        foreach ($this->_table_cols as $key => $val) {
1171            if (array_key_exists($key, $spec)) {
1172                // user-specified
1173                $data[$key] = $spec[$key];
1174            } else {
1175                // default value
1176                $data[$key] = $val['default'];
1177            }
1178        }
1179        
1180        // loop through each calculate column and collect default data
1181        foreach ($this->_calculate_cols as $key => $val) {
1182            if (array_key_exists($key, $spec)) {
1183                // user-specified
1184                $data[$key] = $spec[$key];
1185            } else {
1186                // default value
1187                $data[$key] = $val['default'];
1188            }
1189        }
1190        
1191        // add Solar_Xml_Struct objects
1192        foreach ($this->_xmlstruct_cols as $key) {
1193            $data[$key] = Solar::factory($this->_xmlstruct_class);
1194        }
1195        
1196        // if we have inheritance, set that too
1197        if ($this->_inherit_name) {
1198            $key = $this->_inherit_col;
1199            $data[$key] = $this->_inherit_name;
1200        }
1201        
1202        // done
1203        return $data;
1204    }
1205    
1206    /**
1207     * 
1208     * Fetches count and pages of available records.
1209     * 
1210     * @param array $fetch An array of clauses for the SELECT COUNT()
1211     * statement, including 'where', 'group, and 'having'.
1212     * 
1213     * @return array An array with keys 'count' and 'pages'; 'count' is the
1214     * number of records, 'pages' is the number of pages.
1215     * 
1216     */
1217    public function countPages($fetch = null)
1218    {
1219        // fix up the parameters
1220        $fetch = $this->_fixFetchParams($fetch);
1221        
1222        // add a fake param called 'count' to make this different from the
1223        // orginating query (for cache deconfliction).
1224        $fetch['__count__'] = true;
1225        
1226        // check the cache
1227        if ($fetch['cache']) {
1228            $key = $this->_cache->entry($fetch);
1229            $result = $this->_cache->fetch($key);
1230            if ($result !== false) {
1231                // cache hit
1232                return $result;
1233            }
1234        }
1235        
1236        // clone the fetch for only the "keep" joins
1237        $clone = $fetch->cloneForKeeps();
1238        
1239        // get the base select
1240        $select = $this->newSelect($clone);
1241        
1242        // count on the primary column
1243        $col = "{$this->_model_name}.{$this->_primary_col}";
1244        $result = $select->countPages($col);
1245        
1246        // save in cache?
1247        if ($fetch['cache']) {
1248            $this->_cache->add($key, $result);
1249        }
1250        
1251        // done
1252        return $result;
1253    }
1254    
1255    // -----------------------------------------------------------------
1256    //
1257    // Select
1258    //
1259    // -----------------------------------------------------------------
1260    
1261    /**
1262     * 
1263     * Converts and cleans-up fetch params from arrays to instances of
1264     * Solar_Sql_Model_Params_Fetch.
1265     * 
1266     * @param array $spec The parameters for the fetch.
1267     * 
1268     * @return Solar_Sql_Model_Params_Fetch
1269     * 
1270     */
1271    protected function _fixFetchParams($spec)
1272    {
1273        if ($spec instanceof Solar_Sql_Model_Params_Fetch) {
1274            // already a params object, pre-empt further modification
1275            return $spec;
1276        }
1277        
1278        // baseline object
1279        $fetch = Solar::factory('Solar_Sql_Model_Params_Fetch');
1280        
1281        // defaults
1282        $fetch->load(array(
1283            'cache'  => $this->_config['auto_cache'],
1284            'paging' => $this->_paging,
1285            'alias'  => $this->_model_name,
1286        ));
1287        
1288        // user specification
1289        $fetch->load($spec);
1290        
1291        // add columns if none already specified
1292        if (! $fetch['cols']) {
1293            $fetch->cols($this->_fetch_cols);
1294        }
1295        
1296        // done
1297        return $fetch;
1298    }
1299    
1300    /**
1301     * 
1302     * Returns a WHERE clause array of conditions to use when fetching
1303     * from this model; e.g., single-table inheritance.
1304     * 
1305     * @param array $where The WHERE array being modified.
1306     * 
1307     * @param string $alias The current name of the table for this model
1308     * in the query being constructed; defaults to the model name.
1309     *
1310     * @return array The modified WHERE array.
1311     *
1312     */   
1313    public function getConditions($alias = null)
1314    {
1315        // default to the model name for the alias
1316        if (! $alias) {
1317            $alias = $this->_model_name;
1318        }
1319        
1320        // the array of where clauses
1321        $where = array();
1322        
1323        // is inheritance on?
1324        if ($this->isInherit()) {
1325            $key = "{$alias}.{$this->_inherit_col} = ?";
1326            $val = $this->_inherit_name;
1327            $where = array($key => $val);
1328        }
1329        
1330        // done!
1331        return $where;
1332    }
1333    
1334    /**
1335     * 
1336     * Returns a new Solar_Sql_Select tool, with the proper SQL object
1337     * injected automatically.
1338     * 
1339     * @param Solar_Sql_Model_Params_Fetch|array $fetch Parameters for the
1340     * fetch.
1341     * 
1342     * @return Solar_Sql_Select
1343     * 
1344     */
1345    public function newSelect($fetch = null)
1346    {
1347        $fetch = $this->_fixFetchParams($fetch);
1348        
1349        if (! $fetch['alias']) {
1350            $fetch->alias($this->_model_name);
1351        }
1352        
1353        foreach ($fetch['eager'] as $name => $eager) {
1354            $related = $this->getRelated($name);
1355            $related->modEagerFetch($eager, $fetch);
1356        }
1357        
1358        $use_default_order = ! $fetch['order'] && $fetch['order'] !== false;
1359        if ($use_default_order && $this->_order) {
1360            $fetch->order("{$fetch['alias']}.{$this->_order}");
1361        };
1362        
1363        // get the select object
1364        $select = Solar::factory(
1365            $this->_select_class,
1366            array('sql' => $this->_sql)
1367        );
1368        
1369        // add the explicitly asked-for columns before the eager-join cols.
1370        // this is to make sure the fetchPairs() method works right, because
1371        // adding the eager columns first will mess that up.
1372        $select->from(
1373            "{$this->_table_name} AS {$fetch['alias']}",
1374            $fetch['cols']
1375        );
1376        
1377        $select->multiWhere($this->getConditions($fetch['alias']));
1378        
1379        // all the other pieces
1380        $select->distinct($fetch['distinct'])
1381               ->multiJoin($fetch['join'])
1382               ->multiWhere($fetch['where'])
1383               ->group($fetch['group'])
1384               ->multiHaving($fetch['having'])
1385               ->order($fetch['order'])
1386               ->setPaging($fetch['paging'])
1387               ->bind($fetch['bind']);
1388        
1389        // limit by count/offset, or by page?
1390        if ($fetch['limit']) {
1391            list($count, $offset) = $fetch['limit'];
1392            $select->limit($count, $offset);
1393        } else {
1394            $select->limitPage($fetch['page']);
1395        }
1396        
1397        // done!
1398        return $select;
1399    }
1400    
1401    // -----------------------------------------------------------------
1402    //
1403    // Record and Collection factories
1404    //
1405    // -----------------------------------------------------------------
1406    
1407    /**
1408     * 
1409     * Returns the appropriate record object, honoring inheritance.
1410     * 
1411     * @param array $data The data to load into the record.
1412     * 
1413     * @return Solar_Sql_Model_Record A record object.
1414     * 
1415     */
1416    public function newRecord($data)
1417    {
1418        // the record to return, eventually
1419        $record = null;
1420        
1421        // look for an inheritance in relation to $data
1422        $inherit = null;
1423        if ($this->_inherit_col && ! empty($data[$this->_inherit_col])) {
1424            // inheritance is available, and a value is set for the
1425            // inheritance column in the data
1426            $inherit = trim($data[$this->_inherit_col]);
1427        }
1428        
1429        // did we find an inheritance value?
1430        if ($inherit) {
1431            // try to find a model class based on inheritance, going up the
1432            // stack as needed. this checks for Current_Model_Type,
1433            // Parent_Model_Type, Grandparent_Model_Type, etc.
1434            // 
1435            // blow up if we can't find it, since this is explicitly noted
1436            // as the inheritance class.
1437            $inherit_class = $this->_catalog->getClass($inherit);
1438            
1439            // if different from the current class, reset the model object.
1440            if ($inherit_class != $this->_class) {
1441                // use the inherited model class, it's different from the
1442                // current model. if it's not different, fall through, leaving
1443                // $record == null.  that will invoke the logic below.
1444                $model = $this->_catalog->getModelByClass($inherit_class);
1445                $record = $model->newRecord($data);
1446            }
1447        }
1448        
1449        // do we have a record yet?
1450        if (! $record) {
1451            // no, because an inheritance model was not specified, or was of 
1452            // the same class as this class.
1453            $record = $this->_newRecord();
1454            $record->init($this, $data);
1455        }
1456        
1457        return $record;
1458    }
1459    
1460    /**
1461     * 
1462     * Returns a new record object for this model only.
1463     * 
1464     * @return Solar_Sql_Model_Record A record object.
1465     * 
1466     */
1467    protected function _newRecord()
1468    {
1469        if (empty($this->_record_prototype)) {
1470            // find the record class
1471            $record_class = $this->_stack->load('Record', false);
1472            if (! $record_class) {
1473                // use the default record class
1474                $record_class = $this->_record_class;
1475            }
1476            $this->_record_prototype = Solar::factory($record_class);
1477        }
1478        $record = clone $this->_record_prototype;
1479        return $record;
1480    }    
1481    
1482    /**
1483     * 
1484     * Returns the appropriate collection object for this model.
1485     * 
1486     * @param array $data The data to load into the collection, if any.
1487     * 
1488     * @return Solar_Sql_Model_Collection A collection object.
1489     * 
1490     */
1491    public function newCollection($data = null)
1492    {
1493        $collection = $this->_newCollection();
1494        $collection->setModel($this);
1495        $collection->load($data);
1496        return $collection;
1497    }
1498    
1499    /**
1500     * 
1501     * Returns a new collection object for this model only.
1502     * 
1503     * @return Solar_Sql_Model_Collection A collection object.
1504     * 
1505     */
1506    protected function _newCollection()
1507    {
1508        if (empty($this->_collection_prototype)) {
1509            // find the collection class
1510            $collection_class = $this->_stack->load('Collection', false);
1511            if (! $collection_class) {
1512                // use the default collection class
1513                $collection_class = $this->_collection_class;
1514            }
1515            $this->_collection_prototype = Solar::factory($collection_class);
1516        }
1517        $collection = clone $this->_collection_prototype;
1518        return $collection;
1519    }
1520    
1521    // -----------------------------------------------------------------
1522    //
1523    // Insert, update, or delete rows in the model.
1524    //
1525    // -----------------------------------------------------------------
1526    
1527    /**
1528     * 
1529     * Inserts one row to the model table and deletes cache entries.
1530     * 
1531     * @param array $data The row data to insert.
1532     * 
1533     * @return int|bool On success, the last inserted ID if there is an
1534     * auto-increment column on the model (otherwise boolean true). On failure
1535     * an exception from PDO bubbles up.
1536     * 
1537     * @throws Solar_Sql_Exception on failure of any sort.
1538     * 
1539     * @see Solar_Sql_Model_Cache::deleteAll()
1540     * 
1541     */
1542    public function insert($data)
1543    {
1544        if (! is_array($data)) {
1545            throw $this->_exception('ERR_DATA_NOT_ARRAY', array(
1546                'method' => 'insert',
1547            ));
1548        }
1549        
1550        // reset affected rows
1551        $this->_affected_rows;
1552        
1553        // remove non-existent table columns from the data
1554        foreach ($data as $key => $val) {
1555            if (empty($this->_table_cols[$key])) {
1556                unset($data[$key]);
1557                // not in the table, so no need to check for autoinc
1558                continue;
1559            }
1560            
1561            // remove empty autoinc columns to soothe postgres, which won't
1562            // take explicit NULLs in SERIAL cols.
1563            if ($this->_table_cols[$key]['autoinc'] && empty($val)) {
1564                unset($data[$key]);
1565            }
1566        }
1567        
1568        // perform the insert and track affected rows
1569        $this->_affected_rows = $this->_sql->insert(
1570            $this->_table_name,
1571            $data
1572        );
1573                
1574        // does the table have an autoincrement column?
1575        $autoinc = null;
1576        foreach ($this->_table_cols as $name => $info) {
1577            if ($info['autoinc']) {
1578                $autoinc = $name;
1579                break;
1580            }
1581        }
1582        
1583        // return the last insert id, or just "true" ?
1584        if ($autoinc) {
1585            $id = $this->_sql->lastInsertId($this->_table_name, $autoinc);
1586        } 
1587
1588        // clear the cache for this model and related models
1589        $this->_cache->deleteAll();
1590
1591        if ($autoinc) {
1592            return $id;
1593        } else {
1594            return true;
1595        }
1596    }
1597    
1598    /**
1599     * 
1600     * Updates rows in the model table and deletes cache entries.
1601     * 
1602     * @param array $data The row data to insert.
1603     * 
1604     * @param string|array $where The WHERE clause to identify which rows to 
1605     * update.
1606     * 
1607     * @return int The number of rows affected.
1608     * 
1609     * @throws Solar_Sql_Exception on failure of any sort.
1610     * 
1611     * @see Solar_Sql_Model_Cache::deleteAll()
1612     * 
1613     */
1614    public function update($data, $where)
1615    {
1616        if (! is_array($data)) {
1617            throw $this->_exception('ERR_DATA_NOT_ARRAY', array(
1618                'method' => 'update',
1619            ));
1620        }
1621        
1622        // reset affected rows
1623        $this->_affected_rows = null;
1624        
1625        // don't update the primary key
1626        unset($data[$this->_primary_col]);
1627        
1628        // remove non-existent table columns from the data
1629        foreach ($data as $key => $val) {
1630            if (empty($this->_table_cols[$key])) {
1631                unset($data[$key]);
1632            }
1633        }
1634        
1635        // perform the update and track affected rows
1636        $this->_affected_rows = $this->_sql->update(
1637            $this->_table_name,
1638            $data,
1639            $where
1640        );
1641        
1642        // clear the cache for this model and related models
1643        $this->_cache->deleteAll();
1644        
1645        // done!
1646        return $this->_affected_rows;
1647    }
1648    
1649    /**
1650     * 
1651     * Deletes rows from the model table and deletes cache entries.
1652     * 
1653     * @param string|array $where The WHERE clause to identify which rows to 
1654     * delete.
1655     * 
1656     * @return int The number of rows affected.
1657     * 
1658     * @see Solar_Sql_Model_Cache::deleteAll()
1659     * 
1660     */
1661    public function delete($where)
1662    {
1663        // perform the deletion and track affected rows
1664        $this->_affected_rows = $this->_sql->delete(
1665            $this->_table_name,
1666            $where
1667        );
1668        
1669        // clear the cache for this model and related models
1670        $this->_cache->deleteAll();
1671        
1672        // done!
1673        return $this->_affected_rows;
1674    }
1675    
1676    /**
1677     * 
1678     * Serializes data values in-place based on $this->_serialize_cols and
1679     * $this->_xmlstruct_cols.
1680     * 
1681     * Does not attempt to serialize null values.
1682     * 
1683     * If serializing fails, stores 'null' in the data.
1684     * 
1685     * @param array &$data Record data.
1686     * 
1687     * @return void
1688     * 
1689     */
1690    public function serializeCols(&$data)
1691    {
1692        foreach ($this->_serialize_cols as $key) {
1693
1694            // Don't process columns not in $data
1695            if (! array_key_exists($key, $data)) {
1696                continue;
1697            }
1698
1699            // don't work on empty cols
1700            if (empty($data[$key])) {
1701                // Any empty value is canonicalized as null
1702                $data[$key] = null;
1703                continue;
1704            }
1705            
1706            $data[$key] = serialize($data[$key]);
1707            if (! $data[$key]) {
1708                // serializing failed
1709                $data[$key] = null;
1710            }
1711        }
1712        
1713        foreach ($this->_xmlstruct_cols as $key) {
1714
1715            // Don't process columns not in $data
1716            if (! array_key_exists($key, $data)) {
1717                continue;
1718            }
1719
1720            // don't work on empty cols
1721            if (empty($data[$…

Large files files are truncated, but you can click here to view the full file