Finesse/Wired

View on GitHub
src/Helpers.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php

namespace Finesse\Wired;

use Finesse\MiniDB\Exceptions\DatabaseException as DBDatabaseException;
use Finesse\MiniDB\Exceptions\ExceptionInterface as DBException;
use Finesse\MiniDB\Exceptions\IncorrectQueryException as DBIncorrectQueryException;
use Finesse\MiniDB\Exceptions\InvalidArgumentException as DBInvalidArgumentException;
use Finesse\MiniDB\Exceptions\InvalidReturnValueException as DBInvalidReturnValueException;
use Finesse\Wired\Exceptions\DatabaseException;
use Finesse\Wired\Exceptions\ExceptionInterface;
use Finesse\Wired\Exceptions\IncorrectModelException;
use Finesse\Wired\Exceptions\IncorrectQueryException;
use Finesse\Wired\Exceptions\InvalidArgumentException;
use Finesse\Wired\Exceptions\InvalidReturnValueException;
use Finesse\Wired\Exceptions\NotModelException;

/**
 * Helper functions.
 *
 * @author Surgie
 */
class Helpers
{
    /**
     * Checks that the given class name is a model class.
     *
     * @param string $name Value name
     * @param string $className Class name
     * @throws NotModelException If the class is not a model
     */
    public static function checkModelClass(string $name, string $className)
    {
        if (!is_a($className, ModelInterface::class, true)) {
            throw new NotModelException(sprintf(
                '%s (%s) is not a model class implementation name (%s)',
                $name,
                $className,
                ModelInterface::class
            ));
        }
    }

    /**
     * Checks that the given value is a model of the given class
     *
     * @param ModelInterface|mixed $model The value
     * @param string $expectedClassName Class name
     * @throws NotModelException If the given value is not a model object
     * @throws IncorrectModelException If the given model has a wrong class
     */
    public static function checkModelObjectClass($model, string $expectedClassName)
    {
        if ($model instanceof $expectedClassName) {
            return;
        }

        if ($model instanceof ModelInterface) {
            throw new IncorrectModelException(sprintf(
                'The given model %s is not a %s model',
                get_class($model),
                $expectedClassName
            ));
        }

        throw new NotModelException(sprintf(
            'The given value (%s) is not a model',
            is_object($model) ? get_class($model) : gettype($model)
        ));
    }

    /**
     * Groups array of models by model classes. Checks that the given array values are models.
     *
     * @param ModelInterface[] $models
     * @return ModelInterface[][] The keys are the model class names. Doesn't contain empty arrays.
     * @throws NotModelException
     */
    public static function groupModelsByClass(array $models): array
    {
        $groups = [];

        foreach ($models as $index => $model) {
            if (!($model instanceof ModelInterface)) {
                throw new NotModelException('Argument $models['.$index.'] is not a model');
            }

            $groups[get_class($model)][] = $model;
        }

        return $groups;
    }

    /**
     * Index the given array of objects by an object property.
     *
     * @param array $objects
     * @param string $property
     * @return array The keys are the values of the property
     */
    public static function indexObjectsByProperty(array $objects, string $property): array
    {
        $result = [];

        foreach ($objects as $object) {
            $result[$object->$property] = $object;
        }

        return $result;
    }

    /**
     * Groups array of objects by a property values.
     *
     * @param array $objects
     * @param string $property
     * @param bool $preserveKeys Must the input array keys be preserved in the output arrays
     * @return array[] The keys are the values of the properties. The values are the list of suitable objects.
     */
    public static function groupObjectsByProperty(array $objects, string $property, bool $preserveKeys = false): array
    {
        $groups = [];

        foreach ($objects as $index => $object) {
            $value = $object->$property;
            if ($preserveKeys) {
                $groups[$value][$index] = $object;
            } else {
                $groups[$value][] = $object;
            }
        }

        return $groups;
    }

    /**
     * Groups array of arrays by a key values.
     *
     * @param array[] $arrays
     * @param string $key
     * @param bool $preserveKeys Must the input array keys be preserved in the output arrays
     * @return array[][] The keys are the values of the keys. The values are the list of suitable arrays.
     */
    public static function groupArraysByKey(array $arrays, string $key, bool $preserveKeys = false): array
    {
        $groups = [];

        foreach ($arrays as $index => $array) {
            $value = $array[$key];
            if ($preserveKeys) {
                $groups[$value][$index] = $array;
            } else {
                $groups[$value][] = $array;
            }
        }

        return $groups;
    }

