Subversion Repositories web.active

Rev

Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire WireSaveableItemsLookup
 *
 * Provides same functionality as WireSaveableItems except that this class includes joining/modification of a related lookup table
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 *
 */

abstract class WireSaveableItemsLookup extends WireSaveableItems {

    /**
     * If a lookup table should be left joined, this method should return the table name
     *
     */
    abstract public function getLookupTable();

    /**
     * Cache of value returned from getLookupField() method
     * 
     * @var string|null
     * 
     */
    protected $lookupField = null;

    /**
     * If a lookup table should be left joined, this method returns the name of the array field in $data that contains multiple values
     * 
     * i.e. roles_permissions becomes permissions_id if getTable() returns roles
     * Does not need to be overridden unless the table naming structure doesn't follow existing logic.
     *
     */
    public function getLookupField() { 
        if($this->lookupField) return $this->lookupField;
        $lookupTable = $this->getLookupTable();
        if(!$lookupTable) return ''; 
        $this->lookupField = preg_replace('/_?' . $this->getTable() . '_?/', '', $lookupTable) . '_id';
        return $this->lookupField;
    }

    /**
     * Get the DatabaseQuerySelect to perform the load operation of items
     *
     * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all. 
     * @return DatabaseQuerySelect
     *
     */
    protected function getLoadQuery($selectors = null) {
        $query = parent::getLoadQuery($selectors); 
        $database = $this->wire()->database;
        $table = $database->escapeTable($this->getTable());
        $lookupTable = $database->escapeTable($this->getLookupTable()); 
        $lookupField = $database->escapeCol($this->getLookupField()); 
        $query->select("$lookupTable.$lookupField"); // QA 
        $query->leftjoin("$lookupTable ON $lookupTable.{$table}_id=$table.id ")->orderby("sort");
        // $query->leftjoin("$lookupTable ON $lookupTable.{$table}_id=$table.id ")->orderby("$table.id, $lookupTable.sort");
        return $query; 
    }

    /**
     * Load items from the database table and return them in the same type class that getAll() returns
     *
     * A selector string or Selectors may be provided so that this can be used as a find() by descending classes that don't load all items at once.
     *
     * @param WireArray $items
     * @param Selectors|string|null $selectors Selectors or a selector string to find, or NULL to load all.
     * @return WireArray Returns the same type as specified in the getAll() method.
     *
     */
    protected function ___load(WireArray $items, $selectors = null) {

        $useLazy = $this->useLazy();
        $database = $this->wire()->database;
        $query = $this->getLoadQuery($selectors);
        $sql = $query->getQuery();
        
        $this->getLookupField(); // preload
        
        $stmt = $database->prepare($sql);
        $stmt->execute();
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        
        // note: non-use of lazyNameIndex/lazyIdIndex is intentional
    
        foreach($rows as $row) {
            if($useLazy) {
                $this->lazyItems[] = $row;
            } else {
                /** @var HasLookupItems $item */
                $this->initItem($row, $items);
            }
        }

        $stmt->closeCursor();
        $items->setTrackChanges(true);
        
        return $items; 
    }

    /**
     * Create a new Saveable/Lookup item from a raw array ($row) and add it to $items
     *
     * @param array $row
     * @param WireArray|null $items
     * @return Saveable|HasLookupItems|WireData|Wire
     * @since 3.0.194
     *
     */
    protected function initItem(array &$row, WireArray $items = null) {
        
        $lookupField = $this->getLookupField();
        $lookupValue = $row[$lookupField];
        $item = $this->makeBlankItem(); /** @var HasLookupItems $item */
        
        if($items === null) $items = $this->getWireArray();
        
        unset($row[$lookupField]);
        
        $item->addLookupItem($lookupValue, $row);

        foreach($row as $key => $value) {
            $item->$key = $value;
        }
        
        if($this->useLazy) {
            $items->add($item);
            foreach($this->lazyItems as $key => $a) {
                if($a['id'] != $row['id']) continue;
                if(!isset($a[$lookupField])) continue;
                $lookupValue = $a[$lookupField];
                unset($a[$lookupField]); 
                $item->addLookupItem($lookupValue, $a);
                unset($this->lazyItems[$key]);
            }

        } else if($items->has($item)) {
            // LEFT JOIN is adding more elements of the same item, i.e. from lookup table
            // if the item is already present in $items, then use the existing one rather 
            // and throw out the one we just created
            $item = $items->get($item);
            $item->addLookupItem($lookupValue, $row);
        } else {
            // add a new item
            $items->add($item);
        }

        return $item;
    }

    /**
     * Should the given item key/field be saved in the database?
     *
     * Template method used by ___save()
     * 
     * @param string $key
     * @return bool
     *
     */
    protected function saveItemKey($key) {
        if($key == $this->getLookupField()) return false; 
        return parent::saveItemKey($key); 
    }

    /**
     * Save the provided item to database
     * 
     * @param Saveable $item
     * @return bool
     * @throws WireException
     *
     */
    public function ___save(Saveable $item) {

        if(!$item instanceof HasLookupItems) {
            $class = $this->className();
            throw new WireException("$class::save() requires an item that implements HasLookupItems interface");
        }
    
        $database = $this->wire()->database;    
        $lookupTable = $database->escapeTable($this->getLookupTable());
        $lookupField = $database->escapeCol($this->getLookupField());
        $table = $database->escapeTable($this->getTable());
        $item_id = (int) $item->id; 

        if($item_id) {
            $query = $database->prepare("DELETE FROM $lookupTable WHERE {$table}_id=:item_id");
            $query->bindValue(":item_id", $item_id, \PDO::PARAM_INT);
            $query->execute();
        }
            
        $result = parent::___save($item); 
        $item_id = (int) $item->id; // reload, in case it was 0 before

        $sort = 0; 
        if($item_id) {
            $sql = "INSERT INTO $lookupTable SET {$table}_id=:item_id, $lookupField=:value_id, sort=:sort";
            $query = $database->prepare($sql);
            foreach($item->getLookupItems() as $key => $value) {
                $value_id = (int) $value->id;
                $query->bindValue(":item_id", $item_id, \PDO::PARAM_INT);
                $query->bindValue(":value_id", $value_id, \PDO::PARAM_INT);
                $query->bindValue(":sort", $sort, \PDO::PARAM_INT);
                $query->execute();
                $sort++;
            }
            $this->resetTrackChanges();
        }

        return $result; 
    }

    /** 
     * Delete the provided item from the database
     *
     * @param Saveable $item
     * @return bool
     * 
     */
    public function ___delete(Saveable $item) {
        $database = $this->wire()->database;
        $lookupTable = $database->escapeTable($this->getLookupTable()); 
        $table = $database->escapeTable($this->getTable()); 
        $item_id = (int) $item->id; 
        $query = $database->prepare("DELETE FROM $lookupTable WHERE {$table}_id=:item_id"); // QA
        $query->bindValue(":item_id", $item_id, \PDO::PARAM_INT);
        $query->execute();
        return parent::___delete($item); 
    }
    
    /**
     * debugInfo PHP 5.6+ magic method
     *
     * This is used when you print_r() an object instance.
     *
     * @return array
     *
     */
    public function __debugInfo() {
        $info = parent::__debugInfo();
        $info['loaded'] = array_unique($info['loaded']);
        $info['notLoaded'] = array_unique($info['notLoaded']);
        return $info;
    }

}