luyadev/luya-testsuite

View on GitHub
src/fixtures/ActiveRecordFixture.php

Summary

Maintainability
B
4 hrs
Test Coverage
A
96%
<?php

namespace luya\testsuite\fixtures;

use yii\test\ActiveFixture;
use luya\helpers\ArrayHelper;
use yii\db\sqlite\Schema;

/**
 * Active Record Fixture.
 *
 * Provides a very basic way to generate a database schema for the given table based on the rules or via
 * $schema definition. So you don't have to create migrations or sql files. Just provide the $modelClass
 * to the ActiveRecord you'd like to test.
 *
 * When working with NgRestModels you can also use {{luya\testsuite\fixtures\NgRestModelFixture}} or maybe
 * checkout {{luya\testsuite\cases\NgRestTestCase}} which makes model testing even more easy, as it will auto
 * setup the sqlite connection.
 *
 * The only requirement for the ActiveRecordFixture is to have the sqlite database
 * connection enabled. You could use {{luya\testsuite\cases\NgRestTestCase}} which ensures
 * this behavior by default.
 *
 * ```php
 * 'components' => [
 *     'db' => [
 *         'class' => 'yii\db\Connection',
 *         'dsn' => 'sqlite::memory:',
 *     ]
 * ]
 * ```
 *
 * ActiveRecordFixture usage example:
 *
 * ```php
 * $fixture = new ActiveRecordFixture([
 *     'modelClass' => 'luya\testsuite\tests\data\TestModel', // path to the model
 *     'fixtureData' => ['model1' => [
 *         'id' => 1,
 *         'user_id' => 1,
 *         'group_id' => 1,
 *     ]]
 * ]);
 *
 * // insert new model
 * $newModel = $fixture->newModel;
 * $newModel->attributes = ['user_id' => 1, 'group_id' => '1];
 * $newModel->save();
 *
 * // or return the defined $fixtureData model
 * $model = $fixture->getModel('model1'); // definde in `$fixtureData`
 * ```
 *
 * By default the primary key `id` is used as (in general) the primary key wont appear in the
 * rules defintion. You can override the default primary key defintion with the $primarKey property:
 *
 * ```php
 * $model = new ActiveRecordFixture([
 *     'modelClass' => 'luya\testsuite\tests\data\TestModel',
 *     'primaryKey' => ['my_primary_key' => 'int(11) PRIMARY KEY'],
 *     'fixtureData' => ['model1' => [
 *          'id' => 1,
 *          'user_id' => 1,
 *          'group_id1' => 1,
 *     ]]
 * ]);
 * ```
 *
 * > In order to support compisite primary keys on sqlite you have to define primaryKey property as followed:
 * > `'primaryKey' => ['user_id' => 'int(11)', 'group_id' => 'int(11)', 'PRIMARY KEY (user_id, group_id)']`
 * 
 * An example which provides data and schema from a extended fixture class:
 * 
 * ```php
 * class MyApiModelFixture extends ActiveRecordFixture
 * {
 *     public $modelClass = MyApiModel::class;
 * 
 *     public function getSchema()
 *     {
 *         return [
 *             'id' => 'int(11)',
 *             'access_token' => 'text',
 *             'name' => 'text',
 *         ];
 *     }
 * 
 *     public function getData()
 *     {
 *         return [
 *             1 => [
 *                 'id' => 1,
 *                 'access_token' => '123123123',
 *                 'name' => 'masterapi',
 *             ]
 *         ];
 *     }
 * }
 * ```
 *
 * @property array $schema
 * @property array $primaryKey
 * @property array $data
 * @property \yii\db\ActiveRecord $newModel
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.10
 */
class ActiveRecordFixture extends ActiveFixture
{
    const RULE_TYPE_BOOLEAN = 'boolean';
    const RULE_TYPE_INTEGER = 'integer';
    const RULE_TYPE_SAFE = 'safe';

    /**
     * @var array An array with columns which will be ignored when creating the table columns.
     * @since 1.0.13.1
     */
    public $ignoreColumns = [];

    /**
     * @var boolean Disabled the default behavior to remove safe attributes as wrongly introduced in 1.0.13. By default this behavior must be disable. If enabled
     * all safe attribute rules column will ignored when creating the table.
     * @since 1.0.13.1
     */
    public $removeSafeAttributes = false;