    /**
     * Collects models related models to a plain array. The array has only unique models.
     *
     * @param ModelInterface[] $models The models
     * @param string $relation Relation name from which the relative models should be taken. If you need to collect
     *  relatives from a subrelation, add it's name separated by a dot.
     * @return ModelInterface[]
     */
    public static function collectModelsRelatives(array $models, string $relation): array
    {
        $modelsSet = new \SplObjectStorage;
        foreach ($models as $model) {
            $modelsSet->attach($model);
        }

        foreach (explode('.', $relation) as $relation) {
            $nextModelsSet = new \SplObjectStorage;

            foreach ($models as $model) {
                $relatives = $model->getLoadedRelatives($relation);

                if (is_array($relatives)) {
                    foreach ($relatives as $relative) {
                        // The goal is to get the list of unique objects
                        // We can't index the relatives list by models identifiers because a model can have no identifier
                        // We can't use array_unique because it uses not strict comparison
                        // Using SplObjectStorage is the faster than using in_array or spl_object_hash: https://3v4l.org/h6YA6
                        $nextModelsSet->attach($relative);
                    }
                } elseif ($relatives instanceof ModelInterface) {
                    $nextModelsSet->attach($relatives);
                }
            }

            $models = $nextModelsSet;
        }

        return iterator_to_array($models);
    }

    /**
     * Collects models related models, related models related models and so on to a plain array.
     *
     * @param ModelInterface[] $models The models
     * @param string $relation Relation name from which the relative models should be taken. If you need to collect
     *  relatives from a subrelation, add it's name separated by a dot.
     * @return ModelInterface[]
     */
    public static function collectModelsCyclicRelatives(array $models, string $relation): array
    {
        // The goal is to track the list of unique models
        // We can't index the relatives list by models identifiers because a model can have no identifier
        // Using SplObjectStorage is the faster than using in_array or spl_object_hash: https://3v4l.org/h6YA6
        $relatives = new \SplObjectStorage;

        while ($models) {
            $nextLevelModels = static::collectModelsRelatives($models, $relation);

            foreach ($nextLevelModels as $index => $model) {
                if (isset($relatives[$model])) {
                    // If a related model is already in the relatives list, it's relatives are not collected the
                    // second time. It prevents an infinite loop.
                    unset($nextLevelModels[$index]);
                } else {
                    $relatives->attach($model);
                }
            }

            $models = $nextLevelModels;
        }

        return iterator_to_array($relatives);
    }

    /**
     * Filter relatives lists of a models list.
     *
     * @param ModelInterface[] $models The models list
     * @param string $relation Name of relation which relatives should be filtered
     * @param callable $filter Filter function. Called on each relative model. Takes a relative model as the first
     *  argument and a corresponding model from the list as the second argument. Return values: true — leave the model,
     *  false — remove the model from the relatives list, ModelInterface — replace the relative model with the given
     *  model.
     */
    public static function filterModelsRelatives(array $models, string $relation, callable $filter)
    {
        foreach ($models as $model) {
            static::filterModelRelatives($model, $relation, function (ModelInterface $relative) use ($filter, $model) {
                return $filter($relative, $model);
            });
        }
    }

    /**
     * Filters a model relatives list.
     *
     * @param ModelInterface $model The model
     * @param string $relation Name of relation which relatives should be filtered
     * @param callable $filter Filter function. Called on each relative model. Takes a relative model as the first
     *  argument. Return values: true — leave the model, false — remove the model from the relatives list,
     *  ModelInterface — replace the relative model with the given model.
     */
    public static function filterModelRelatives(ModelInterface $model, string $relation, callable $filter)
    {
        if (!$model->doesHaveLoadedRelatives($relation)) {
            return;
        }

        $relatives = $model->getLoadedRelatives($relation);

        if ($relatives === null) {
            $relatives = [];
            $areRelativesMultiple = false;
        } elseif (is_array($relatives)) {
            $areRelativesMultiple = true;
        } else {
            $relatives = [$relatives];
            $areRelativesMultiple = false;
        }

        /** @var ModelInterface[] $relatives */
        $areRelativesChanged = false;

        foreach ($relatives as $index => $relative) {
            $newRelative = $filter($relative);

            if ($newRelative === true) {
                continue;
            }
            if ($newRelative === false) {
                unset($relatives[$index]);
                $areRelativesChanged = true;
                continue;
            }
            if ($newRelative !== $relative) {
                $relatives[$index] = $newRelative;
                $areRelativesChanged = true;
                continue;
            }
        }

        if ($areRelativesChanged) {
            $model->setLoadedRelatives(
                $relation,
                $areRelativesMultiple ? array_values($relatives) : $relatives[0] ?? null
            );
        }
    }

