Skip to content
Snippets Groups Projects
SimpleORMapCollection.class.php 8.29 KiB
Newer Older
<?php
/**
 * SimpleORMapCollection.class.php
 * simple object-relational mapping
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      André Noack <noack@data-quest.de>
 * @copyright   2012 Stud.IP Core-Group
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 *
 * @template T of SimpleORMap
class SimpleORMapCollection extends SimpleCollection
{
    /**
     * @var int Exception error code denoting a wrong type of objects.
     */
    const WRONG_OBJECT_TYPE = 1;

    /**
     * @var int Exception error code denoting that an object of this `id` already exists.
     */
    const OBJECT_EXISTS = 2;

    /**
     * the record object this collection belongs to
     *
     */
    protected $related_record;

    /**
     * relation options
     * @var array
     */
    protected $relation_options = [];

    /**
     * creates a collection from an array of objects
     * all objects should be of the same type
     *
     * @throws InvalidArgumentException if first entry is not SimpleOrMap
     * @param T[] $data array with SimpleORMap objects
     * @param bool $strict check every element for correct type and unique pk
     * @return SimpleORMapCollection<T>
    public static function createFromArray(array $data, $strict = true)
    {
        $ret = new SimpleORMapCollection();
        if (count($data)) {
            $first = current($data);
            if ($first instanceof SimpleORMap) {
                $ret->setClassName(get_class($first));
                if ($strict) {
                    foreach ($data as $one) {
                        $ret[] = $one;
                    }
                } else {
                    $ret->exchangeArray($data);
                }
            } else {
                throw new InvalidArgumentException('This collection only accepts objects derived from SimpleORMap', self::WRONG_OBJECT_TYPE);
            }
        }
        return $ret;
    }

    /**
     * Constructor
     *
     * @param ?Closure         $finder  callable to fill collection
     * @param ?array           $options relationship options
     * @param SimpleORMap|null $record  related record
    public function __construct(Closure $finder = null, array $options = null, SimpleORMap $record = null)
    {
        $this->relation_options = $options;
        $this->related_record = $record;
        parent::__construct($finder === null ? [] : $finder);
    }

    /**
     * Sets the value at the specified index
     * checks if the value is an object of specified class
     *
     * @see ArrayObject::offsetSet()
     * @throws InvalidArgumentException if the given model does not fit (wrong type or id)
     */
    public function offsetSet($index, $newval): void
    {
        if (!is_null($index)) {
            $index = (int)$index;
        }
        if (!is_a($newval, $this->getClassName())) {
            throw new InvalidArgumentException('This collection only accepts objects of type: ' .  $this->getClassName(), self::WRONG_OBJECT_TYPE);
        }
        if ($this->related_record && $this->relation_options['type'] === 'has_many') {
            $foreign_key_value = call_user_func($this->relation_options['assoc_func_params_func'], $this->related_record);
            call_user_func($this->relation_options['assoc_foreign_key_setter'], $newval, $foreign_key_value);
        }
        if ($newval->id !== null) {
            $exists = $this->find($newval->id);
            if ($exists) {
                throw new InvalidArgumentException('Element could not be appended, element with id: ' . $exists->id . ' is in the way', self::OBJECT_EXISTS);
            }
        }
    }

    /**
     * sets the allowed class name
     */
    public function setClassName($class_name)
    {
        $this->relation_options['class_name'] = strtolower($class_name);
        $this->deleted->relation_options['class_name'] = strtolower($class_name);
    }

    /**
     * sets the related record
     *
     * @param SimpleORMap $record
     */
    public function setRelatedRecord(SimpleORMap $record)
    {
        $this->related_record = $record;
    }

    /**
     * gets the allowed classname
     *
     * @return string
     */
    public function getClassName()
    {
        return strtolower($this->relation_options['class_name']);
    }

    /**
     * reloads the elements of the collection
     * by calling the finder function
     *
     * @throws InvalidArgumentException
     */
    public function refresh()
    {
        if (is_callable($this->finder)) {
            $data = call_user_func($this->finder, $this->related_record);
            foreach ($data as $one) {
                if (!is_a($one, $this->getClassName())) {
                    throw new InvalidArgumentException('This collection only accepts objects of type: ' .  $this->getClassName(), self::WRONG_OBJECT_TYPE);
                }
            }
            $this->exchangeArray($data);
            $this->deleted->exchangeArray([]);
            return $this->last_count = $this->count();
        }
    }

    /**
     * returns element with given primary key value
     *
     * @param string $value primary key value to search for
     */
    public function find($value)
    {
        return $this->findOneBy('id', $value);
    }

    /**
     * returns the collection as grouped array
     * first param is the column to group by, it becomes the key in
     * the resulting array, default is pk. Limit returned fields with second param
     * The grouped entries can optoionally go through the given
     * callback. If no callback is provided, only the first grouped
     * entry is returned, suitable for grouping by unique column
     *
     * @param string $group_by the column to group by, pk if ommitted
     * @param mixed $only_these_fields limit returned fields
     * @param ?callable $group_func closure to aggregate grouped entries
     * @return array assoc array
     */
    public function toGroupedArray($group_by = 'id', $only_these_fields = null, callable $group_func = null)
    {
        $result = [];
        foreach ($this as $record) {
            $key = $record->getValue($group_by);
            if (is_array($key)) {
                $key = join('_', $key);
            }
            $result[$key][] = $record->toArray($only_these_fields);
        }
        if ($group_func === null) {
            $group_func = 'current';
        }
        return array_map($group_func, $result);
    }

    /**
     * mark element(s) for deletion
     * element(s) with given primary key are moved to
     * internal deleted collection
     *
     * @param string $id primary key of element
     * @return int number of unsetted elements
     */
    public function unsetByPk($id)
    {
        return $this->unsetBy('id', $id);
    }

    /**
     * merge in another collection, elements must be of
     * the same type, if an element already exists it is
     * replaced or ignored depending on second param
     *
     * @param SimpleORMapCollection $a_collection
     * @param string $mode 'replace' or 'ignore'
    public function merge(SimpleCollection $a_collection, string $mode = 'ignore')
    {
        $mode = func_get_arg(1);
        foreach ($a_collection as $element) {
            try {
                /**
                 * @throws InvalidArgumentException
                 * @see SimpleORMapCollection::offsetSet()
                 */
                $this[] = $element;
            } catch (InvalidArgumentException $e) {
                if ($e->getCode() === self::OBJECT_EXISTS) {
                    if ($mode === 'replace') {
                        $this->unsetByPk($element->id);
                        $this[] = $element;
                    } // else $mode means 'ignore'
                } else {
                    throw $e;
                }
            }
        }
        $this->storage = array_values($this->storage);
    }