├── .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 |
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 |
= Html::encode($this->title) ?>
15 |
16 | = $this->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 |
= Html::encode($this->title) ?>
17 |
18 |
19 |
20 | = Html::a('Добавить', ['create'], ['class' => 'btn btn-success']) ?>
21 |
22 |
23 | = GridView::widget([
24 | 'dataProvider' => $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 |
= Html::encode($this->title) ?>
16 |
17 | = $this->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 |
= Html::encode($this->title) ?>
16 |
17 |
18 | = Html::a('Обновить', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?>
19 | = Html::a('Удалить', ['delete', 'id' => $model->id], [
20 | 'class' => 'btn btn-danger',
21 | 'data' => [
22 | 'confirm' => 'Вы уверены что хотите удалить запись?',
23 | 'method' => 'post',
24 | ],
25 | ]) ?>
26 |
27 |
28 | = DetailView::widget([
29 | 'model' => $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 | Time |
238 | Duration |
239 | Url / Query |
240 | Run Query on node |
241 |
242 |
243 |
244 | $rows
245 |
246 |
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 |
--------------------------------------------------------------------------------