├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_EN.md ├── composer.json ├── docs └── find.md ├── example-client ├── controllers │ └── RestTestController.php ├── models │ ├── Test.php │ └── TestSearch.php └── views │ └── rest-test │ ├── _form.php │ ├── create.php │ ├── index.php │ ├── update.php │ └── view.php └── src ├── ActiveRecord.php ├── Command.php ├── Connection.php ├── DebugAction.php ├── DebugPanel.php ├── Query.php ├── QueryBuilder.php ├── RestDataProvider.php └── RestQuery.php /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE & OS files 2 | .*.swp 3 | .DS_Store 4 | .buildpath 5 | .idea 6 | .project 7 | .settings 8 | Thumbs.db 9 | nbproject 10 | 11 | # php-cs-fixer cache 12 | .php_cs.cache 13 | 14 | # vendor dirs 15 | vendor 16 | 17 | # composer lock files 18 | composer.lock 19 | 20 | # phpunit generated files 21 | coverage.clover 22 | 23 | # PHARs 24 | php-cs-fixer.phar 25 | phpunit.phar 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | apexwire/yii2-restclient changelog 2 | --------------------------- 3 | 4 | ## 0.5 Under development 5 | 6 | ## 0.4.1 2016-12-27 7 | 8 | - fixBug: namespace yii\restclient? (продолжение). #14 9 | 10 | ## 0.4 2016-11-18 11 | 12 | - fixBug: save custom modelName #12 13 | - Implement `ActiveRecord::populateRecord()` method. #11 14 | - Изменен namespace #13 15 | 16 | ## 0.3 2016-09-23 17 | 18 | - ActiveRecord. Обновлены комментарии. Убрана лишние подключения (use ...). При обработке ошибки в функция insert/updateInternal отслеживаем исключение GuzzleHttp\Exception\ClientException вместо \Exception. 19 | - Command. Обновлены комментарии. Убрана лишние подключения (use ...). Обновлены функции queryAll, queryOne. Функция queryOne теперь поддерживает возмозмость поиска одной записи (через обращение к списку). 20 | - Connection. Обновлены комментарии. Убрана лишние подключения (use ...). 21 | - DebugPanel. Исправлен баг: иногда значение массива $timing[2] может быть строка. Добавлен код определение мтода запроса к странице. Если метод GET то отображаются ссылки "run query", "to new tab", При других методах ссылки не позволяют повторить запрос (поэтому и убраны) 22 | - Query. Добавлена функция prepare(). 23 | - QueryBuilder. Теперь наследуемся от yii\base\Object вместо yii\db\QueryBuilder. В связи с этим добавлены новые параметры, функции и удалены неиспользуемые функции. 24 | - RestDataProvider. Обновлены подключения (use ...). 25 | - RestQuery. Добавлена функция removeDuplicatedModels. Обновлена функция one. Раньше она не корректно обрабатывала код : Contact::find()->where(['email' => $email])->one 26 | - Поиск сломал страницу просмотра #10 27 | 28 | ## 0.2 2016-03-17 29 | 30 | - QueryBuilder. Функции buildLimit и buildOrderBy не поддерживаются и выдают исключение 31 | - QueryBuilder. Функция buildPerPage - устанавливает количество записей на страницу 32 | - QueryBuilder. Функция buildSort (бывшая buildOrderBy) - реализует сортировку записей 33 | - добавлены GET парметры для HEAD запросов 34 | - подправлен DebugAction. Добавлен параметр время выполнения. Параметр time изменен на duration 35 | - DebugPanel корректная обработка ajax ответов. Отображает так же время выполнения. Отображает headers в случае если запрос HEAD. (task #5) 36 | - Query. Удалены параметры $index и $type. Добавлен параметр $searchModel (task #3) 37 | - QueryBuilder. Добавлена функция для обработки условия выборки - buildFind. При использовании функции buildCondition и buildWhere теперь выбраывается исключение 38 | - RestQuery. Генерируем searchModel на основе modelClass. Например если название модели "common\models\User" то название searchModel будет сгенерировано "UserSearch" (потому что в yii2 для поиска используется своя модель) (task #3) 39 | - добавлена папка "example-client". Включает в себя файлы клиентской части: контроллера, двух моделей и представлений 40 | - добавлено описание работы поиска docs/find.md 41 | - удалены старые файлы документации 42 | 43 | ## 0.1 2016-03-11 44 | 45 | Базовая переработка расширения и приведения к стандартному поведению Yii2 Rest. 46 | Изменениям подверглисе все файлы, многое переписано, некоторое добавлено/удалено. 47 | Могут встречаться "артефакты", которые по возможности будут исправлены в будующих версиях. 48 | 49 | - изменены namespace 50 | - удалены не используемые функции и файлы 51 | - добавлены/изменены комментарии 52 | - подправлены DebugPanel и DebugAction 53 | - убрано дублирование переменных 54 | - переработаны метода добавления и редактирования. Добавлен функционал обработки ошибки 422 (ошибка валидации) 55 | - изменены ссылки, по которым идут запросы для API 56 | - изменены файлы changelog.md и readme.md, добавлено русское описание 57 | - и т.д. 58 | 59 | ## Development started 2016-03-03 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2016, ApexWire 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of [Yii Rest Client] nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii2 Rest Client 2 | ===== 3 | 4 | **Инструменты для использования API, как ActiveRecord для Yii2** 5 | 6 | Используйте свой API как ActiveRecord 7 | 8 | ## Установка 9 | 10 | Предпочтительный способ установки расширения через [composer](http://getcomposer.org/download/). 11 | 12 | Запустить 13 | 14 | ```sh 15 | php composer.phar require "apexwire/yii2-restclient" 16 | ``` 17 | 18 | или добавить 19 | 20 | ```json 21 | "apexwire/yii2-restclient": "*" 22 | ``` 23 | 24 | в разделе "require" вашего composer.json 25 | 26 | ## Конфигурация 27 | 28 | Добавьте этот код в ваш файл конфигурации: 29 | 30 | ```php 31 | 'components' => [ 32 | 'restclient' => [ 33 | 'class' => 'apexwire\restclient\Connection', 34 | 'config' => [ 35 | 'base_uri' => 'https://api.site.com/', 36 | ], 37 | ], 38 | ], 39 | ``` 40 | 41 | ## Применение 42 | 43 | Определите свою модель 44 | 45 | ```php 46 | class MyModel extends \apexwire\restclient\ActiveRecord 47 | { 48 | public function attributes() 49 | { 50 | return ['id', 'name', 'status']; 51 | } 52 | } 53 | ``` 54 | 55 | ## Debug 56 | 57 | Пример подключения debug панели 58 | 59 | ```php 60 | $config['modules']['debug'] = [ 61 | 'class' => 'yii\debug\Module', 62 | 'panels' => [ 63 | 'rest' => ['class' => 'apexwire\restclient\DebugPanel'], 64 | ], 65 | ]; 66 | ``` 67 | 68 | ## Возможности 69 | 70 | - можно указать список полей, которые вернутся: `MyModel::find()->select(['id','name'])` 71 | - можно указать лимит: `MyModel::find()->limit(2)` 72 | - поддерживается пагинация 73 | - поддерживается сортировка 74 | - поддерживается поиск. Пример [тут](docs/find.md). 75 | 76 | ## Лицензия 77 | 78 | Этот проект был выпущен под лицензией [BSD-3-Clause](LICENSE). 79 | Подробнее [тут](http://choosealicense.com/licenses/bsd-3-clause). 80 | 81 | Copyright © 2016, ApexWire 82 | 83 | ## Выражение признательности 84 | 85 | - Проект основан на расширении [Yii2 HiArt](https://github.com/hiqdev/yii2-hiart). 86 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | Yii2 Rest Client 2 | ===== 3 | 4 | **Tools to use API as ActiveRecord for Yii2** 5 | 6 | Use your API as ActiveRecord 7 | 8 | ## Installation 9 | 10 | The preferred way to install this yii2-extension is through [composer](http://getcomposer.org/download/). 11 | 12 | Either run 13 | 14 | ```sh 15 | php composer.phar require "apexwire/yii2-restclient" 16 | ``` 17 | 18 | or add 19 | 20 | ```json 21 | "apexwire/yii2-restclient": "*" 22 | ``` 23 | 24 | to the require section of your composer.json. 25 | 26 | ## Configuration 27 | 28 | To use this extension, configure restclient component in your application config: 29 | 30 | ```php 31 | 'components' => [ 32 | 'restclient' => [ 33 | 'class' => 'apexwire\restclient\Connection', 34 | 'config' => [ 35 | 'base_uri' => 'https://api.site.com/', 36 | ], 37 | ], 38 | ], 39 | ``` 40 | 41 | ## Usage 42 | 43 | Define your Model 44 | 45 | ```php 46 | class MyModel extends \apexwire\restclient\ActiveRecord 47 | { 48 | public function attributes() 49 | { 50 | return ['id', 'name', 'status']; 51 | } 52 | } 53 | ``` 54 | 55 | ## Debug 56 | 57 | Connection example debug panel 58 | 59 | ```php 60 | $config['modules']['debug'] = [ 61 | 'class' => 'yii\debug\Module', 62 | 'panels' => [ 63 | 'rest' => ['class' => 'apexwire\restclient\DebugPanel'], 64 | ], 65 | ]; 66 | ``` 67 | 68 | ## License 69 | 70 | This project is released under the terms of the BSD-3-Clause [license](LICENSE). 71 | Read more [here](http://choosealicense.com/licenses/bsd-3-clause). 72 | 73 | Copyright © 2016, ApexWire 74 | 75 | ## Acknowledgments 76 | 77 | - This project is based on [Yii2 HiArt](https://github.com/hiqdev/yii2-hiart). 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apexwire/yii2-restclient", 3 | "type": "yii2-extension", 4 | "description": "Tools to use API as ActiveRecord for Yii2", 5 | "keywords": [ 6 | "api", 7 | "ActiveRecord", 8 | "extension", 9 | "yii2" 10 | ], 11 | "homepage": "https://github.com/apexwire/yii2-restclient", 12 | "license": "BSD-3-Clause", 13 | "support": { 14 | "source": "https://github.com/apexwire/yii2-restclient", 15 | "issues": "https://github.com/apexwire/yii2-restclient/issues", 16 | "wiki": "https://github.com/apexwire/yii2-restclient/wiki" 17 | }, 18 | "authors": [ 19 | { 20 | "name": "ApexWire", 21 | "email": "apexwire@gmail.com" 22 | } 23 | ], 24 | "require": { 25 | "yiisoft/yii2": "~2.0", 26 | "guzzlehttp/guzzle": "6.*" 27 | }, 28 | "require-dev": { 29 | "minii/db": "*@dev", 30 | "minii/helpers": "*@dev" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "apexwire\\restclient\\": "src" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/find.md: -------------------------------------------------------------------------------- 1 | Поиск 2 | ===== 3 | 4 | Расширение Yii2 Rest Client поддерживает поиск по api. 5 | 6 | ## Как устроено 7 | 8 | Реализация поиска аналогична обычному поиску. То есть необходимо реализовать модель поиска, в которой обрабатывается GET параметры. 9 | Пример модели: [TestSearch](../example-client/models/TestSearch.php). 10 | 11 | При поиске по параметру на клиенте, параметры поиска добавляются к api ссылке GET параметрами. Параметры передаются в массиве. 12 | 13 | ## Реализация поиска в серверной части 14 | 15 | В Yii2 Rest пока не реализована поддержка поиска. Поэтому необходимо реализовать поиск самим. 16 | 17 | Для этого необходимо: 18 | 19 | - изменить значение prepareDataProvider в IndexAction 20 | - создать модель для поиска, например "TestSearch" и реализовать в ней статический метод search, входным параметром которой является массив с параметрами 21 | 22 | Переопределяем массив actions, изменив "index", в ApiController. Переменная $searchClass содержит полное название модели поиска. 23 | Если она определена включается поиск. 24 | 25 | ```php 26 | public function actions() 27 | { 28 | $actions = parent::actions(); 29 | 30 | return ArrayHelper::merge($actions, [ 31 | 'index' => [ 32 | 'class' => 'yii\rest\IndexAction', 33 | 'modelClass' => $this->modelClass, 34 | 'prepareDataProvider' => function ($action) { 35 | $modelClass = $action->modelClass; 36 | $searchClass = $this->searchClass; 37 | 38 | $query = ($searchClass) 39 | ? $searchClass::search(\Yii::$app->request->get(mb_substr(mb_strrchr($searchClass, '\\'), 1))) 40 | : $modelClass::find(); 41 | 42 | return new ActiveDataProvider([ 43 | 'query' => $query 44 | ]); 45 | }, 46 | ], 47 | ]); 48 | } 49 | ``` 50 | 51 | `mb_substr(mb_strrchr($searchClass, '\\'), 1)` - Получаем из строки "app\models\TestSearch" название модели "TestSearch" 52 | 53 | Пример функции search в модели TestSearch. 54 | 55 | ```php 56 | public static function search($params = []) 57 | { 58 | $query = parent::find(); 59 | 60 | $query->andFilterWhere([ 61 | 'id' => ArrayHelper::getValue($params, 'id'), 62 | 'status' => ArrayHelper::getValue($params, 'status'), 63 | ]); 64 | 65 | $query->andFilterWhere(['like', 'name', ArrayHelper::getValue($params, 'name')]); 66 | 67 | return $query; 68 | } 69 | ``` 70 | 71 | 72 | ## Важно 73 | 74 | При реализации клиентской и сервервой части следует обратить внимание на: 75 | 76 | - поиск. Название моделей, с помощью которых осушествляется поиск, должны быть идентичны -------------------------------------------------------------------------------- /example-client/controllers/RestTestController.php: -------------------------------------------------------------------------------- 1 | search(\Yii::$app->request->queryParams); 20 | 21 | return $this->render('index', [ 22 | 'searchModel' => $searchModel, 23 | 'dataProvider' => $dataProvider, 24 | ]); 25 | } 26 | 27 | /** 28 | * Displays a single Test model. 29 | * @param string $id 30 | * @return mixed 31 | * @throws NotFoundHttpException 32 | */ 33 | public function actionView($id) 34 | { 35 | return $this->render('view', [ 36 | 'model' => $this->findModel($id), 37 | ]); 38 | } 39 | 40 | /** 41 | * Creates a new Test model. 42 | * If creation is successful, the browser will be redirected to the 'view' page. 43 | * @return mixed 44 | */ 45 | public function actionCreate() 46 | { 47 | $model = new Test(); 48 | 49 | if ($model->load(\Yii::$app->request->post()) && $model->save()) { 50 | return $this->redirect(['view', 'id' => (string)$model->id]); 51 | } else { 52 | return $this->render('create', [ 53 | 'model' => $model, 54 | ]); 55 | } 56 | } 57 | 58 | /** 59 | * Updates an existing Test model. 60 | * If update is successful, the browser will be redirected to the 'view' page. 61 | * @param string $id 62 | * @return string|\yii\web\Response 63 | * @throws NotFoundHttpException 64 | */ 65 | public function actionUpdate($id) 66 | { 67 | $model = $this->findModel($id); 68 | 69 | if ($model->load(\Yii::$app->request->post()) && $model->save()) { 70 | return $this->redirect(['view', 'id' => (string)$model->id]); 71 | } else { 72 | return $this->render('update', [ 73 | 'model' => $model, 74 | ]); 75 | } 76 | } 77 | 78 | /** 79 | * Deletes an existing Block model. 80 | * If deletion is successful, the browser will be redirected to the 'index' page. 81 | * @param integer $id 82 | * @return mixed 83 | */ 84 | public function actionDelete($id) 85 | { 86 | $model = $this->findModel($id); 87 | $model->delete(); 88 | 89 | return $this->redirect(['index']); 90 | } 91 | 92 | /** 93 | * Finds the Test model based on its primary key value. 94 | * If the model is not found, a 404 HTTP exception will be thrown. 95 | * @param string $id 96 | * @return Test the loaded model 97 | * @throws NotFoundHttpException if the model cannot be found 98 | */ 99 | protected function findModel($id) 100 | { 101 | $model = Test::findOne($id); 102 | 103 | if (is_null($model)) { 104 | throw new NotFoundHttpException('The requested page does not exist.'); 105 | } 106 | 107 | return $model; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /example-client/models/Test.php: -------------------------------------------------------------------------------- 1 | 256], 24 | [['name', 'status'], 'safe'], 25 | [['name', 'status'], 'required'], 26 | ]; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function attributes() 33 | { 34 | return [ 35 | 'id', 36 | 'status', 37 | 'name', 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function attributeLabels() 45 | { 46 | return [ 47 | 'id' => 'Id', 48 | 'status' => 'Статус', 49 | 'name' => 'Название', 50 | ]; 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /example-client/models/TestSearch.php: -------------------------------------------------------------------------------- 1 | $query, 25 | ]); 26 | 27 | $this->load($params); 28 | 29 | if (!$this->validate()) { 30 | return $dataProvider; 31 | } 32 | 33 | $query->andFilterWhere([ 34 | 'id' => $this->id, 35 | 'status' => $this->status, 36 | ]); 37 | 38 | $query->andFilterWhere(['like', 'name', $this->name]); 39 | 40 | return $dataProvider; 41 | } 42 | } -------------------------------------------------------------------------------- /example-client/views/rest-test/_form.php: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 | 15 | field($model, 'name')->textInput() ?> 16 | field($model, 'status')->textInput() ?> 17 | 18 |
19 | isNewRecord ? 'Добавить' : 'Обновить', 20 | ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> 21 |
22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /example-client/views/rest-test/create.php: -------------------------------------------------------------------------------- 1 | title = 'Добавить'; 9 | $this->params['breadcrumbs'][] = ['label' => 'Список', 'url' => ['index']]; 10 | $this->params['breadcrumbs'][] = $this->title; 11 | ?> 12 |
13 | 14 |

