We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Managed_DataObject.php 19.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Wrapper for Memcached_DataObject which knows its own schema definition.
 * Builds its own damn settings from a schema definition.
 *
24
 * @author Brion Vibber <brion@status.net>
25
 */
26
abstract class Managed_DataObject extends Memcached_DataObject
27 28 29 30
{
    /**
     * The One True Thingy that must be defined and declared.
     */
31 32 33 34
    public static function schemaDef()
    {
        throw new MethodNotImplementedException(__METHOD__);
    }
35

36 37 38 39 40 41 42 43 44
    /**
     * Get an instance by key
     *
     * @param string $k Key to use to lookup (usually 'id' for this class)
     * @param mixed  $v Value to lookup
     *
     * @return get_called_class() object if found, or null for no hits
     *
     */
45
    static function getKV($k,$v=NULL)
46
    {
47
        return parent::getClassKV(get_called_class(), $k, $v);
48 49
    }

50 51 52 53 54 55 56 57 58 59 60 61
    /**
     * Get an instance by compound key
     *
     * This is a utility method to get a single instance with a given set of
     * key-value pairs. Usually used for the primary key for a compound key; thus
     * the name.
     *
     * @param array $kv array of key-value mappings
     *
     * @return get_called_class() object if found, or null for no hits
     *
     */
62
    static function pkeyGet(array $kv)
63 64 65
    {
        return parent::pkeyGetClass(get_called_class(), $kv);
    }
66

67 68 69 70 71
    static function pkeyCols()
    {
        return parent::pkeyColsClass(get_called_class());
    }

mattl's avatar
mattl committed
72 73 74 75 76 77 78 79 80 81 82 83 84 85
    /**
     * Get multiple items from the database by key
     *
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param boolean $skipNulls return only non-null results?
     *
     * @return array Array of objects, in order
     */
	static function multiGet($keyCol, array $keyVals, $skipNulls=true)
	{
	    return parent::multiGetClass(get_called_class(), $keyCol, $keyVals, $skipNulls);
	}

mattl's avatar
mattl committed
86 87 88 89 90 91 92 93 94 95 96 97 98 99
    /**
     * Get multiple items from the database by key
     *
     * @param string  $keyCol    name of column for key
     * @param array   $keyVals   key values to fetch
     * @param array   $otherCols Other columns to hold fixed
     *
     * @return array Array mapping $keyVals to objects, or null if not found
     */
	static function pivotGet($keyCol, array $keyVals, array $otherCols=array())
	{
	    return parent::pivotGetClass(get_called_class(), $keyCol, $keyVals, $otherCols);
	}

100 101 102 103
    /**
     * Get a multi-instance object
     *
     * This is a utility method to get multiple instances with a given set of
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
     * values for a specific column.
     *
     * @param string $keyCol  key column name
     * @param array  $keyVals array of key values
     *
     * @return get_called_class() object with multiple instances if found,
     *         Exception is thrown when no entries are found.
     *
     */
    static function listFind($keyCol, array $keyVals)
    {
        return parent::listFindClass(get_called_class(), $keyCol, $keyVals);
    }

    /**
119
     * Get a multi-instance object separated into an array
120 121
     *
     * This is a utility method to get multiple instances with a given set of
122
     * values for a specific key column. Usually used for the primary key when
123
     * multiple values are desired. Result is an array.
124
     *
125 126
     * @param string $keyCol  key column name
     * @param array  $keyVals array of key values
127
     *
128
     * @return array with an get_called_class() object for each $keyVals entry
129 130
     *
     */
131
    static function listGet($keyCol, array $keyVals)
132 133 134 135
    {
        return parent::listGetClass(get_called_class(), $keyCol, $keyVals);
    }

136 137 138 139 140 141
    /**
     * get/set an associative array of table columns
     *
     * @access public
     * @return array (associative)
     */
mattl's avatar
mattl committed
142
    public function table()
143
    {
144
        $table = static::schemaDef();
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
        return array_map(array($this, 'columnBitmap'), $table['fields']);
    }

    /**
     * get/set an  array of table primary keys
     *
     * Key info is pulled from the table definition array.
     * 
     * @access private
     * @return array
     */
    function keys()
    {
        return array_keys($this->keyTypes());
    }

    /**
     * Get a sequence key
     *
     * Returns the first serial column defined in the table, if any.
     *
     * @access private
     * @return array (column,use_native,sequence_name)
     */

    function sequenceKey()
    {
mattl's avatar
mattl committed
172
        $table = static::schemaDef();
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
        foreach ($table['fields'] as $name => $column) {
            if ($column['type'] == 'serial') {
                // We have a serial/autoincrement column.
                // Declare it to be a native sequence!
                return array($name, true, false);
            }
        }

        // No sequence key on this table.
        return array(false, false, false);
    }

    /**
     * Return key definitions for DB_DataObject and Memcache_DataObject.
     *
     * DB_DataObject needs to know about keys that the table has; this function
     * defines them.
     *
     * @return array key definitions
     */

    function keyTypes()
    {
mattl's avatar
mattl committed
196
        $table = static::schemaDef();
197
        $keys = array();
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

        if (!empty($table['unique keys'])) {
            foreach ($table['unique keys'] as $idx => $fields) {
                foreach ($fields as $name) {
                    $keys[$name] = 'U';
                }
            }
        }

        if (!empty($table['primary key'])) {
            foreach ($table['primary key'] as $name) {
                $keys[$name] = 'K';
            }
        }
        return $keys;
    }

    /**
     * Build the appropriate DB_DataObject bitfield map for this field.
     *
     * @param array $column
     * @return int
     */
    function columnBitmap($column)
    {
Brion Vibber's avatar
Brion Vibber committed
223 224 225 226 227 228 229 230 231 232 233 234
        $type = $column['type'];

        // For quoting style...
        $intTypes = array('int',
                          'integer',
                          'float',
                          'serial',
                          'numeric');
        if (in_array($type, $intTypes)) {
            $style = DB_DATAOBJECT_INT;
        } else {
            $style = DB_DATAOBJECT_STR;
235 236
        }

Brion Vibber's avatar
Brion Vibber committed
237 238 239 240 241 242 243 244 245 246
        // Data type formatting style...
        $formatStyles = array('blob' => DB_DATAOBJECT_BLOB,
                              'text' => DB_DATAOBJECT_TXT,
                              'date' => DB_DATAOBJECT_DATE,
                              'time' => DB_DATAOBJECT_TIME,
                              'datetime' => DB_DATAOBJECT_DATE | DB_DATAOBJECT_TIME,
                              'timestamp' => DB_DATAOBJECT_MYSQLTIMESTAMP);

        if (isset($formatStyles[$type])) {
            $style |= $formatStyles[$type];
247 248
        }

Brion Vibber's avatar
Brion Vibber committed
249
        // Nullable?
250
        if (!empty($column['not null'])) {
Brion Vibber's avatar
Brion Vibber committed
251
            $style |= DB_DATAOBJECT_NOTNULL;
252 253
        }

Brion Vibber's avatar
Brion Vibber committed
254
        return $style;
255
    }
Evan Prodromou's avatar
Evan Prodromou committed
256 257 258 259 260

    function links()
    {
        $links = array();

mattl's avatar
mattl committed
261
        $table = static::schemaDef();
Evan Prodromou's avatar
Evan Prodromou committed
262 263 264

        foreach ($table['foreign keys'] as $keyname => $keydef) {
            if (count($keydef) == 2 && is_string($keydef[0]) && is_array($keydef[1]) && count($keydef[1]) == 1) {
265 266 267
                if (isset($keydef[1][0])) {
                    $links[$keydef[1][0]] = $keydef[0].':'.$keydef[1][1];
                }
Evan Prodromou's avatar
Evan Prodromou committed
268 269 270 271
            }
        }
        return $links;
    }
272 273 274 275 276 277 278 279 280 281

    /**
     * Return a list of all primary/unique keys / vals that will be used for
     * caching. This will understand compound unique keys, which
     * Memcached_DataObject doesn't have enough info to handle properly.
     *
     * @return array of strings
     */
    function _allCacheKeys()
    {
mattl's avatar
mattl committed
282
        $table = static::schemaDef();
283 284 285
        $ckeys = array();

        if (!empty($table['unique keys'])) {
286 287
            $keyNames = $table['unique keys'];
            foreach ($keyNames as $idx => $fields) {
288 289
                $val = array();
                foreach ($fields as $name) {
290
                    $val[$name] = self::valueString($this->$name);
291
                }
292
                $ckeys[] = self::multicacheKey($this->tableName(), $val);
293 294 295 296 297 298 299
            }
        }

        if (!empty($table['primary key'])) {
            $fields = $table['primary key'];
            $val = array();
            foreach ($fields as $name) {
300
                $val[$name] = self::valueString($this->$name);
301
            }
302
            $ckeys[] = self::multicacheKey($this->tableName(), $val);
303 304 305
        }
        return $ckeys;
    }
306

307 308 309 310 311
    public function escapedTableName()
    {
        return common_database_tablename($this->tableName());
    }

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
    /**
     * Returns an object by looking at the primary key column(s).
     *
     * Will require all primary key columns to be defined in an associative array
     * and ignore any keys which are not part of the primary key.
     *
     * Will NOT accept NULL values as part of primary key.
     *
     * @param   array   $vals       Must match all primary key columns for the dataobject.
     *
     * @return  Managed_DataObject  of the get_called_class() type
     * @throws  NoResultException   if no object with that primary key
     */
    static function getByPK(array $vals)
    {
        $classname = get_called_class();

        $pkey = static::pkeyCols();
        if (is_null($pkey)) {
            throw new ServerException("Failed to get primary key columns for class '{$classname}'");
        }

        $object = new $classname();
        foreach ($pkey as $col) {
            if (!array_key_exists($col, $vals)) {
mattl's avatar
mattl committed
337
                throw new ServerException("Missing primary key column '{$col}' for ".get_called_class()." among provided keys: ".implode(',', array_keys($vals)));
338 339 340 341 342 343 344 345 346 347 348
            } elseif (is_null($vals[$col])) {
                throw new ServerException("NULL values not allowed in getByPK for column '{$col}'");
            }
            $object->$col = $vals[$col];
        }
        if (!$object->find(true)) {
            throw new NoResultException($object);
        }
        return $object;
    }

mattl's avatar
mattl committed
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
    /**
     * Returns an object by looking at given unique key columns.
     *
     * Will NOT accept NULL values for a unique key column. Ignores non-key values.
     *
     * @param   array   $vals       All array keys which are set must be non-null.
     *
     * @return  Managed_DataObject  of the get_called_class() type
     * @throws  NoResultException   if no object with that primary key
     */
    static function getByKeys(array $vals)
    {
        $classname = get_called_class();

        $object = new $classname();

        $keys = $object->keys();
        if (is_null($keys)) {
            throw new ServerException("Failed to get key columns for class '{$classname}'");
        }

        foreach ($keys as $col) {
            if (!array_key_exists($col, $vals)) {
                continue;
            } elseif (is_null($vals[$col])) {
mattl's avatar
mattl committed
374
                throw new ServerException("NULL values not allowed in getByKeys for column '{$col}'");
mattl's avatar
mattl committed
375 376 377 378 379 380 381 382 383
            }
            $object->$col = $vals[$col];
        }
        if (!$object->find(true)) {
            throw new NoResultException($object);
        }
        return $object;
    }

384 385 386
    static function getByID($id)
    {
        if (empty($id)) {
387
            throw new EmptyIdException(get_called_class());
388 389 390 391 392 393
        }
        // getByPK throws exception if id is null
        // or if the class does not have a single 'id' column as primary key
        return static::getByPK(array('id' => $id));
    }

394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    /**
     * Returns an ID, checked that it is set and reasonably valid
     *
     * If this dataobject uses a special id field (not 'id'), just
     * implement your ID getting method in the child class.
     *
     * @return int ID of dataobject
     * @throws Exception (when ID is not available or not set yet)
     */
    public function getID()
    {
        // FIXME: Make these exceptions more specific (their own classes)
        if (!isset($this->id)) {
            throw new Exception('No ID set.');
        } elseif (empty($this->id)) {
            throw new Exception('Empty ID for object! (not inserted yet?).');
        }

mattl's avatar
mattl committed
412
        return intval($this->id);
413
    }
414

mattl's avatar
mattl committed
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
    /**
     * WARNING: Only use this on Profile and Notice. We should probably do
     * this with traits/"implements" or whatever, but that's over the top
     * right now, I'm just throwing this in here to avoid code duplication
     * in Profile and Notice classes.
     */
    public function getAliases()
    {
        $aliases = array();
        $aliases[$this->getUri()] = $this->getID();

        try {
            $aliases[$this->getUrl()] = $this->getID();
        } catch (InvalidUrlException $e) {
            // getUrl failed because no valid URL could be returned, just ignore it
        }

        if (common_config('fix', 'fancyurls')) {
            /**
             * Here we add some hacky hotfixes for remote lookups that have been taught the
             * (at least now) wrong URI but it's still obviously the same user. Such as:
             * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1
             * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1
             * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1
             * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1
             */
            foreach ($aliases as $alias=>$id) {
                try {
                    // get a "fancy url" version of the alias, even without index.php/
                    $alt_url = common_fake_local_fancy_url($alias);
                    // store this as well so remote sites can be sure we really are the same profile
                    $aliases[$alt_url] = $id;
                } catch (Exception $e) {
                    // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be
                }

                try {
                    // get a non-"fancy url" version of the alias, i.e. add index.php/
                    $alt_url = common_fake_local_nonfancy_url($alias);
                    // store this as well so remote sites can be sure we really are the same profile
                    $aliases[$alt_url] = $id;
                } catch (Exception $e) {
                    // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be
                }
            }
        }
        return $aliases;
    }

464
    // 'update' won't write key columns, so we have to do it ourselves.
465
    // This also automatically calls "update" _before_ it sets the keys.
mattl's avatar
mattl committed
466 467 468 469 470
    // FIXME: This only works with single-column primary keys so far! Beware!
    /**
     * @param DB_DataObject &$orig  Must be "instanceof" $this
     * @param string         $pid   Primary ID column (no escaping is done on column name!)
     */
471
    public function updateWithKeys(Managed_DataObject $orig, $pid=null)
472 473 474 475 476
    {
        if (!$orig instanceof $this) {
            throw new ServerException('Tried updating a DataObject with a different class than itself.');
        }

477 478 479 480
        if ($this->N <1) {
            throw new ServerException('DataObject must be the result of a query (N>=1) before updateWithKeys()');
        }

481 482
        // do it in a transaction
        $this->query('BEGIN');
483

484 485 486 487 488 489 490
        $parts = array();
        foreach ($this->keys() as $k) {
            if (strcmp($this->$k, $orig->$k) != 0) {
                $parts[] = $k . ' = ' . $this->_quote($this->$k);
            }
        }
        if (count($parts) == 0) {
491 492 493 494 495
            // No changes to keys, it's safe to run ->update(...)
            if ($this->update($orig) === false) {
                common_log_db_error($this, 'UPDATE', __FILE__);
                // rollback as something bad occurred
                $this->query('ROLLBACK');
496
                throw new ServerException("Could not UPDATE non-keys for {$this->tableName()}");
497
            }
mattl's avatar
mattl committed
498 499
            $orig->decache();
            $this->encache();
500 501 502

            // commit our db transaction since we won't reach the COMMIT below
            $this->query('COMMIT');
mattl's avatar
mattl committed
503
            // @FIXME return true only if something changed (otherwise 0)
504 505 506
            return true;
        }

507 508 509 510 511 512 513 514 515 516 517 518 519 520
        if ($pid === null) {
            $schema = static::schemaDef();
            $pid = $schema['primary key'];
            unset($schema);
        }
        $pidWhere = array();
        foreach((array)$pid as $pidCol) { 
            $pidWhere[] = sprintf('%1$s = %2$s', $pidCol, $this->_quote($orig->$pidCol));
        }
        if (empty($pidWhere)) {
            throw new ServerException('No primary ID column(s) set for updateWithKeys');
        }

        $qry = sprintf('UPDATE %1$s SET %2$s WHERE %3$s',
mattl's avatar
mattl committed
521 522
                            common_database_tablename($this->tableName()),
                            implode(', ', $parts),
523
                            implode(' AND ', $pidWhere));
mattl's avatar
mattl committed
524

525
        $result = $this->query($qry);
526 527 528 529
        if ($result === false) {
            common_log_db_error($this, 'UPDATE', __FILE__);
            // rollback as something bad occurred
            $this->query('ROLLBACK');
530
            throw new ServerException("Could not UPDATE key fields for {$this->tableName()}");
531
        }
532 533

        // Update non-keys too, if the previous endeavour worked.
534 535
        // The ->update call uses "$this" values for keys, that's why we can't do this until
        // the keys are updated (because they might differ from $orig and update the wrong entries).
536 537 538 539
        if ($this->update($orig) === false) {
            common_log_db_error($this, 'UPDATE', __FILE__);
            // rollback as something bad occurred
            $this->query('ROLLBACK');
540
            throw new ServerException("Could not UPDATE non-keys for {$this->tableName()}");
541
        }
mattl's avatar
mattl committed
542
        $orig->decache();
543 544 545 546
        $this->encache();

        // commit our db transaction
        $this->query('COMMIT');
mattl's avatar
mattl committed
547
        // @FIXME return true only if something changed (otherwise 0)
548 549
        return $result;
    }
550 551 552 553 554

    static public function beforeSchemaUpdate()
    {
        // NOOP
    }
555 556 557 558 559 560 561 562 563 564 565 566

    static function newUri(Profile $actor, Managed_DataObject $object, $created=null)
    {
        if (is_null($created)) {
            $created = common_sql_now();
        }
        return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
                                        $actor->getID(),
                                        ActivityUtils::resolveUri($object->getObjectType(), true),
                                        $object->getID(),
                                        common_date_iso8601($created));
    }
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588

    protected function onInsert()
    {
        // NOOP by default
    }

    protected function onUpdate($dataObject=false)
    {
        // NOOP by default
    }

    public function insert()
    {
        $this->onInsert();
        return parent::insert();
    }

    public function update($dataObject=false)
    {
        $this->onUpdate($dataObject);
        return parent::update($dataObject);
    }
589
}