Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Sign In with Google Sign In with OpenID

Support for different primary key states

edited December 2014 in Framework

Started mulling around with the Model class to try and add support for multi-column primary keys and tables without a primary key. I started out with the constructor method and I'd like some feedback on my changes. I tried my best make it backwards compatible.

public function __construct ($vals = false, $is_new = true) {
    $this->table = ($this->table === '') ? strtolower (get_class ($this)) : $this->table;
    $vals = is_object ($vals) ? (array) $vals : $vals;

    if (is_array ($vals)) {
        if ((bool)$this->key == false) { // no-key
            $this->keyval = array();
            $this->data = $vals;
            foreach ($this->key as $key) {
                if (isset($vals[$key]) $this->keyval[$key] = $vals[$key];
            }
        } elseif (is_array($this->key)) { // array-key
            if (count(array_diff(array_keys($vals),$this->key)) == 0) {
                // query DB with multi column primary key values
                $res = DB::single ('select * from `' . $this->table . '` where `' . join('` = ?, `', array_keys($vals)) . '` = ?', array_values($vals));
                if (! $res) {
                    $this->error = 'No object exists with that primary key.';
                } else {
                    $this->data = (array) $res;
                    $this->keyval = array();
                    foreach ($this->key as $key) {
                        $this->keyval[$key] = $this->data[$key];
                    }
                }
            } else {
                $this->keyval = array();
                $this->data = $vals;
                foreach ($this->key as $key) {
                    if (isset($vals[$key]) $this->keyval[$key] = $vals[$key];
                }
            }
        } elseif (isset ($vals[$this->key])) $this->keyval = $vals[$this->key];

        if ($is_new) $this->is_new = true;

    } elseif ($vals !== false) {
        // Still trying to figure out the best way to handle the array-key/no-key when passed a single value (required for backwards compatibility)
        // Maybe just set an error?
        if (is_array($this->key) || (bool)$this->key == false) { // array-key || no-key
            $this->error = 'Model requires values passed as an associative array of key=>value entries.';
        } else {
            $res = DB::single ('select * from `' . $this->table . '` where `' . $this->key . '` = ?', $vals);
            if (! $res) {
                $this->error = 'No object by that ID.';
            } else {
                $this->data = (array) $res;
                $this->keyval = $this->data[$this->key];
            }
        }
    } else {
        $this->is_new = true;
    }
}

Comments

  • First a couple quick notes I made:

    • I see you're also working to support tables with no primary key, which is also currently a limitation of Model.
    • The foreach on lines 9-11 can probably be removed, since we just asserted that $this->key is false on line 6.
    • I think the elseif block starting on line 32 still needs the line $this->data = $vals; or we lose those values. This may be better as just an else, but I'm not sure.
    • I think it is correct to fail if you pass a single non-array value and there's either a multi-field primary key or no primary key for that model (as per your comment on line 37. Tables with multi-field primary keys or no primary key should behave differently in how they're called.

    I think a helper method that checks an array to see if it contains only the primary key fields might be useful. For example, something like this:

    /**
     * Checks whether a value is valid as a primary key value.
     */
    private function _is_valid_key ($val) {
        if ((bool) $this->key === false) {
            return false; // no primary key, always false
        } elseif (is_array ($this->key)) {
            if (! is_array ($val)) {
                return false; // must be an array
            }
            foreach ($val as $k => $v) {
                if (! in_array ($k, $this->key)) {
                    return false; // has extra fields
                }
            }
            foreach ($this->key as $k => $v) {
                if (! isset ($val[$k])) {
                    return false; // missing key field
                }
            }
        }
        return true;
    }
    

    Note: I didn't test the above, just wrote it super quick :P

    This way, you could check validity via:

    if ($this->_is_valid_key ($vals)) {
        // works as a primary key
    }
    

    For reference, here's a little sample model I created to reason about these changes. Thought it might help to think of it in real use terms :)

    <?php
    
    /**
     * Schema (assumes `#prefix#user` and `#prefix#book` tables):
     *
     *     create table #prefix#book_list (
     *         user int not null,            -- #prefix#user.id
     *         book int not null,            -- #prefix#book.id
     *         added datetime not null,
     *         notes text not null,
     *         primary key (user, book),
     *         index (user, added),
     *         index (book)
     *     );
     */
    class BookList extends Model {
        public $table = '#prefix#book_list';
        public $key = array ('user', 'book');
    }
    
    // Common ways of calling this model might be:
    
    // Create a new, empty object and fill it
    $booklist = new BookList ();
    $booklist->user = User::val ('id');
    $booklist->book = $book->id;
    // etc.
    
    // Create a new, pre-filled object
    $booklist = new BookList (array (
        'user' => User::val ('id'),
        'book' => $book->id,
        'added' => gmdate ('Y-m-d H:i:s'),
        'notes' => 'All-time fav!'
    ));
    
    // Fetch an existing object
    $booklist = new BookList (array ('user' => User::val ('id'), 'book' => $book->id));
    
    // Fetch all books a user has added
    $booklist = BookList::query ()
        ->where ('user', User::val ('id'))
        ->order ('added', 'desc')
        ->fetch ();
    
    // Fetch all users who have added a book to their list
    $booklist = BookList::query ()
        ->where ('book', $book->id)
        ->fetch ();
    

    I think overall it looks like a good start, and would be nice to see support for this. Gotta run, but wanted to get back to you sooner than later! :)

  • edited January 2015

    Pushing forward with the multi key state support, I've tackled the put() method and would like some feed back on it. (made a gist, so the thread doesn't get cluttered) https://gist.github.com/BronyBorn/b9fe291a98132d2f3eef I also updated the __construct() method with the notes you mentioned. Added a slightly different helper function which is also in the gist.

  • Added a proposed change to the field() method in the gist.

  • edited January 2015

    Made changes to the remove() and get() methods. The gist is updated with the code.

    I think I've pretty much got the array-key support figured out. My main concern is how to handle some of the no-key data queries. I made some notes in the gist about maybe checking for any existing ->where() clauses and using them to poll for fields as a possible way to handle them, but for now they set an error and return.

    I think I got all of the applicable methods...

  • Nope, forgot about the __call() method with the cross model references and I have no idea how to handle that...

  • Can we move this to a pull request? It's hard to compare the gist against the whole Model.php file. Thanks :)

Sign In or Register to comment.