nrawe/wabi-orm

View on GitHub
library/model.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
declare(strict_types=1);

/**
 * This subpackage provides helpers for working with models.
 */
namespace WabiORM;

use function ICanBoogie\{pluralize, singularize};

/**
 * Resource which contains information about a model.
 */
interface ModelInfoInterface {
    /**
     * The primary key of the model.
     *
     * @return string
     */
    public function primaryKey(): string;

    /**
     * The key to use when looking for the model in other tables.
     *
     * @return string
     */
    public function relationKey(): string;

    /**
     * The table name for the model.
     *
     * @return string
     */
    public function tableName(): string;
}

/**
 * {@inheritDoc}
 */
final class ModelInfo implements ModelInfoInterface {

    /**
     * The primary key of the model.
     *
     * @var string
     */
    protected $primaryKey;

    /**
     * The key to use when looking for the model in other tables.
     *
     * @var string
     */
    protected $relationKey;

    /**
     * The table name for the model.
     *
     * @var string
     */
    protected $tableName;

    /**
     * Creates a new ModelInfo object.
     *
     * @param string ...$params The fields which make up the ModelInfo.
     */
    public function __construct(array $params) {
        foreach ($params as $key => $value) {
            if (\property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public function primaryKey(): string {
        return $this->primaryKey;
    }

    /**
     * {@inheritDoc}
     */
    public function relationKey(): string {
        return $this->relationKey;
    }

    /**
     * {@inheritDoc}
     */
    public function tableName(): string {
        return $this->tableName;
    }
}

/**
 * Uses reflection to create a new model instance.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param string $model
 * @param bool $withConstructor
 * @return object
 */
function create_model(string $model, bool $withConstructor = true): object {
    invariant(
        \class_exists($model),
        'create_model() is unable to create instances for non-existant models'
    );

    $rc = new \ReflectionClass($model);

    invariant(
        !$withConstructor || $rc->getConstructor()->getNumberOfRequiredParameters() === 0,
        'create_model() cannot create instances of models with required constructor arguments'
    );

    return $withConstructor
        ? $rc->newInstance()
        : $rc->newInstanceWithoutConstructor();
}

/**
 * Returns whether the model should be considered to have been persisted.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return boolean
 */
function is_persisted(object $model): bool {
    $info = model_info_cached($model);

    return !is_null($model->{$info->primaryKey()});
}

/**
 * Returns the data currently contained in the model.
 * 
 * This can be configured per-model through the presence of a
 * `withDataForPersistence` method, which should return an array of key/value
 * pairs.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return array
 */
function model_data(object $model): array {
    $data = \get_object_vars($model);

    if (\method_exists($model, 'withDataForPersistence')) {
        $data = $model->withDataForPersistence();
    }

    invariant(
        \is_array($data),
        'model_data() was unable to determine the data for the given model'
    );

    return $data;
}

/**
 * Returns a q() compatible data structure for a delete from the database.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return array
 */
function model_data_for_delete(object $model): array {
    $info = model_info_cached($model);
    $data = model_data($model);

    $id = $data[$info->primaryKey()];

    return [
        'id' => $id,
        'key' => $info->primaryKey(),
        'table' => $info->tableName(),
    ];
}

/**
 * Returns a q() compatible data structure for a insert into the database.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return array
 */
function model_data_for_insert(object $model): array {
    $info = model_info_cached($model);
    $data = model_data($model);

    unset($data[$info->primaryKey()]);

    return [
        'table' => $info->tableName(),
        'fields' => \array_keys($data),
        'values' => \array_values($data),
    ];
}

/**
 * Returns a q() compatible data structure for a updating a model in the
 * database.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return array
 */
function model_data_for_update(object $model): array {
    $info = model_info_cached($model);
    $data = model_data($model);

    $primaryKey = $info->primaryKey();
    $id = $data[$primaryKey];

    unset($data[$primaryKey]);

    return [
        'fields' => $data,
        'id' => $id,
        'key' => $primaryKey,
        'table' => $info->tableName(),
    ];
}

/**
 * Returns the table name based on the given model.
 * 
 * This is a rather naive implementation based on the general convention of
 * pluralisation.
 *
 * @internal 
 * @subpackage WabiORM.Model
 * @param string|object $model
 * @return string
 */
function model_default_table_name($model): string {
    $base = class_basename($model);

    return pluralize(snake($base));
}

/**
 * Returns the relation key of the model based on its class name.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param string|object $model
 * @return string
 */
function model_default_relation_key($model): string {
    return singularize(snake(class_basename($model))) . '_id';
}

/**
 * Caching wrapper for model_info to reduce runtime expense.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param string|object $model
 * @return ModelInfoInterface
 */
function model_info_cached($model): ModelInfoInterface {
    static $cache = [];

    $key = is_string($model) ? $model : get_class($model);

    if (\array_key_exists($key, $cache)) {
        return $cache[$key];
    }

    return $cache[$key] = model_info($model);
}

/**
 * Helper function which returns an array of meta data ("info") from a model.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param string|object $model
 * @return ModelInfoInterface
 */
function model_info($model): ModelInfoInterface {
    invariant(
        is_string($model) || is_object($model),
        'model_info() can only return data from a class reference or instance'
    );

    if (is_string($model)) {
        return model_info(create_model($model, false));
    }

    $override = model_override($model);

    return new ModelInfo([
        'primaryKey' => $override('withPrimaryKey', 'id'),
        'tableName' => $override('withTableName', model_default_table_name($model)),
        'relationKey' => $override('withRelationKey', model_default_relation_key($model)),
    ]);
}

/**
 * Partial application which can be used to access an override from a model,
 * or return a default.
 *
 * @internal
 * @subpackage WabiORM.Model
 * @param object $model
 * @return callable
 */
function model_override(object $model): callable {
    return function (string $method, string $default) use ($model) {
        if (\method_exists($model, $method)) {
            return $model->$method();
        }

        return $default;
    };
}