title) ?>

15 | 16 | render('_form', [ 17 | 'model' => $model, 18 | ]) ?> 19 | 20 |
21 | -------------------------------------------------------------------------------- /example-client/views/rest-test/index.php: -------------------------------------------------------------------------------- 1 | title = 'Список'; 12 | $this->params['breadcrumbs'][] = $this->title; 13 | ?> 14 |
15 | 16 |

title) ?>

17 | 18 |
19 |

20 | 'btn btn-success']) ?> 21 |

22 | 23 | $dataProvider, 25 | 'filterModel' => $searchModel, 26 | 'columns' => [ 27 | ['class' => 'yii\grid\SerialColumn'], 28 | 'name', 29 | 'status', 30 | ['class' => 'yii\grid\ActionColumn'], 31 | ], 32 | ]); ?> 33 | 34 |
35 | -------------------------------------------------------------------------------- /example-client/views/rest-test/update.php: -------------------------------------------------------------------------------- 1 | title = 'Обновить: ' . ' ' . $model->name; 9 | $this->params['breadcrumbs'][] = ['label' => 'Список', 'url' => ['index']]; 10 | $this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; 11 | $this->params['breadcrumbs'][] = 'Обновить'; 12 | ?> 13 |
14 | 15 |

title) ?>

16 | 17 | render('_form', [ 18 | 'model' => $model, 19 | ]) ?> 20 | 21 |
22 | -------------------------------------------------------------------------------- /example-client/views/rest-test/view.php: -------------------------------------------------------------------------------- 1 | title = $model->id; 10 | $this->params['breadcrumbs'][] = ['label' => 'Счета', 'url' => ['index']]; 11 | $this->params['breadcrumbs'][] = $this->title; 12 | ?> 13 |
14 | 15 |