    /**
     * Convert an array of objects to an array of the objects property values.
     *
     * @param array $objects The objects
     * @param string $property The property name
     * @param bool $ignoreNull Should null values be ignored?
     * @return mixed[] The property values. THe indexes are the same as in the input array.
     */
    public static function getObjectsPropertyValues(array $objects, string $property, bool $ignoreNull = false): array
    {
        $values = [];

        foreach ($objects as $index => $object) {
            if (isset($object->$property)) {
                $values[$index] = $object->$property;
            } elseif (!$ignoreNull) {
                $values[$index] = null;
            }
        }

        return $values;
    }

    /**
     * Turns the given exception to a this package exception (if possible).
     *
     * @param \Throwable $exception
     * @return ExceptionInterface|\Throwable
     */
    public static function wrapException(\Throwable $exception): \Throwable
    {
        if ($exception instanceof ExceptionInterface) {
            return $exception;
        }

        if ($exception instanceof DBException) {
            if ($exception instanceof DBInvalidArgumentException) {
                return new InvalidArgumentException($exception->getMessage(), $exception->getCode(), $exception);
            }
            if ($exception instanceof DBInvalidReturnValueException) {
                return new InvalidReturnValueException($exception->getMessage(), $exception->getCode(), $exception);
            }
            if ($exception instanceof DBIncorrectQueryException) {
                return new IncorrectQueryException($exception->getMessage(), $exception->getCode(), $exception);
            }
            if ($exception instanceof DBDatabaseException) {
                return new DatabaseException($exception->getMessage(), $exception->getCode(), $exception);
            }
        }

        return $exception;
    }

    /**
     * Gets object public properties names and values
     *
     * @param object $object
     * @return array The keys are the properties names and the values are the properties values
     */
    public static function getObjectProperties($object): array
    {
        return get_object_vars($object);
    }

    /**
     * Checks whether an object method can be called (exists and public).
     *
     * Warning! It returns true if a class name and a not-static method name is given. If you know how to fix it, PR or
     * issue is welcome.
     *
     * @param object|string $object An object or a class name
     * @param string $name The method name
     * @return bool
     */
    public static function canCallMethod($object, string $name): bool
    {
        return is_callable([$object, $name]);
    }

    /**
     * Gets the model identifier field name from a model instance, a model class name or a query object
     *
     * @param string|ModelQuery|ModelInterface $hint
     * @return string
     * @throws IncorrectQueryException
     * @throws InvalidArgumentException
     * @throws NotModelException
     */
    public static function getModelIdentifierField($hint): string
    {
        if ($hint instanceof ModelInterface) {
            return $hint::getIdentifierField();
        }

        if ($hint instanceof ModelQuery) {
            $hint = $hint->getModelClass();
            if ($hint === null) {
                throw new IncorrectQueryException("The given query doesn't have a context model");
            }
        }

        if (is_string($hint)) {
            static::checkModelClass('The given string', $hint);
            /** @var ModelInterface $hint */
            return $hint::getIdentifierField();
        }

        throw new InvalidArgumentException(sprintf(
            'The argument expected to be a %s object, a % object or a model class name, %s given',
            ModelQuery::class,
            ModelInterface::class,
            is_object($hint) ? get_class($hint) : gettype($hint)
        ));
    }

    /**
     * Returns items of the $new array that are not presented in the $old array or which values differ
     *
     * @param array $old
     * @param array $new
     * @return array The keys are from the $new array, as well as the values
     */
    public static function getFieldsToUpdate(array $old, array $new): array
    {
        $changed = [];

        foreach ($new as $key => $value) {
            if (!array_key_exists($key, $old) || $value !== $old[$key]) {
                $changed[$key] = $value;
            }
        }

        return $changed;
    }
}