Which is the best method to populate a dropdown?

DATABASE

item

int id
int category_id
varchar(255) name

category

int id
varchar(255) name

Implementation 1

ItemController.php

public function actionCreate()
{
...
$categories = ArrayHelper::map(Category::find()->all(), 'id', 'name');
return $this->render('create', [
            'model' => $model,
            'categories' => $categories,
        ]);
}

views/item/_form.php

<?= $form->field($model, 'category_id')->dropDownList($categories, ['prompt' => 'Select...']); ?>

Implementation 2

Item.php

...
public function getCategoryOptions()
{
    return ArrayHelper::map(Category::find()->all(), 'id', 'name');
}

views/item/_form.php

<?= $form->field($model, 'category_id')->dropDownList($model->categoryOptions, 'id', 'name'), ['prompt' => 'Select...']); ?>

Implementation 3

views/item/_form.php

<?= $form->field($model, 'category_id')->dropDownList(ArrayHelper::map(Category::find()->all(), 'id', 'name'), ['prompt' => 'Select...']); ?>

Personally, I’d prefer variant 2.

2 Likes

Should create this function in Category model. Make it static function and reuse everywhere.

Arrayhelper not needed

Category::find()->select('name')->indexBy('id')->column();
5 Likes

I have Trait who implements this type of method for any ActiveRecord model class.

2 Likes

It’s very interesting. Could you show us how you do it?

1 Like

@TomaszKane that will be interesting to see, can you share how you do it with trait?

1 Like

It’s something like this:

<?php

namespace alterpage\application\traits;

use alterpage\helpers\ArrayHelper;
use yii\base\InvalidConfigException;
use yii\caching\DbDependency;
use yii\caching\Dependency;
use yii\db\ActiveRecord;
use yii\db\ExpressionInterface;


trait ModelMapTrait
{

    protected static $modelMapValueColumn;


    /**
     * @param string|\Closure $valueColumn
     * @param string|array|ExpressionInterface $whereCondition
     *
     * @return array
     *
     * @throws InvalidConfigException
     */
    public static function getMap($valueColumn = null, $whereCondition = null): array
    {
        if (is_null($valueColumn)) {
            $valueColumn = static::getModelMapValueColumn();
        }
        $primaryKey = current(self::primaryKey());
        $query = self::find();
        if ($whereCondition) {
            $query->where($whereCondition);
        }
        $array = ArrayHelper::map(
                $query
                ->asArray()
                ->cache(static::getModelMapCacheTime(), static::getModelMapCacheDependency())
                ->all(),
            $primaryKey,
            $valueColumn
        );

        return $array;
    }


    /**
     * @return int Cache time in seconds
     */
    protected static function getModelMapCacheTime(): int
    {
        return 3600;
    }

    /**
     * @return string|\Closure "Name" column
     *
     * @throws InvalidConfigException
     */
    protected static function getModelMapValueColumn()
    {
        if (empty(self::$modelMapValueColumn)) {
            /** @var ActiveRecord $model */
            $model = new self;
            if ($model->hasAttribute('system_name')) {
                self::$modelMapValueColumn = 'system_name';
            } elseif ($model->hasAttribute('name')) {
                self::$modelMapValueColumn = 'name';
            } elseif ($model->hasAttribute('title')) {
                self::$modelMapValueColumn = 'title';
            } else {
                throw new InvalidConfigException('ModelMapValueColumn was not configured.');
            }
        }

        return self::$modelMapValueColumn;
    }

    protected static function getModelMapCacheDependency(): Dependency
    {
        return new DbDependency(['sql' => 'SELECT max(updated_at) FROM ' . self::tableName()]);
    }

}

In ActiveRecord model:

class Product extends ActiveRecord
{
    use ModelMapTrait;
}

Usage:

Product::getMap()

If needs override in Product model cache time, dependency or $modelMapValueColumn.
99,5% of my models work with it.

5 Likes

@TomaszKane Nice! Well thought trait. Thank you for sharing it.

This is the best method:

Model:

public function getCategoryOptions()
{
    return ArrayHelper::map(Category::find()->all(), 'id', 'name');
}

Controller:

public function actionCreate()
{
    $model = new Product();
    $categories = $model->getCategoryOptions();    

    return $this->render('create', [
        'model' => $model,
        'categories' => $categories,
    ]);
}

View:

<?= $form->field($model, 'category_id')->dropDownList($categories, ['prompt' => 'Select...']); ?>

You should only be passing in data to your view file, you should not be making any model calls from your view file. That way, when you decide to swap your view layer to something different, or if you want to return JSON, then you already have all the data you need.

2 Likes

Your best method required map method which is not needed for normal queries. What i need to do if i want to use this method outside Product model? Create object of product model there? :grinning:

Thanks everyone for the answers!
I have adopted the following solution:

Category.php

public static function getOptions()
{
	return self::find()->select('name')->indexBy('id')->column();
}

ItemController.php

public function actionCreate()
{
...
$categoryOptions = Category::getOptions();
...
return $this->render('create', [
            'model' => $model,
            'categoryOptions' => $categoryOptions,
        ]);
}

_form.php

<?= $form->field($model, 'category_id')->dropDownList($categoryOptions, ['prompt' => 'Select...']); ?>
1 Like