title) ?>

16 | 17 |

18 | $model->id], ['class' => 'btn btn-primary']) ?> 19 | $model->id], [ 20 | 'class' => 'btn btn-danger', 21 | 'data' => [ 22 | 'confirm' => 'Вы уверены что хотите удалить запись?', 23 | 'method' => 'post', 24 | ], 25 | ]) ?> 26 |

27 | 28 | $model, 30 | 'attributes' => [ 31 | 'id', 32 | 'name', 33 | 'status', 34 | ], 35 | ]) ?> 36 | 37 |
38 | -------------------------------------------------------------------------------- /src/ActiveRecord.php: -------------------------------------------------------------------------------- 1 | get(Connection::getDriverName()); 38 | } 39 | 40 | /** 41 | * @inheritdoc 42 | * 43 | * @return RestQuery 44 | */ 45 | public static function find($options = []) 46 | { 47 | $config = [ 48 | 'class' => RestQuery::className(), 49 | 'options' => $options, 50 | ]; 51 | 52 | return \Yii::createObject($config, [get_called_class()]); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public static function findAll($condition, $options = []) 59 | { 60 | return static::find($options)->andWhere($condition)->all(); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public static function primaryKey() 67 | { 68 | return ['id']; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function attributes() 75 | { 76 | throw new InvalidConfigException('The attributes() method of RestClient ActiveRecord has to be implemented by child classes.'); 77 | } 78 | 79 | /** 80 | * @return string the name of the index this record is stored in. 81 | */ 82 | public static function modelName() 83 | { 84 | return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | public function insert($runValidation = true, $attributes = null) 91 | { 92 | if ($runValidation && !$this->validate($attributes)) { 93 | return false; 94 | } 95 | 96 | if (!$this->beforeSave(true)) { 97 | return false; 98 | } 99 | 100 | $values = $this->getDirtyAttributes($attributes); 101 | 102 | try { 103 | $result = static::getDb()->createCommand(['index' => static::modelName()])->insert($values); 104 | 105 | $pk = static::primaryKey()[0]; 106 | $this->$pk = $result['id']; 107 | if ($pk !== 'id') { 108 | $values[$pk] = $result['id']; 109 | } 110 | $changedAttributes = array_fill_keys(array_keys($values), null); 111 | $this->setOldAttributes($values); 112 | $this->afterSave(true, $changedAttributes); 113 | } catch (ClientException $e) { 114 | 115 | if ($e->getCode() == 422) { 116 | $res = $e->getResponse()->getBody()->getContents(); 117 | 118 | if (preg_grep('|application/json|i', $e->getResponse()->getHeader('Content-Type'))) { 119 | $res = Json::decode($res); 120 | 121 | foreach ($res as $error) { 122 | $this->addError($error['field'], $error['message']); 123 | } 124 | 125 | return false; 126 | } else { 127 | throw new HttpException($e->getCode(), 'Не верный формат данных.', $e->getCode()); 128 | } 129 | } else { 130 | throw new \Exception('При создании возникли ошибки', 500, $e); 131 | } 132 | } 133 | 134 | return true; 135 | } 136 | 137 | /** 138 | * @inheritdoc 139 | */ 140 | protected function updateInternal($attributes = null) 141 | { 142 | if (!$this->beforeSave(false)) { 143 | return false; 144 | } 145 | 146 | $values = $this->getAttributes($attributes); 147 | 148 | if (empty($values)) { 149 | $this->afterSave(false, $values); 150 | 151 | return 0; 152 | } 153 | 154 | try { 155 | $result = static::getDb()->createCommand(['index' => static::modelName()])->update( 156 | $this->getOldPrimaryKey(), 157 | $values 158 | ); 159 | 160 | $changedAttributes = []; 161 | foreach ($values as $name => $value) { 162 | $changedAttributes[$name] = $this->getOldAttribute($name); 163 | $this->setOldAttribute($name, $value); 164 | } 165 | 166 | $this->afterSave(false, $changedAttributes); 167 | } catch (ClientException $e) { 168 | 169 | if ($e->getCode() == 422) { 170 | 171 | $res = $e->getResponse()->getBody()->getContents(); 172 | 173 | if (preg_grep('|application/json|i', $e->getResponse()->getHeader('Content-Type'))) { 174 | $res = Json::decode($res); 175 | 176 | foreach ($res as $error) { 177 | $this->addError($error['field'], $error['message']); 178 | } 179 | 180 | return false; 181 | } else { 182 | throw new HttpException($e->getCode(), 'Не верный формат данных.', $e->getCode()); 183 | } 184 | } else { 185 | throw new \Exception('При обновлении возникли ошибки', 500, $e); 186 | } 187 | } 188 | 189 | return $result; 190 | } 191 | 192 | 193 | /** 194 | * @inheritdoc 195 | */ 196 | public function delete($options = []) 197 | { 198 | $result = false; 199 | 200 | try { 201 | if ($this->beforeDelete()) { 202 | 203 | static::getDb()->createCommand(['index' => static::modelName()])->delete( 204 | $this->getOldPrimaryKey(), 205 | $options 206 | ); 207 | 208 | $result = true; 209 | $this->setOldAttributes(null); 210 | $this->afterDelete(); 211 | } 212 | } catch (\Exception $e) { 213 | 214 | if ($e->getCode() == 404) { 215 | throw new NotFoundHttpException('Страница для удаления не найдена.'); 216 | } else { 217 | throw new \Exception('При удалении возникли ошибки', 500, $e); 218 | } 219 | } 220 | 221 | return $result; 222 | } 223 | 224 | /** 225 | * @inheritdoc 226 | */ 227 | public function getIsNewRecord() 228 | { 229 | return !$this->getPrimaryKey(); 230 | } 231 | 232 | /** 233 | * @inheritdoc 234 | */ 235 | public function unlinkAll($name, $delete = false) 236 | { 237 | throw new NotSupportedException('unlinkAll() is not supported by RestClient, use unlink() instead.'); 238 | } 239 | 240 | /** 241 | * @inheritdoc 242 | * @throws \yii\base\UnknownPropertyException 243 | */ 244 | public static function populateRecord($record, $row) 245 | { 246 | $attributes = array_flip($record->attributes()); 247 | foreach ($attributes as $attributeName => $attributeValue) { 248 | if (!array_key_exists($attributeName, $row)) { 249 | throw new UnknownPropertyException("Attribute `{$attributeName}` not found in API response. Available fields: " . implode(', ', array_keys($row)) . '.'); 250 | } 251 | } 252 | parent::populateRecord($record, $row); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | index; 52 | $query = is_array($this->queryParts) ? $this->queryParts : []; 53 | 54 | return $this->db->get($url, $query); 55 | } 56 | 57 | /** 58 | * @return mixed 59 | */ 60 | public function queryOne() 61 | { 62 | /* @var $query RestQuery */ 63 | $query = $this->query; 64 | 65 | /* @var $class ActiveRecord */ 66 | $class = $query->modelClass; 67 | $pks = $class::primaryKey(); 68 | 69 | $url = $this->index; 70 | if (count($pks) == 1) { 71 | $primaryKey = current($pks); 72 | if (count($this->query->where) == 1 && isset($this->query->where[$primaryKey])) { 73 | 74 | return $this->db->get($url . '/' . $this->query->where[$primaryKey]); 75 | } 76 | } 77 | 78 | $query = is_array($this->queryParts) ? $this->queryParts : []; 79 | 80 | return $this->db->get($url, $query); 81 | } 82 | 83 | /** 84 | * CURL function 85 | */ 86 | 87 | /** 88 | * Делаем HEAD запрос 89 | * 90 | * @return mixed 91 | */ 92 | public function head() 93 | { 94 | $query = is_array($this->queryParts) ? $this->queryParts : []; 95 | 96 | return $this->db->head($this->index, $query); 97 | } 98 | 99 | /** 100 | * Запрос на создание 101 | * 102 | * @param array $data 103 | * @param array $options 104 | * @return mixed 105 | */ 106 | public function insert($data = [], $options = []) 107 | { 108 | return $this->db->post($this->index, $options, $data); 109 | } 110 | 111 | /** 112 | * Запрос на обновление 113 | * 114 | * @param $id 115 | * @param array $data 116 | * @param array $options 117 | * @return mixed 118 | */ 119 | public function update($id, $data = [], $options = []) 120 | { 121 | $url = $this->index . '/' . $id; 122 | 123 | return $this->db->put($url, $options, $data); 124 | } 125 | 126 | /** 127 | * Запрос на удаление 128 | * 129 | * @param $id 130 | * @param array $options 131 | * @return mixed 132 | */ 133 | public function delete($id, $options = []) 134 | { 135 | $url = $this->index . '/' . $id; 136 | 137 | return $this->db->delete($url, $options); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | [ 30 | * 'restclient' => [ 31 | * 'class' => 'apexwire\restclient\Connection', 32 | * 'config' => [ 33 | * 'base_uri' => 'https://api.site.com/', 34 | * ], 35 | * ], 36 | * ], 37 | * ``` 38 | */ 39 | class Connection extends Component 40 | { 41 | /** */ 42 | const EVENT_AFTER_OPEN = 'afterOpen'; 43 | 44 | /** 45 | * @var array Config 46 | */ 47 | public $config = []; 48 | 49 | /** 50 | * @var Handler 51 | */ 52 | protected static $_handler = null; 53 | 54 | /** 55 | * @var array authorization config 56 | */ 57 | protected $_auth = []; 58 | 59 | /** 60 | * @var Closure Callback to test if API response has error 61 | * The function signature: `function ($response)` 62 | * Must return `null`, if the response does not contain an error. 63 | */ 64 | protected $_errorChecker; 65 | 66 | /** @type Response */ 67 | protected $_response; 68 | 69 | 70 | public function setAuth($auth) 71 | { 72 | $this->_auth = $auth; 73 | } 74 | 75 | public function getAuth() 76 | { 77 | if ($this->_auth instanceof Closure) { 78 | $this->_auth = call_user_func($this->_auth, $this); 79 | } 80 | 81 | return $this->_auth; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | * @throws InvalidConfigException 87 | */ 88 | public function init() 89 | { 90 | if (!$this->config['base_uri']) { 91 | throw new InvalidConfigException('The `base_uri` config option must be set'); 92 | } 93 | } 94 | 95 | /** 96 | * Closes the connection when this component is being serialized. 97 | * @return array 98 | */ 99 | public function __sleep() 100 | { 101 | return array_keys(get_object_vars($this)); 102 | } 103 | 104 | /** 105 | * Returns the name of the DB driver for the current [[dsn]]. 106 | * 107 | * @return string name of the DB driver 108 | */ 109 | public static function getDriverName() 110 | { 111 | return 'restclient'; 112 | } 113 | 114 | /** 115 | * Creates a command for execution. 116 | * 117 | * @param array $config the configuration for the Command class 118 | * 119 | * @return Command the DB command 120 | */ 121 | public function createCommand($config = []) 122 | { 123 | $config['db'] = $this; 124 | $command = new Command($config); 125 | 126 | return $command; 127 | } 128 | 129 | /** 130 | * Creates new query builder instance. 131 | * 132 | * @return QueryBuilder 133 | */ 134 | public function getQueryBuilder() 135 | { 136 | return new QueryBuilder($this); 137 | } 138 | 139 | /** 140 | * Performs GET HTTP request. 141 | * @param string $url URL 142 | * @param array $query query options 143 | * @param string $body request body 144 | * @param bool $raw if response body contains JSON and should be decoded 145 | * @throws \yii\base\InvalidConfigException 146 | * @return mixed response 147 | */ 148 | public function get($url, $query = [], $body = null, $raw = false) 149 | { 150 | try { 151 | return $this->makeRequest('GET', $url, $query, $body, $raw); 152 | } catch (ClientException $e) { 153 | if (404 === $e->getCode()) { 154 | return false; 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Performs HEAD HTTP request. 161 | * @param string $url URL 162 | * @param array $query query options 163 | * @param string $body request body 164 | * @throws \yii\base\InvalidConfigException 165 | * @return mixed response 166 | */ 167 | public function head($url, $query = [], $body = null) 168 | { 169 | $this->makeRequest('HEAD', $url, $query, $body); 170 | 171 | return $this->_response->getHeaders(); 172 | } 173 | 174 | /** 175 | * Performs POST HTTP request. 176 | * @param string $url URL 177 | * @param array $query query options 178 | * @param string $body request body 179 | * @param bool $raw if response body contains JSON and should be decoded 180 | * @throws \yii\base\InvalidConfigException 181 | * @return mixed response 182 | */ 183 | public function post($url, $query = [], $body = null, $raw = false) 184 | { 185 | return $this->makeRequest('POST', $url, $query, $body, $raw); 186 | } 187 | 188 | /** 189 | * Performs PUT HTTP request. 190 | * @param string $url URL 191 | * @param array $query query options 192 | * @param string $body request body 193 | * @param bool $raw if response body contains JSON and should be decoded 194 | * @throws \yii\base\InvalidConfigException 195 | * @return mixed response 196 | */ 197 | public function put($url, $query = [], $body = null, $raw = false) 198 | { 199 | return $this->makeRequest('PUT', $url, $query, $body, $raw); 200 | } 201 | 202 | /** 203 | * Performs DELETE HTTP request. 204 | * @param string $url URL 205 | * @param array $query query options 206 | * @param string $body request body 207 | * @param bool $raw if response body contains JSON and should be decoded 208 | * @throws \yii\base\InvalidConfigException 209 | * @return mixed response 210 | */ 211 | public function delete($url, $query = [], $body = null, $raw = false) 212 | { 213 | return $this->makeRequest('DELETE', $url, $query, $body, $raw); 214 | } 215 | 216 | /** 217 | * Make request and check for error. 218 | * @param string $method 219 | * @param string $url URL 220 | * @param array $query query options, (GET parameters) 221 | * @param string $body request body, (POST parameters) 222 | * @param bool $raw if response body contains JSON and should be decoded 223 | * @throws \yii\base\InvalidConfigException 224 | * @return mixed response 225 | */ 226 | public function makeRequest($method, $url, $query = [], $body = null, $raw = false) 227 | { 228 | return $this->handleRequest($method, $this->prepareUrl($url, $query), $body, $raw); 229 | } 230 | 231 | /** 232 | * Creates URL. 233 | * @param mixed $path path 234 | * @param array $query query options 235 | * @return array 236 | */ 237 | private function prepareUrl($path, array $query = []) 238 | { 239 | $url = $path; 240 | $query = array_merge($this->getAuth(), $query); 241 | if (!empty($query)) { 242 | $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query); 243 | } 244 | 245 | return $url; 246 | } 247 | 248 | /** 249 | * Handles the request with handler. 250 | * Returns array or raw response content, if $raw is true. 251 | * 252 | * @param string $method POST, GET, etc 253 | * @param string $url the URL for request, not including proto and site 254 | * @param array|string $body the request body. When array - will be sent as POST params, otherwise - as RAW body. 255 | * @param bool $raw Whether to decode data, when response is decodeable (JSON). 256 | * @return array|string 257 | */ 258 | protected function handleRequest($method, $url, $body = null, $raw = false) 259 | { 260 | $method = strtoupper($method); 261 | $profile = $method . ' ' . $url . '#' . (is_array($body) ? http_build_query($body) : $body); 262 | $options = [(is_array($body) ? 'form_params' : 'body') => $body]; 263 | Yii::beginProfile($profile, __METHOD__); 264 | $this->_response = $this->getHandler()->request($method, $url, $options); 265 | Yii::endProfile($profile, __METHOD__); 266 | 267 | $res = $this->_response->getBody()->getContents(); 268 | if (!$raw && preg_grep('|application/json|i', $this->_response->getHeader('Content-Type'))) { 269 | $res = Json::decode($res); 270 | } 271 | 272 | return $res; 273 | } 274 | 275 | /** 276 | * Returns the request handler (Guzzle client for the moment). 277 | * Creates and setups handler if not set. 278 | * @return Handler 279 | */ 280 | public function getHandler() 281 | { 282 | if (static::$_handler === null) { 283 | static::$_handler = new Handler($this->config); 284 | } 285 | 286 | return static::$_handler; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/DebugAction.php: -------------------------------------------------------------------------------- 1 | controller->loadData($tag); 52 | 53 | $timings = $this->panel->calculateTimings(); 54 | ArrayHelper::multisort($timings, 3, SORT_DESC); 55 | if (!isset($timings[$logId])) { 56 | throw new HttpException(404, 'Log message not found.'); 57 | } 58 | $message = $timings[$logId][1]; 59 | if (($pos = mb_strpos($message, '#')) !== false) { 60 | $url = mb_substr($message, 0, $pos); 61 | $body = mb_substr($message, $pos + 1); 62 | } else { 63 | $url = $message; 64 | $body = null; 65 | } 66 | $method = mb_substr($url, 0, $pos = mb_strpos($url, ' ')); 67 | $url = mb_substr($url, $pos + 1); 68 | 69 | parse_str($body, $options); 70 | 71 | /* @var $db Connection */ 72 | $db = \Yii::$app->get($this->db); 73 | $time = microtime(true); 74 | switch ($method) { 75 | case 'GET': 76 | $result = $db->get($url, $options, $body, true); 77 | break; 78 | case 'POST': 79 | $result = $db->post($url, $options, $body, true); 80 | break; 81 | case 'PUT': 82 | $result = $db->put($url, $options, $body, true); 83 | break; 84 | case 'DELETE': 85 | $result = $db->delete($url, $options, $body, true); 86 | break; 87 | case 'HEAD': 88 | $result = $db->head($url, $options, $body); 89 | break; 90 | default: 91 | throw new NotSupportedException("Request method '$method' is not supported by HiArt."); 92 | } 93 | $time = microtime(true) - $time; 94 | $now = microtime(true); 95 | Yii::$app->response->format = Response::FORMAT_JSON; 96 | 97 | return [ 98 | 'time' => date('H:i:s.', $now) . sprintf('%03d', (int)(($now - (int)$now) * 1000)), 99 | 'duration' => sprintf('%.1f ms', $time * 1000), 100 | 'result' => $result, 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/DebugPanel.php: -------------------------------------------------------------------------------- 1 | actions['rest-query'] = [ 38 | 'class' => 'apexwire\\restclient\\DebugAction', 39 | 'panel' => $this, 40 | 'db' => Connection::getDriverName(), 41 | ]; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getName() 48 | { 49 | return 'Rest Client'; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getSummary() 56 | { 57 | $timings = $this->calculateTimings(); 58 | $queryCount = count($timings); 59 | $queryTime = 0; 60 | foreach ($timings as $timing) { 61 | $queryTime += $timing[3]; 62 | } 63 | $queryTime = number_format($queryTime * 1000) . ' ms'; 64 | $url = $this->getUrl(); 65 | $output = << 67 | Rest Client 68 | $queryCount 69 | $queryTime 70 | 71 | 72 | HTML; 73 | 74 | return $queryCount > 0 ? $output : ''; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function getDetail() 81 | { 82 | $apiUrl = null; 83 | $timings = $this->calculateTimings(); 84 | ArrayHelper::multisort($timings, 3, SORT_DESC); 85 | $rows = []; 86 | $i = 0; 87 | // Try to get API URL 88 | try { 89 | $restClient = \Yii::$app->get('restclient'); 90 | $apiUrl = (StringHelper::endsWith($restClient->config['base_uri'], '/')) 91 | ? $restClient->config['base_uri'] 92 | : $restClient->config['base_uri'] . '/'; 93 | } catch (InvalidConfigException $e) { 94 | // Pass 95 | } 96 | 97 | foreach ($timings as $logId => $timing) { 98 | $time = $duration = '-'; 99 | if (is_double($timing[2])) { 100 | $time = date('H:i:s.', $timing[2]) . sprintf('%03d', (int)(($timing[2] - (int)$timing[2]) * 1000)); 101 | $duration = sprintf('%.1f ms', $timing[3] * 1000); 102 | } 103 | $message = $timing[1]; 104 | $traces = $timing[4]; 105 | 106 | if (($pos = mb_strpos($message, '#')) !== false) { 107 | $url = mb_substr($message, 0, $pos); 108 | $body = mb_substr($message, $pos + 1); 109 | } else { 110 | $url = $message; 111 | $body = null; 112 | } 113 | 114 | if (($pos = mb_strpos($message, ' ')) !== false) { 115 | $method = mb_substr($message, 0, $pos); 116 | } else { 117 | $method = null; 118 | } 119 | 120 | $traceString = ''; 121 | if (!empty($traces)) { 122 | $traceString .= Html::ul($traces, [ 123 | 'class' => 'trace', 124 | 'item' => function ($trace) { 125 | return "
  • {$trace['class']}{$trace['type']}{$trace['function']}({$trace['line']})
  • "; 126 | }, 127 | ]); 128 | } 129 | 130 | $runLink = $newTabLink = ''; 131 | if ($method == 'GET') { 132 | $runLink = Html::a('run query', 133 | Url::to(['rest-query', 'logId' => $logId, 'tag' => $this->tag]), 134 | ['class' => 'restclient-link', 'data' => ['id' => $i]] 135 | ); 136 | $newTabLink = Html::a('to new tab', 137 | $apiUrl . preg_replace('/^[A-Z]+\s+/', '', $url) . $body, 138 | ['target' => '_blank'] 139 | ); 140 | } 141 | 142 | $url_encoded = Html::encode((isset($apiUrl)) ? str_replace(' ', ' ' . $apiUrl, $url) : $url); 143 | $body_encoded = Html::encode($body); 144 | $rows[] = << 146 | $time 147 | $duration 148 |
    $url_encoded

    $body_encoded

    $traceString
    149 | $runLink
    $newTabLink 150 | 151 | 152 | 153 | 154 | 155 | 156 | HTML; 157 | ++$i; 158 | } 159 | $rows = implode("\n", $rows); 160 | 161 | \Yii::$app->view->registerCss(<<view->registerJs(<</g, '>'); 173 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 174 | var cls = 'number'; 175 | if (/^"/.test(match)) { 176 | if (/:$/.test(match)) { 177 | cls = 'key'; 178 | } else { 179 | cls = 'string'; 180 | } 181 | } else if (/true|false/.test(match)) { 182 | cls = 'boolean'; 183 | } else if (/null/.test(match)) { 184 | cls = 'null'; 185 | } 186 | return '' + match + ''; 187 | }); 188 | } 189 | 190 | $('.restclient-link').on('click', function (event) { 191 | event.preventDefault(); 192 | 193 | var id = $(this).data('id'); 194 | var result = $('.restclient-wrapper[data-id=' + id +']'); 195 | result.find('.result').html('Sending request...'); 196 | result.show(); 197 | $.ajax({ 198 | type: 'POST', 199 | url: $(this).attr('href'), 200 | success: function (data) { 201 | var is_json = true; 202 | try { 203 | var json = JSON.parse(data.result); 204 | } catch(e) { 205 | is_json = false; 206 | } 207 | result.find('.time').html(data.time); 208 | result.find('.duration').html(data.duration); 209 | if (is_json) { 210 | result.find('.result').html( syntaxHighlight( JSON.stringify( JSON.parse(data.result), undefined, 10) ) ); 211 | } else if (data.result instanceof Object) { 212 | console.log(typeof(data.result)); 213 | var html = ''; 214 | for (var key in data.result) { html += key+':'+data.result[key]+'
    '; } 215 | result.find('.result').html( html ); 216 | } else { 217 | result.find('.result').html( data.result ); 218 | } 219 | }, 220 | error: function (jqXHR, textStatus, errorThrown) { 221 | result.find('.time').html(''); 222 | result.find('.result').html('Error: ' + errorThrown + ' - ' + textStatus + '
    ' + jqXHR.responseText); 223 | }, 224 | dataType: 'json' 225 | }); 226 | return false; 227 | }); 228 | JS 229 | , View::POS_READY); 230 | 231 | return <<Rest Client Queries 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | $rows 245 | 246 |
    TimeDurationUrl / QueryRun Query on node
    247 | HTML; 248 | } 249 | 250 | /** 251 | * Расчет времени 252 | * 253 | * @return array 254 | */ 255 | public function calculateTimings() 256 | { 257 | if ($this->_timings !== null) { 258 | return $this->_timings; 259 | } 260 | 261 | $messages = is_array($this->data['messages']) ? $this->data['messages'] : []; 262 | $timings = []; 263 | $stack = []; 264 | foreach ($messages as $i => $log) { 265 | list($token, $level, $category, $timestamp) = $log; 266 | $log[5] = $i; 267 | if ($level === Logger::LEVEL_PROFILE_BEGIN) { 268 | $stack[] = $log; 269 | } elseif ($level === Logger::LEVEL_PROFILE_END) { 270 | if (($last = array_pop($stack)) !== null && $last[0] === $token) { 271 | $timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]]; 272 | } 273 | } 274 | } 275 | 276 | $now = microtime(true); 277 | while (($last = array_pop($stack)) !== null) { 278 | $delta = $now - $last[3]; 279 | $timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]]; 280 | } 281 | ksort($timings); 282 | 283 | return $this->_timings = $timings; 284 | } 285 | 286 | /** 287 | * {@inheritdoc} 288 | */ 289 | public function save() 290 | { 291 | $target = $this->module->logTarget; 292 | $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, 293 | ['apexwire\restclient\Connection::handleRequest']); 294 | 295 | return ['messages' => $messages]; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | get(Connection::getDriverName()); 47 | } 48 | 49 | $commandConfig = $db->getQueryBuilder()->build($this); 50 | 51 | return $db->createCommand($commandConfig); 52 | } 53 | 54 | /** 55 | * Получаем количество объектов 56 | * 57 | * @param string $q 58 | * @param null $db 59 | * @return mixed 60 | */ 61 | public function count($q = '*', $db = null) 62 | { 63 | $result = $this->createCommand($db)->head(); 64 | 65 | return current(ArrayHelper::getValue($result, 'X-Pagination-Total-Count')); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function exists($db = null) 72 | { 73 | throw new NotSupportedException('exists in is not supported.'); 74 | } 75 | 76 | /** 77 | * @inheritdoc 78 | * @deprecated 79 | */ 80 | public function from($tables) 81 | { 82 | throw new NotSupportedException('from in is not supported.'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | '', 31 | SORT_DESC => '-', 32 | ]; 33 | 34 | /** 35 | * @var Connection the database connection. 36 | */ 37 | public $db; 38 | /** 39 | * @var string the separator between different fragments of a SQL statement. 40 | * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement. 41 | */ 42 | public $separator = ' '; 43 | /** 44 | * @var array the abstract column types mapped to physical column types. 45 | * This is mainly used to support creating/modifying tables using DB-independent data type specifications. 46 | * Child classes should override this property to declare supported type mappings. 47 | */ 48 | public $typeMap = []; 49 | 50 | /** 51 | * @var array map of query condition to builder methods. 52 | * These methods are used by [[buildCondition]] to build SQL conditions from array syntax. 53 | */ 54 | protected $conditionBuilders = [ 55 | 'AND' => 'buildAndCondition', 56 | ]; 57 | 58 | 59 | /** 60 | * Constructor. 61 | * @param Connection $connection the database connection. 62 | * @param array $config name-value pairs that will be used to initialize the object properties 63 | */ 64 | public function __construct($connection, $config = []) 65 | { 66 | $this->db = $connection; 67 | parent::__construct($config); 68 | } 69 | 70 | 71 | /** 72 | * @param Query $query 73 | * @param array $params 74 | * @return array 75 | * @throws NotSupportedException 76 | */ 77 | public function build($query, $params = []) 78 | { 79 | $this->buildSelect($query->select, $params); 80 | $this->buildPerPage($query->limit, $params); 81 | $this->buildPage($query->offset, $query->limit, $params); 82 | $this->buildFind($query->where, $query->searchModel, $params); 83 | $this->buildSort($query->orderBy, $params); 84 | 85 | return [ 86 | 'query' => $query, 87 | 'queryParts' => $params, 88 | 'index' => $query->from 89 | ]; 90 | } 91 | 92 | /** 93 | * Устанавливаем количество записей на страницу 94 | * 95 | * @param $limit 96 | * @param $params 97 | */ 98 | public function buildPerPage($limit, &$params) 99 | { 100 | if (is_int($limit)) { 101 | $params['per-page'] = $limit; 102 | } 103 | } 104 | 105 | /** 106 | * @param $offset 107 | * @param $limit 108 | * @param $params 109 | */ 110 | public function buildPage($offset, $limit, &$params) 111 | { 112 | if ($offset > 0) { 113 | $params['page'] = ceil($offset / $limit) + 1; 114 | } 115 | } 116 | 117 | /** 118 | * Преобразуем массив параметров where в массив для поиска 119 | * 120 | * @param $condition 121 | * @param $searchModel 122 | * @param $params 123 | */ 124 | public function buildFind($condition, $searchModel, &$params) 125 | { 126 | if (!empty($condition) && is_array($condition)) { 127 | 128 | $where = $this->buildCondition($condition, $params); 129 | $params = $this->getParams($searchModel, $where, $params); 130 | } 131 | } 132 | 133 | /** 134 | * @param string $searchModel 135 | * @param string|array $where 136 | * @param array $params 137 | * @return array 138 | */ 139 | protected function getParams($searchModel, $where, $params = []) 140 | { 141 | if (is_array($where)) { 142 | foreach ($where as $key => $value) { 143 | 144 | if (is_array($value)) { 145 | $params = $this->getParams($searchModel, $value, $params); 146 | } else { 147 | $params[$searchModel . '[' . $key . ']'] = $value; 148 | unset($params[$key]); 149 | } 150 | } 151 | } 152 | 153 | return $params; 154 | } 155 | 156 | /** 157 | * Устанавливаем параметр сортировки 158 | * 159 | * @param $orderBy 160 | * @param $params 161 | * @return array 162 | */ 163 | public function buildSort($orderBy, &$params) 164 | { 165 | if (!empty($orderBy)) { 166 | $params['sort'] = $this->_sort[reset($orderBy)] . key($orderBy); 167 | } 168 | } 169 | 170 | /** 171 | * @inheritdoc 172 | */ 173 | public function buildSelect($columns, &$params) 174 | { 175 | if (!empty($columns) AND is_array($columns)) { 176 | $params['fields'] = implode(',', $columns); 177 | } 178 | } 179 | 180 | /** 181 | * @param $condition 182 | * @param $params 183 | * @return array|string 184 | * @throws NotSupportedException 185 | */ 186 | public function buildCondition($condition, &$params) 187 | { 188 | if ($condition instanceof Expression) { 189 | foreach ($condition->params as $n => $v) { 190 | $params[$n] = $v; 191 | } 192 | 193 | return $condition->expression; 194 | } elseif (!is_array($condition)) { 195 | return (string)$condition; 196 | } elseif (empty($condition)) { 197 | return ''; 198 | } 199 | 200 | if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... 201 | $operator = strtoupper($condition[0]); 202 | if (!isset($this->conditionBuilders[$operator])) { 203 | throw new NotSupportedException($operator . ' in is not supported.'); 204 | } 205 | $method = $this->conditionBuilders[$operator]; 206 | array_shift($condition); 207 | 208 | return $this->$method($operator, $condition, $params); 209 | } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... 210 | return $this->buildHashCondition($condition); 211 | } 212 | } 213 | 214 | /** 215 | * @inheritdoc 216 | */ 217 | public function buildHashCondition($condition) 218 | { 219 | //TODO: проверить работу 220 | $parts = []; 221 | foreach ($condition as $attribute => $value) { 222 | if (is_array($value)) { // IN condition 223 | $parts[$attribute . 's'] = implode(',', $value); 224 | } else { 225 | $parts[$attribute] = $value; 226 | } 227 | } 228 | 229 | return $parts; 230 | } 231 | 232 | /** 233 | * @inheritdoc 234 | */ 235 | public function buildAndCondition($operator, $operands, &$params) 236 | { 237 | $parts = []; 238 | foreach ($operands as $operand) { 239 | if (is_array($operand)) { 240 | $operand = $this->buildCondition($operand, $params); 241 | } 242 | if ($operand instanceof Expression) { 243 | foreach ($operand->params as $n => $v) { 244 | $params[$n] = $v; 245 | } 246 | $operand = $operand->expression; 247 | } 248 | if ($operand !== '') { 249 | $parts[] = $operand; 250 | } 251 | } 252 | 253 | return $parts; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/RestDataProvider.php: -------------------------------------------------------------------------------- 1 | query instanceof QueryInterface) { 36 | throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); 37 | } 38 | 39 | return (int)$this->query->count(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/RestQuery.php: -------------------------------------------------------------------------------- 1 | modelClass = $modelClass; 43 | 44 | parent::__construct($config); 45 | } 46 | 47 | 48 | /** 49 | * Creates a DB command that can be used to execute this query. 50 | * 51 | * @param Connection $db the DB connection used to create the DB command. 52 | * If null, the DB connection returned by [[modelClass]] will be used. 53 | * 54 | * @return Command the created DB command instance. 55 | */ 56 | public function createCommand($db = null) 57 | { 58 | /** @type ActiveRecord $modelClass */ 59 | $modelClass = $this->modelClass; 60 | if ($db === null) { 61 | $db = $modelClass::getDb(); 62 | } 63 | 64 | if ($this->from === null) { 65 | $this->from = $modelClass::modelName(); 66 | } 67 | 68 | if ($this->searchModel === null) { 69 | $this->searchModel = mb_substr(mb_strrchr($this->modelClass, '\\'), 1) . 'Search'; 70 | } 71 | 72 | return parent::createCommand($db); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function all($db = null) 79 | { 80 | return parent::all($db); 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function populate($rows) 87 | { 88 | if (empty($rows)) { 89 | return []; 90 | } 91 | 92 | $models = $this->createModels($rows); 93 | if (!empty($this->join) && $this->indexBy === null) { 94 | $models = $this->removeDuplicatedModels($models); 95 | } 96 | if (!empty($this->with)) { 97 | $this->findWith($this->with, $models); 98 | } 99 | if (!$this->asArray) { 100 | foreach ($models as $model) { 101 | $model->afterFind(); 102 | } 103 | } 104 | 105 | return $models; 106 | } 107 | 108 | /** 109 | * Removes duplicated models by checking their primary key values. 110 | * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. 111 | * @param array $models the models to be checked 112 | * @throws InvalidConfigException if model primary key is empty 113 | * @return array the distinctive models 114 | */ 115 | private function removeDuplicatedModels($models) 116 | { 117 | $hash = []; 118 | /* @var $class ActiveRecord */ 119 | $class = $this->modelClass; 120 | $pks = $class::primaryKey(); 121 | 122 | if (count($pks) > 1) { 123 | // composite primary key 124 | foreach ($models as $i => $model) { 125 | $key = []; 126 | foreach ($pks as $pk) { 127 | if (!isset($model[$pk])) { 128 | // do not continue if the primary key is not part of the result set 129 | break 2; 130 | } 131 | $key[] = $model[$pk]; 132 | } 133 | $key = serialize($key); 134 | if (isset($hash[$key])) { 135 | unset($models[$i]); 136 | } else { 137 | $hash[$key] = true; 138 | } 139 | } 140 | } elseif (empty($pks)) { 141 | throw new InvalidConfigException("Primary key of '{$class}' can not be empty."); 142 | } else { 143 | // single column primary key 144 | $pk = reset($pks); 145 | foreach ($models as $i => $model) { 146 | if (!isset($model[$pk])) { 147 | // do not continue if the primary key is not part of the result set 148 | break; 149 | } 150 | $key = $model[$pk]; 151 | if (isset($hash[$key])) { 152 | unset($models[$i]); 153 | } elseif ($key !== null) { 154 | $hash[$key] = true; 155 | } 156 | } 157 | } 158 | 159 | return array_values($models); 160 | } 161 | 162 | /** 163 | * @inheritdoc 164 | */ 165 | public function one($db = null) 166 | { 167 | $row = parent::one($db); 168 | if ($row !== false) { 169 | $models = $this->populate(isset($row[0]) ? $row : [$row]); 170 | 171 | return reset($models) ?: null; 172 | } 173 | 174 | return null; 175 | } 176 | } 177 | --------------------------------------------------------------------------------