Yii2 REST query
UPDATE: Apr 29 2016
This is one more approach simpler than the one I introduced in the previous update. It is always about involving the Search class generated by gii. I like using it to define and maintain all the search related logic in a single place like using custom scenarios, handle validations, or involve related models on the filtering process (like in this example). So I'm going back to my first answer :
public function actions()
{
$actions = parent::actions();
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider()
{
$searchModel = new \app\models\ProductSearch();
return $searchModel->search(\Yii::$app->request->queryParams);
}
Then be sure that your search class is using load($params,'')
instead of load($params)
or alternatively add this to the model class:
class Product extends \yii\db\ActiveRecord
{
public function formName()
{
return '';
}
That should be enough to have your requests looking like:
/products?name=iphone&status=available&sort=name,-price
UPDATE: Sep 23 2015
This is the same approach but by implementing a complete & cleaner solution :
namespace app\api\modules\v1\controllers;
use yii\rest\ActiveController;
use yii\helpers\ArrayHelper;
use yii\web\BadRequestHttpException;
class ProductController extends ActiveController
{
public $modelClass = 'app\models\Product';
// Some reserved attributes like maybe 'q' for searching all fields at once
// or 'sort' which is already supported by Yii RESTful API
public $reservedParams = ['sort','q'];
public function actions() {
$actions = parent::actions();
// 'prepareDataProvider' is the only function that need to be overridden here
$actions['index']['prepareDataProvider'] = [$this, 'indexDataProvider'];
return $actions;
}
public function indexDataProvider() {
$params = \Yii::$app->request->queryParams;
$model = new $this->modelClass;
// I'm using yii\base\Model::getAttributes() here
// In a real app I'd rather properly assign
// $model->scenario then use $model->safeAttributes() instead
$modelAttr = $model->attributes;
// this will hold filtering attrs pairs ( 'name' => 'value' )
$search = [];
if (!empty($params)) {
foreach ($params as $key => $value) {
// In case if you don't want to allow wired requests
// holding 'objects', 'arrays' or 'resources'
if(!is_scalar($key) or !is_scalar($value)) {
throw new BadRequestHttpException('Bad Request');
}
// if the attr name is not a reserved Keyword like 'q' or 'sort' and
// is matching one of models attributes then we need it to filter results
if (!in_array(strtolower($key), $this->reservedParams)
&& ArrayHelper::keyExists($key, $modelAttr, false)) {
$search[$key] = $value;
}
}
}
// you may implement and return your 'ActiveDataProvider' instance here.
// in my case I prefer using the built in Search Class generated by Gii which is already
// performing validation and using 'like' whenever the attr is expecting a 'string' value.
$searchByAttr['ProductSearch'] = $search;
$searchModel = new \app\models\ProductSearch();
return $searchModel->search($searchByAttr);
}
}
Now your GET request will look like :
/products?name=iphone
Or even like :
/products?name=iphone&status=available&sort=name,-price
-
Note:
If instead of
/products?name=iphone
you are looking for a specific action to handle searching or filtering requests like :/products/search?name=iphone
Then, in the code above you'll need to remove the actions function with all its content :
public function actions() { ... }
rename :
indexDataProvider()
toactionSearch()
& finally add
'extraPatterns' => ['GET search' => 'search']
to your yii\web\UrlManager::rules as described in @KedvesHunor's answer.
Original answer: May 31 2015
There is a short way to do this, if when using Gii to generate CRUD for your model, you defined a Search Model Class, then you can use it to filter results, all you have to do is to override the prepareDataProvider
function of indexAction
to force it return the ActiveDataProvider
instance returned by the search
function of your model search class rather than creating a custom new one.
To resume if your model is Product.php & you generated a ProductSearch.php as a search class to it, then in your Controller you just need to add this :
public function actions() {
$actions = parent::actions();
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider() {
$searchModel = new \app\models\ProductSearch();
return $searchModel->search(\Yii::$app->request->queryParams);
}
Then to filter results, your url may look like this :
api.test.loc/v1/products?ProductSearch[name]=iphone
or even like this :
api.test.loc/v1/products?ProductSearch[available]=1&ProductSearch[name]=iphone
Ok i figured out, just put this in your Controller and modifiy the URL router in config.
public function actionSearch()
{
if (!empty($_GET)) {
$model = new $this->modelClass;
foreach ($_GET as $key => $value) {
if (!$model->hasAttribute($key)) {
throw new \yii\web\HttpException(404, 'Invalid attribute:' . $key);
}
}
try {
$provider = new ActiveDataProvider([
'query' => $model->find()->where($_GET),
'pagination' => false
]);
} catch (Exception $ex) {
throw new \yii\web\HttpException(500, 'Internal server error');
}
if ($provider->getCount() <= 0) {
throw new \yii\web\HttpException(404, 'No entries found with this query string');
} else {
return $provider;
}
} else {
throw new \yii\web\HttpException(400, 'There are no query string');
}
}
And the URL rule (edit)
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => ['v1/product'], 'extraPatterns' => ['GET search' => 'search']],
],
],
I would not recommend to use Superglobals $_GET directly . Instead you can use Yii::$app->request->get()
.
Following is the example how you can create a generic search action and use it in the controller.
At the controller End
public function actions() {
$actions = [
'search' => [
'class' => 'app\[YOUR NAMESPACE]\SearchAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'params' => \Yii::$app->request->get()
],
];
return array_merge(parent::actions(), $actions);
}
public function verbs() {
$verbs = [
'search' => ['GET']
];
return array_merge(parent::verbs(), $verbs);
}
Custom Search Action
<?php
namespace app\[YOUR NAMESPACE];
use Yii;
use yii\data\ActiveDataProvider;
use yii\rest\Action;
class SearchAction extends Action {
/**
* @var callable a PHP callable that will be called to prepare a data provider that
* should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead.
* The signature of the callable should be:
*
* ```php
* function ($action) {
* // $action is the action object currently running
* }
* ```
*
* The callable should return an instance of [[ActiveDataProvider]].
*/
public $prepareDataProvider;
public $params;
/**
* @return ActiveDataProvider
*/
public function run() {
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
/**
* Prepares the data provider that should return the requested collection of the models.
* @return ActiveDataProvider
*/
protected function prepareDataProvider() {
if ($this->prepareDataProvider !== null) {
return call_user_func($this->prepareDataProvider, $this);
}
/**
* @var \yii\db\BaseActiveRecord $modelClass
*/
$modelClass = $this->modelClass;
$model = new $this->modelClass([
]);
$safeAttributes = $model->safeAttributes();
$params = array();
foreach($this->params as $key => $value){
if(in_array($key, $safeAttributes)){
$params[$key] = $value;
}
}
$query = $modelClass::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
if (empty($params)) {
return $dataProvider;
}
foreach ($params as $param => $value) {
$query->andFilterWhere([
$param => $value,
]);
}
return $dataProvider;
}
}