    /**
     * @var boolean If the table already exists while creating, the creation process will be skipped. This can be the case if any other test
     * created the table but cleanup was not executed therefore an "table exists already" exception can be prevented.
     * @since 1.0.16
     */
    public $skipIfExists = true;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        $this->build();
    }

    /**
     * Create table, create columns from schema and load fixture data.
     *
     * @since 1.0.16
     */
    public function build()
    {
        // create table schema
        $this->createTable();
        // create
        $this->createColumns();
        // load fixture data into the model
        $this->load();
    }
    
    /**
     * Cleanup (drop table) and then rebuild (create) the table.
     *
     * @since 1.0.15
     * @see {{build()}}
     * @see {{cleanup()}}
     */
    public function rebuild()
    {
        $this->cleanup();
        $this->build();
    }

    /**
     * Create instance of the model class.
     *
     * @return \yii\db\ActiveRecord
     */
    public function getNewModel()
    {
        $class = $this->modelClass;
        
        return new $class();
    }
    
    private $_primaryKey;
    
    /**
     * Example
     *
     * ```
     * 'primaryKey' => ['id' => 'INT(11) PRIMARY KEY],
     * ```
     *
     * @param array $primaryKey
     */
    public function setPrimaryKey(array $primaryKey)
    {
        $this->_primaryKey = $primaryKey;
    }
    
    /**
     * Returns the primary key name(s) for this AR class.
     * The default implementation will return the primary key(s) as declared
     * in the DB table that is associated with this AR class.
     *
     * If the DB table does not declare any primary key, you should override
     * this method to return the attributes that you want to use as primary keys
     * for this AR class.
     *
     * Note that an array should be returned even for a table with single primary key.
     *
     * @return string|array the primary keys of the associated database table.
     */
    public function getPrimaryKey()
    {
        if ($this->_primaryKey === null) {
            $this->_primaryKey = ['id' => Schema::TYPE_PK];
        }
        
        return $this->_primaryKey;
    }
    
    private $_data = [];
    
    /**
     *
     * @param array $data
     */
    public function setFixtureData(array $data)
    {
        $this->_data = $data;
    }
    
    /**
     *
     * {@inheritDoc}
     * @see \yii\test\ActiveFixture::getData()
     */
    public function getData()
    {
        return $this->_data;
    }
    
    private $_schema;
    
    /**
     *
     * @param array $schema
     */
    public function setSchema(array $schema)
    {
        $this->_schema = $schema;
    }
    
    public function getSchema()
    {
        // this allows even empty arrays to override.
        if (empty($this->_schema)) {
            $this->_schema = $this->createSchemaFromRules();
        }

        return $this->_schema;
    }
    
    /**
     * Create the database scheme based on the rules as attributes and fields are not available
     * as they are virtual properties from the table definition.
     */
    public function createSchemaFromRules()
    {
        $object = $this->getNewModel();
        
        $fields = [];
        foreach ($object->rules() as $row) {
            list($attributes, $rule) = $row;
            
            // if a none scalar value like callable or object, skip this rule
            if (!is_scalar($rule)) {
                continue;
            }

            $rule = strtolower($rule);

            foreach ((array) $attributes as $name) {
                if ($rule == self::RULE_TYPE_BOOLEAN) {
                    $fields[$name] = Schema::TYPE_BOOLEAN;
                } elseif ($rule == self::RULE_TYPE_INTEGER) {
                    $fields[$name] = Schema::TYPE_INTEGER;
                }

                if (!isset($fields[$name])) {
                    $fields[$name] = Schema::TYPE_TEXT;
                }

                if ($this->removeSafeAttributes && $rule == self::RULE_TYPE_SAFE) {
                    // remove safe validators fields as they are commonly used for setter getter
                    if (isset($fields[$name])) {
                        unset($fields[$name]);
                    }
                }
            }
        }
        
        // remove primary keys
        foreach ($this->primaryKey as $key => $value) {
            ArrayHelper::remove($fields, $key);
        }

        foreach ($fields as $name => $type) {
            if (in_array($name, $this->ignoreColumns)) {
                unset($fields[$name]);
            }
        }
        
        // try to find from labels
        return $fields;
    }

    /**
     * Decided whether table name should be resolved from property or model (if $tableName is empty).
     *
     * @return string
     * @since 1.0.23
     */
    public function getTableName()
    {
        $class = $this->modelClass;
        return $this->tableName ? $this->tableName : $class::tableName();
    }
    
    /**
     * Create the table based on the schema with primary keys
     */
    public function createTable()
    {
        $fields = [];
        
        foreach ($this->getPrimaryKey() as $key => $value) {
            $fields[$key] = $value;
        }
        // if skipIfExists is enabled, the table will only be created when the table does notexists.
        if ($this->skipIfExists) {
            if (!$this->db->schema->getTableSchema($this->getTableName())) {
                $this->db->createCommand()->createTable($this->getTableName(), $fields)->execute();
            }
        } else {
            $this->db->createCommand()->createTable($this->getTableName(), $fields)->execute();
        }
    }
    
    /**
     * Add columns to table.
     */
    public function createColumns()
    {
        foreach ($this->getSchema() as $column => $type) {
            $tableColumns = $this->db->schema->getTableSchema($this->getTableName(), true);
            if (!$tableColumns->getColumn($column)) {
                $this->db->createCommand()->addColumn($this->getTableName(), $column, $type)->execute();
            }
        }
    }
    
    /**
     * Cleanup active record fixture.
     */
    public function cleanup()
    {
        $class = $this->modelClass;
        $tableName = $this->getTableName();
        // ensure the table exists before dropping
        if ($this->db->schema->getTableSchema($tableName)) {
            $this->db->createCommand()->dropTable($tableName)->execute();
        }
    }
}