├── tests
├── models
│ ├── .gitignore
│ ├── RelatedRestModel.php
│ └── RestModel.php
├── fixtures
│ ├── RestModelFixture.php
│ ├── RelatedRestModelFixture.php
│ └── data
│ │ ├── rest-model.php
│ │ └── related-rest-model.php
├── bootstrap.php
├── log
│ └── ArrayTarget.php
├── RelationTest.php
├── UrlWithoutPluralisationTest.php
├── UrlTest.php
└── TestCase.php
├── .editorconfig
├── src
├── Exception.php
├── conditions
│ ├── ConditionBuilderTrait.php
│ ├── NotConditionBuilder.php
│ ├── BetweenConditionBuilder.php
│ ├── SimpleConditionBuilder.php
│ ├── HashConditionBuilder.php
│ ├── ConjunctionConditionBuilder.php
│ ├── LikeConditionBuilder.php
│ └── InConditionBuilder.php
├── RestDataProvider.php
├── ActiveFixture.php
├── Query.php
├── ActiveRecord.php
├── QueryBuilder.php
├── Command.php
├── ActiveQuery.php
└── Connection.php
├── phpunit.xml.dist
├── .gitignore
├── ecs.php
├── LICENSE
├── .github
└── workflows
│ ├── ecs.yml
│ └── build.yml
├── composer.json
└── README.md
/tests/models/.gitignore:
--------------------------------------------------------------------------------
1 | ReservationEvent.php
2 | ReservationObject.php
3 | ReservationProperty.php
4 | ReservationShow.php
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 4
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/src/Exception.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace simialbi\yii2\rest;
11 |
12 | class Exception extends \yii\db\Exception
13 | {
14 | public function getName(): string
15 | {
16 | return 'REST Exception';
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/fixtures/RestModelFixture.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest\fixtures;
11 |
12 | use simialbi\yii2\rest\ActiveFixture;
13 |
14 | class RestModelFixture extends ActiveFixture
15 | {
16 | public $modelClass = 'yiiunit\extensions\rest\models\RestModel';
17 | }
18 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | ./tests
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # phpstorm project files
2 | .idea
3 |
4 | # netbeans project files
5 | nbproject
6 |
7 | # zend studio for eclipse project files
8 | .buildpath
9 | .project
10 | .settings
11 |
12 | # windows thumbnail cache
13 | Thumbs.db
14 |
15 | # composer vendor dir
16 | /vendor
17 |
18 | # composer itself is not needed and related files
19 | composer.phar
20 | composer.lock
21 |
22 | # Mac DS_Store Files
23 | .DS_Store
24 |
25 | # phpunit itself is not needed
26 | phpunit.phar
27 | # local phpunit config
28 | /phpunit.xml
29 | .phpunit.result.cache
30 |
--------------------------------------------------------------------------------
/tests/fixtures/RelatedRestModelFixture.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest\fixtures;
11 |
12 | use simialbi\yii2\rest\ActiveFixture;
13 |
14 | class RelatedRestModelFixture extends ActiveFixture
15 | {
16 | public $modelClass = 'yiiunit\extensions\rest\models\RelatedRestModel';
17 | public $depends = ['yiiunit\extensions\rest\fixtures\RestModelFixture'];
18 | }
19 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace simialbi\yii2\rest\conditions;
11 |
12 | use yii\data\DataFilter;
13 | use yii\helpers\ArrayHelper;
14 |
15 | trait ConditionBuilderTrait
16 | {
17 | /**
18 | * Returns the operator that is represented by this condition class
19 | *
20 | * @throws \Exception
21 | */
22 | protected function getOperator(string $operator): string
23 | {
24 | return ArrayHelper::getValue(array_flip((new DataFilter())->filterControls), $operator, 'and');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/models/RelatedRestModel.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest\models;
11 |
12 | use simialbi\yii2\rest\ActiveRecord;
13 |
14 | /**
15 | * @package yiiunit\extensions\rest\models
16 | *
17 | * @property integer $id
18 | * @property integer $rest_model_id
19 | * @property string $subject
20 | * @property string $message
21 | * @property integer $created_at
22 | * @property integer $updated_at
23 | * @property string $created_by
24 | * @property string $updated_by
25 | */
26 | class RelatedRestModel extends ActiveRecord
27 | {
28 | public static function primaryKey(): array
29 | {
30 | return ['id'];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/fixtures/data/rest-model.php:
--------------------------------------------------------------------------------
1 | 1,
8 | 'name' => 'Model 1',
9 | 'description' => 'This is the representation of model 1',
10 | 'created_at' => 1555660035,
11 | 'updated_at' => 1555660035,
12 | 'created_by' => 'simialbi',
13 | 'updated_by' => 'simialbi',
14 | ],
15 | [
16 | 'id' => 2,
17 | 'name' => 'Model 2',
18 | 'description' => 'This is the representation of model 2',
19 | 'created_at' => 1555660035,
20 | 'updated_at' => 1555660035,
21 | 'created_by' => 'simialbi',
22 | 'updated_by' => 'simialbi',
23 | ],
24 | [
25 | 'id' => 3,
26 | 'name' => 'Model 3',
27 | 'description' => 'This is the representation of model 3',
28 | 'created_at' => 1555660035,
29 | 'updated_at' => 1555660035,
30 | 'created_by' => 'simialbi',
31 | 'updated_by' => 'simialbi',
32 | ],
33 | ];
34 |
--------------------------------------------------------------------------------
/tests/log/ArrayTarget.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest\log;
11 |
12 | use yii\helpers\ArrayHelper;
13 | use yii\log\Target;
14 |
15 | /**
16 | * @package yiiunit\extensions\rest\log
17 | *
18 | * @property-read array $cache
19 | */
20 | class ArrayTarget extends Target
21 | {
22 | /**
23 | * @var array Stores log data
24 | */
25 | private $_cache = [];
26 |
27 | /**
28 | * Exports log [[messages]] to a specific destination.
29 | * Child classes must implement this method.
30 | */
31 | public function export()
32 | {
33 | $this->_cache = ArrayHelper::merge($this->_cache, $this->messages);
34 | }
35 |
36 | /**
37 | * Getter for cache variable
38 | */
39 | public function getCache(): array
40 | {
41 | return $this->_cache;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | withConfiguredRule(
12 | ClassDefinitionFixer::class,
13 | [
14 | 'space_before_parenthesis' => true,
15 | ],
16 | )
17 | ->withFileExtensions(['php'])
18 | ->withPaths(
19 | [
20 | __DIR__ . '/src',
21 | __DIR__ . '/tests',
22 | ]
23 | )
24 | ->withPhpCsFixerSets(false, false, false, false, false, true)
25 | ->withPreparedSets(
26 | true,
27 | false,
28 | false,
29 | false,
30 | true,
31 | true,
32 | true,
33 | false,
34 | true,
35 | false,
36 | false,
37 | true,
38 | true
39 | )
40 | ->withRules([NoUnusedImportsFixer::class, OrderedTraitsFixer::class]);
41 |
--------------------------------------------------------------------------------
/src/conditions/NotConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use yii\db\conditions\NotCondition;
12 | use yii\db\ExpressionInterface;
13 |
14 | /**
15 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
16 | */
17 | class NotConditionBuilder extends \yii\db\conditions\NotConditionBuilder
18 | {
19 | use ConditionBuilderTrait;
20 |
21 | public function build(ExpressionInterface $expression, array &$params = []): array
22 | {
23 | /** @var NotCondition $expression */
24 | $operand = $expression->getCondition();
25 | if (empty($operand)) {
26 | return [];
27 | }
28 |
29 | $expression = $this->queryBuilder->buildCondition($operand, $params);
30 |
31 | return [
32 | $this->getNegationOperator() => $expression,
33 | ];
34 | }
35 |
36 | protected function getNegationOperator(): string
37 | {
38 | return 'not';
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/RestDataProvider.php:
--------------------------------------------------------------------------------
1 | query instanceof QueryInterface) {
33 | throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. simialbi\yii2\rest\Query or its subclasses.');
34 | }
35 |
36 | return (int) $this->query->count();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2016, ApexWire
4 | Copyright © 2017 simialbi
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/conditions/BetweenConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use yii\db\conditions\BetweenCondition;
12 | use yii\db\ExpressionInterface;
13 |
14 | /**
15 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
16 | */
17 | class BetweenConditionBuilder extends \yii\db\conditions\BetweenConditionBuilder
18 | {
19 | use ConditionBuilderTrait;
20 |
21 | public function build(ExpressionInterface $expression, array &$params = []): array
22 | {
23 | /** @var BetweenCondition $expression */
24 | $operator = $expression->getOperator();
25 | $column = $expression->getColumn();
26 |
27 | $phName1 = $this->createPlaceholder($expression->getIntervalStart(), $params);
28 | $phName2 = $this->createPlaceholder($expression->getIntervalEnd(), $params);
29 |
30 | if ($operator === 'BETWEEN') {
31 | return [
32 | $column => [
33 | 'gt' => $phName1,
34 | 'lt' => $phName2,
35 | ],
36 | ];
37 | } else {
38 | return $this->queryBuilder->buildCondition([
39 | 'or',
40 | ['<', $column, $expression->getIntervalStart()],
41 | ['>', $column, $expression->getIntervalEnd()],
42 | ], $params);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/models/RestModel.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest\models;
11 |
12 | use simialbi\yii2\rest\ActiveRecord;
13 |
14 | /**
15 | * @package yiiunit\extensions\rest\models
16 | *
17 | * @property integer $id
18 | * @property string $name
19 | * @property string $description
20 | * @property integer $created_at
21 | * @property integer $updated_at
22 | * @property string $created_by
23 | * @property string $updated_by
24 | *
25 | * @property-read RelatedRestModel[] $relatedRests
26 | * @property-read RelatedRestModel $relatedRest
27 | */
28 | class RestModel extends ActiveRecord
29 | {
30 | public static function primaryKey(): array
31 | {
32 | return ['id'];
33 | }
34 |
35 | /**
36 | * Get related rests
37 | */
38 | public function getRelatedRests(): \yii\db\ActiveQueryInterface
39 | {
40 | return $this->hasMany(RelatedRestModel::class, [
41 | 'rest_model_id' => 'id',
42 | ]);
43 | }
44 |
45 | /**
46 | * Get related rest
47 | * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface
48 | */
49 | public function getRelatedRest()
50 | {
51 | return $this->hasOne(RelatedRestModel::class, [
52 | 'rest_model_id' => 'id',
53 | ]);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/conditions/SimpleConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use yii\db\conditions\SimpleCondition;
12 | use yii\db\ExpressionInterface;
13 |
14 | /**
15 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
16 | */
17 | class SimpleConditionBuilder extends \yii\db\conditions\SimpleConditionBuilder
18 | {
19 | use ConditionBuilderTrait;
20 |
21 | /**
22 | * @throws \Exception
23 | */
24 | public function build(ExpressionInterface $expression, array &$params = []): array
25 | {
26 | /** @var SimpleCondition $expression */
27 | $operator = $expression->getOperator();
28 | $column = $expression->getColumn();
29 | $value = $expression->getValue();
30 |
31 | if ($value === null) {
32 | return [
33 | $column => [
34 | $this->getOperator($operator) => null,
35 | ],
36 | ];
37 | }
38 | if ($value instanceof ExpressionInterface) {
39 | return [
40 | $column => [
41 | $this->getOperator($operator) => $this->queryBuilder->buildExpression($value, $params),
42 | ],
43 | ];
44 | }
45 |
46 | $phName = $this->queryBuilder->bindParam($value, $params);
47 | return [
48 | $column => [
49 | $this->getOperator($operator) => $phName,
50 | ],
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.github/workflows/ecs.yml:
--------------------------------------------------------------------------------
1 | name: ecs
2 |
3 | on: [ push, pull_request, workflow_dispatch ]
4 |
5 | env:
6 | DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi --ignore-platform-req=php"
7 |
8 | jobs:
9 | easy-coding-standard:
10 | name: PHP ${{ matrix.php }}-${{ matrix.os }}
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | os: [ ubuntu-latest ]
16 | php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ]
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | - name: Install PHP
22 | uses: shivammathur/setup-php@v2
23 | with:
24 | php-version: ${{ matrix.php }}
25 | - name: Determine composer cache directory
26 | id: composer-cache
27 | run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
28 | - name: Cache dependencies composer installed with composer
29 | uses: actions/cache@v4
30 | with:
31 | path: ${{ env.COMPOSER_CACHE_DIR }}
32 | key: php${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
33 | restore-keys: php${{ matrix.php }}-composer-
34 | - name: Update composer
35 | run: composer self-update
36 | - name: Install dependencies with composer
37 | run: composer update $DEFAULT_COMPOSER_FLAGS
38 | - name: Run easy-coding-standard
39 | run: vendor/bin/ecs check src tests --ansi
40 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [ push, pull_request, workflow_dispatch ]
4 |
5 | env:
6 | DEFAULT_COMPOSER_FLAGS: "--prefer-dist --no-interaction --no-progress --optimize-autoloader"
7 |
8 | jobs:
9 | phpunit:
10 | name: PHP ${{ matrix.php }}-${{ matrix.os }}
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | os: [ ubuntu-latest ]
16 | php: [ "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4" ]
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | - name: Install PHP
22 | uses: shivammathur/setup-php@v2
23 | with:
24 | php-version: ${{ matrix.php }}
25 | extensions: apc, curl, dom, intl, mbstring, mcrypt
26 | ini-values: date.timezone='UTC'
27 | - name: Determine composer cache directory
28 | id: composer-cache
29 | run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
30 | - name: Cache dependencies composer installed with composer
31 | uses: actions/cache@v4
32 | with:
33 | path: ${{ env.COMPOSER_CACHE_DIR }}
34 | key: php${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
35 | restore-keys: php${{ matrix.php }}-composer-
36 | - name: Update composer
37 | run: composer self-update
38 | - name: Install dependencies with composer
39 | run: composer update $DEFAULT_COMPOSER_FLAGS
40 | - name: Run unit tests
41 | run: vendor/bin/phpunit --verbose --colors=always
42 |
--------------------------------------------------------------------------------
/tests/fixtures/data/related-rest-model.php:
--------------------------------------------------------------------------------
1 | 1,
8 | 'rest_model_id' => 1,
9 | 'subject' => 'Related Model 1',
10 | 'message' => 'This is the first related model',
11 | 'created_at' => 1555660035,
12 | 'updated_at' => 1555660035,
13 | 'created_by' => 'simialbi',
14 | 'updated_by' => 'simialbi',
15 | ],
16 | [
17 | 'id' => 2,
18 | 'rest_model_id' => 1,
19 | 'subject' => 'Related Model 2',
20 | 'message' => 'This is the second related model',
21 | 'created_at' => 1555660035,
22 | 'updated_at' => 1555660035,
23 | 'created_by' => 'simialbi',
24 | 'updated_by' => 'simialbi',
25 | ],
26 | [
27 | 'id' => 3,
28 | 'rest_model_id' => 1,
29 | 'subject' => 'Related Model 3',
30 | 'message' => 'This is the third related model',
31 | 'created_at' => 1555660035,
32 | 'updated_at' => 1555660035,
33 | 'created_by' => 'simialbi',
34 | 'updated_by' => 'simialbi',
35 | ],
36 | [
37 | 'id' => 1,
38 | 'rest_model_id' => 2,
39 | 'subject' => 'Related Model 2.1',
40 | 'message' => 'This is the first related model',
41 | 'created_at' => 1555660035,
42 | 'updated_at' => 1555660035,
43 | 'created_by' => 'simialbi',
44 | 'updated_by' => 'simialbi',
45 | ],
46 | [
47 | 'id' => 1,
48 | 'rest_model_id' => 2,
49 | 'subject' => 'Related Model 2.2',
50 | 'message' => 'This is the second related model',
51 | 'created_at' => 1555660035,
52 | 'updated_at' => 1555660035,
53 | 'created_by' => 'simialbi',
54 | 'updated_by' => 'simialbi',
55 | ],
56 | ];
57 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simialbi/yii2-rest-client",
3 | "type": "yii2-extension",
4 | "description": "REST client (AR-like model) for Yii Framework 2.0 (via yii2-http-client, extends ApexWire/yii2-restclient)",
5 | "keywords": [
6 | "yii2",
7 | "rest",
8 | "client",
9 | "model",
10 | "active record"
11 | ],
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "ApexWire",
16 | "email": "apexwire@gmail.com"
17 | },
18 | {
19 | "name": "simialbi",
20 | "email": "simi.albi@outlook.com"
21 | }
22 | ],
23 | "support": {
24 | "source": "https://github.com/simialbi/yii2-rest-client",
25 | "issues": "https://github.com/simialbi/yii2-rest-client/issues"
26 | },
27 | "minimum-stability": "beta",
28 | "require": {
29 | "php": ">=7.3",
30 | "yiisoft/yii2": "^2.0.14",
31 | "yiisoft/yii2-httpclient": "^2.0.0"
32 | },
33 | "require-dev": {
34 | "yiisoft/yii2-coding-standards": "~2.0",
35 | "phpunit/phpunit": "^9.6.22",
36 | "symplify/easy-coding-standard": "^12.1"
37 | },
38 | "autoload": {
39 | "psr-4": {
40 | "simialbi\\yii2\\rest\\": "src"
41 | }
42 | },
43 | "autoload-dev": {
44 | "psr-4": {
45 | "yiiunit\\extensions\\rest\\": "tests"
46 | }
47 | },
48 | "repositories": [
49 | {
50 | "type": "composer",
51 | "url": "https://asset-packagist.org"
52 | }
53 | ],
54 | "extra": {
55 | "branch-alias": {
56 | "dev-master": "2.0.x-dev"
57 | }
58 | },
59 | "config": {
60 | "allow-plugins": {
61 | "yiisoft/yii2-composer": true
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/conditions/HashConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use simialbi\yii2\rest\Query;
12 | use yii\db\conditions\InCondition;
13 | use yii\db\ExpressionInterface;
14 | use yii\helpers\ArrayHelper;
15 |
16 | /**
17 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
18 | */
19 | class HashConditionBuilder extends \yii\db\conditions\HashConditionBuilder
20 | {
21 | use ConditionBuilderTrait;
22 |
23 | public function build(ExpressionInterface $expression, array &$params = []): array
24 | {
25 | /** @var \yii\db\conditions\HashCondition $expression */
26 |
27 | $hash = $expression->getHash();
28 | $parts = [];
29 | foreach ($hash as $column => $value) {
30 | if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
31 | // IN condition
32 | $parts[] = $this->queryBuilder->buildCondition(new InCondition($column, 'IN', $value), $params);
33 | } else {
34 | if ($value === null) {
35 | $parts[] = [
36 | $column => null,
37 | ];
38 | } elseif ($value instanceof ExpressionInterface) {
39 | $parts[] = [
40 | $column => $this->queryBuilder->buildExpression($value, $params),
41 | ];
42 | } else {
43 | $phName = $this->queryBuilder->bindParam($value, $params);
44 | $parts[] = [
45 | $column => $phName,
46 | ];
47 | }
48 | }
49 | }
50 |
51 | return count($parts) === 1 ? $parts[0] : $parts;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/RelationTest.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest;
11 |
12 | use Yii;
13 | use yiiunit\extensions\rest\fixtures\RestModelFixture;
14 | use yiiunit\extensions\rest\models\RestModel;
15 |
16 | class RelationTest extends TestCase
17 | {
18 | protected function setUp(): void
19 | {
20 | parent::setUp();
21 |
22 | $this->mockWebApplication();
23 | Yii::$app->log->logger->flush();
24 | }
25 |
26 | public function testRelationGetAll()
27 | {
28 | $fixture = new RestModelFixture();
29 | $fixture->load();
30 |
31 | /** @var RestModel $model */
32 | $model = $fixture->getModel(0);
33 |
34 | $this->assertInstanceOf(RestModel::class, $model);
35 |
36 | Yii::$app->log->logger->flush();
37 |
38 | // var_dump($model);
39 | $model->getRelatedRests()->all();
40 |
41 | $logEntry = $this->parseLogs();
42 |
43 | $this->assertEquals('GET', $logEntry['method']);
44 | $this->assertStringStartsWith('https://api.site.com/related-rest-models?filter%5Brest_model_id%5D=1', $logEntry['url']);
45 | }
46 |
47 | public function testRelationGetOne()
48 | {
49 | $fixture = new RestModelFixture();
50 | $fixture->load();
51 |
52 | /** @var RestModel $model */
53 | $model = $fixture->getModel(0);
54 |
55 | $this->assertInstanceOf(RestModel::class, $model);
56 |
57 | Yii::$app->log->logger->flush();
58 |
59 | $related = $model->relatedRest;
60 |
61 | $logEntry = $this->parseLogs();
62 |
63 | $this->assertEquals('GET', $logEntry['method']);
64 | $this->assertStringStartsWith('https://api.site.com/related-rest-models?filter%5Brest_model_id%5D=1', $logEntry['url']);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/conditions/ConjunctionConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use yii\db\conditions\ConjunctionCondition;
12 | use yii\db\ExpressionInterface;
13 |
14 | /**
15 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
16 | */
17 | class ConjunctionConditionBuilder extends \yii\db\conditions\ConjunctionConditionBuilder
18 | {
19 | use ConditionBuilderTrait;
20 |
21 | /**
22 | * @throws \Exception
23 | */
24 | public function build(ExpressionInterface $condition, array &$params = []): array
25 | {
26 | /** @var ConjunctionCondition $condition */
27 | $parts = $this->buildExpressionsFrom($condition, $params);
28 |
29 | if (empty($parts)) {
30 | return [];
31 | }
32 |
33 | if (count($parts) === 1) {
34 | return $parts;
35 | }
36 |
37 | return [
38 | $this->getOperator($condition->getOperator()) => $parts,
39 | ];
40 | }
41 |
42 | /**
43 | * Builds expressions, that are stored in $condition
44 | *
45 | * @param ExpressionInterface|ConjunctionCondition $condition the expression to be built.
46 | * @param array $params the binding parameters.
47 | *
48 | * @return string[]
49 | */
50 | private function buildExpressionsFrom(ExpressionInterface $condition, array &$params = []): array
51 | {
52 | $parts = [];
53 | foreach ($condition->getExpressions() as $condition) {
54 | if (is_array($condition)) {
55 | $condition = $this->queryBuilder->buildCondition($condition, $params);
56 | }
57 | if ($condition instanceof ExpressionInterface) {
58 | $condition = $this->queryBuilder->buildExpression($condition, $params);
59 | }
60 | if ($condition !== '') {
61 | $parts[] = $condition;
62 | }
63 | }
64 |
65 | return $parts;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/conditions/LikeConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use yii\db\conditions\LikeCondition;
12 | use yii\db\ExpressionInterface;
13 |
14 | /**
15 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
16 | */
17 | class LikeConditionBuilder extends \yii\db\conditions\LikeConditionBuilder
18 | {
19 | use ConditionBuilderTrait;
20 |
21 | /**
22 | * @throws \Exception
23 | */
24 | public function build(ExpressionInterface $expression, array &$params = []): array
25 | {
26 | /** @var LikeCondition $expression */
27 | $operator = $expression->getOperator();
28 | $column = $expression->getColumn();
29 | $values = $expression->getValue();
30 | // $escape = $expression->getEscapingReplacements();
31 | // if ($escape === null || $escape === []) {
32 | // $escape = $this->escapingReplacements;
33 | // }
34 |
35 | [$andor, $not] = $this->parseOperator($operator);
36 |
37 | if (!is_array($values)) {
38 | $values = [$values];
39 | }
40 |
41 | if (empty($values)) {
42 | return $not ? [] : $this->queryBuilder->buildCondition([
43 | 0 => 1,
44 | ], $params);
45 | }
46 |
47 | $parts = [];
48 | foreach ($values as $value) {
49 | if ($value instanceof ExpressionInterface) {
50 | $phName = $this->queryBuilder->buildExpression($value, $params);
51 | } else {
52 | $phName = $this->queryBuilder->bindParam($value, $params);
53 | }
54 | $parts[] = [
55 | $column => [
56 | 'like' => reset($phName),
57 | ],
58 | ];
59 | }
60 |
61 | if (count($parts) === 1) {
62 | return reset($parts);
63 | }
64 |
65 | array_unshift($parts, $this->getOperator($andor));
66 |
67 | return $this->queryBuilder->buildCondition($parts, $params);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/conditions/InConditionBuilder.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace simialbi\yii2\rest\conditions;
10 |
11 | use simialbi\yii2\rest\Query;
12 | use yii\db\conditions\InCondition;
13 | use yii\db\ExpressionInterface;
14 |
15 | /**
16 | * @property \simialbi\yii2\rest\QueryBuilder $queryBuilder
17 | */
18 | class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
19 | {
20 | use ConditionBuilderTrait;
21 |
22 | public function build(ExpressionInterface $expression, array &$params = []): array
23 | {
24 | /** @var InCondition $expression */
25 |
26 | $operator = $expression->getOperator();
27 | $column = $expression->getColumn();
28 | $values = $expression->getValues();
29 |
30 | if ($column === []) {
31 | return [
32 | 0 => 1,
33 | ];
34 | }
35 |
36 | if ($values instanceof Query) {
37 | // TODO
38 | // return $this->buildSubqueryInCondition($operator, $column, $values, $params);
39 | return [];
40 | }
41 | if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
42 | // TODO
43 | // return $this->buildCompositeInCondition($operator, $column, $values, $params);
44 | return [];
45 | }
46 |
47 | if (is_array($column)) {
48 | $column = reset($column);
49 | }
50 |
51 | $sqlValues = $this->buildValues($expression, $values, $params);
52 | if (empty($sqlValues)) {
53 | return [
54 | 0 => 1,
55 | ];
56 | }
57 |
58 | if (count($sqlValues) > 1) {
59 | $operator = ($operator === 'IN') ? 'in' : 'nin';
60 | return [
61 | $column => [
62 | $operator => $sqlValues,
63 | ],
64 | ];
65 | }
66 |
67 | return $operator === 'IN'
68 | ? [
69 | $column => reset($sqlValues),
70 | ]
71 | : $this->queryBuilder->buildCondition([
72 | 'not', [
73 | $column => reset($sqlValues),
74 | ]], $params);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/UrlWithoutPluralisationTest.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest;
11 |
12 | use Yii;
13 | use yiiunit\extensions\rest\models\RestModel;
14 |
15 | class UrlWithoutPluralisationTest extends TestCase
16 | {
17 | protected function setUp(): void
18 | {
19 | parent::setUp();
20 |
21 | $this->mockWebApplication([
22 | 'components' => [
23 | 'rest' => [
24 | 'usePluralisation' => false,
25 | ],
26 | ],
27 | ]);
28 | Yii::$app->log->logger->flush();
29 | }
30 |
31 | public function testGetOne()
32 | {
33 | RestModel::findOne(1);
34 |
35 | $logEntry = $this->parseLogs();
36 |
37 | $this->assertEquals('GET', $logEntry['method']);
38 | $this->assertStringStartsWith('https://api.site.com/rest-model/1', $logEntry['url']);
39 | }
40 |
41 | public function testGetAnotherOne()
42 | {
43 | RestModel::find()->where([
44 | 'id' => 1,
45 | ])->one();
46 |
47 | $logEntry = $this->parseLogs();
48 |
49 | $this->assertEquals('GET', $logEntry['method']);
50 | $this->assertStringStartsWith('https://api.site.com/rest-model/1', $logEntry['url']);
51 | }
52 |
53 | public function testDeleteOne()
54 | {
55 | $model = new RestModel([
56 | 'oldAttributes' => [
57 | 'id' => 1,
58 | 'name' => 'test',
59 | 'description' => 'test',
60 | 'created_at' => time(),
61 | 'updated_at' => time(),
62 | 'created_by' => 'simialbi',
63 | 'updated_by' => 'simialbi',
64 | ],
65 | 'attributes' => [
66 | 'id' => 1,
67 | 'name' => 'test',
68 | 'description' => 'test',
69 | 'created_at' => time(),
70 | 'updated_at' => time(),
71 | 'created_by' => 'simialbi',
72 | 'updated_by' => 'simialbi',
73 | ],
74 | ]);
75 |
76 | $model->delete();
77 |
78 | $logEntry = $this->parseLogs();
79 |
80 | $this->assertEquals('DELETE', $logEntry['method']);
81 | $this->assertStringStartsWith('https://api.site.com/rest-model/1', $logEntry['url']);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/UrlTest.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace yiiunit\extensions\rest;
11 |
12 | use Yii;
13 | use yiiunit\extensions\rest\models\RestModel;
14 |
15 | class UrlTest extends TestCase
16 | {
17 | protected function setUp(): void
18 | {
19 | parent::setUp();
20 |
21 | $this->mockWebApplication();
22 | Yii::$app->log->logger->flush();
23 | }
24 |
25 | public function testGetOne()
26 | {
27 | RestModel::findOne(1);
28 |
29 | $logEntry = $this->parseLogs();
30 |
31 | $this->assertEquals('GET', $logEntry['method']);
32 | $this->assertStringStartsWith('https://api.site.com/rest-models/1', $logEntry['url']);
33 | }
34 |
35 | public function testGetAnotherOne()
36 | {
37 | RestModel::find()->where([
38 | 'id' => 1,
39 | ])->one();
40 |
41 | $logEntry = $this->parseLogs();
42 |
43 | $this->assertEquals('GET', $logEntry['method']);
44 | $this->assertStringStartsWith('https://api.site.com/rest-models/1', $logEntry['url']);
45 | }
46 |
47 | public function testFilter()
48 | {
49 | RestModel::find()->where([
50 | 'name' => 'John',
51 | ])->one();
52 |
53 | $logEntry = $this->parseLogs();
54 |
55 | $this->assertEquals('GET', $logEntry['method']);
56 | $this->assertStringStartsWith('https://api.site.com/rest-models?filter%5Bname%5D=John', $logEntry['url']);
57 | }
58 |
59 | public function testWithoutFilterKeyword()
60 | {
61 | $this->mockWebApplication([
62 | 'components' => [
63 | 'rest' => [
64 | 'useFilterKeyword' => false,
65 | ],
66 | ],
67 | ]);
68 |
69 | RestModel::find()->where([
70 | 'name' => 'John',
71 | ])->one();
72 |
73 | $logEntry = $this->parseLogs();
74 |
75 | $this->assertEquals('GET', $logEntry['method']);
76 | $this->assertStringStartsWith('https://api.site.com/rest-models?name=John', $logEntry['url']);
77 | }
78 |
79 | public function testDeleteOne()
80 | {
81 | $model = new RestModel([
82 | 'oldAttributes' => [
83 | 'id' => 1,
84 | 'name' => 'test',
85 | 'description' => 'test',
86 | 'created_at' => time(),
87 | 'updated_at' => time(),
88 | 'created_by' => 'simialbi',
89 | 'updated_by' => 'simialbi',
90 | ],
91 | 'attributes' => [
92 | 'id' => 1,
93 | 'name' => 'test',
94 | 'description' => 'test',
95 | 'created_at' => time(),
96 | 'updated_at' => time(),
97 | 'created_by' => 'simialbi',
98 | 'updated_by' => 'simialbi',
99 | ],
100 | ]);
101 |
102 | $model->delete();
103 |
104 | $logEntry = $this->parseLogs();
105 |
106 | $this->assertEquals('DELETE', $logEntry['method']);
107 | $this->assertStringStartsWith('https://api.site.com/rest-models/1', $logEntry['url']);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 |
9 | namespace yiiunit\extensions\rest;
10 |
11 | use Yii;
12 | use yii\di\Container;
13 | use yii\helpers\ArrayHelper;
14 |
15 | class TestCase extends \PHPUnit\Framework\TestCase
16 | {
17 | private $_index = 0;
18 |
19 | protected function tearDown(): void
20 | {
21 | parent::tearDown();
22 | $this->destroyApplication();
23 | }
24 |
25 |
26 | protected function mockWebApplication(array $config = [], string $appClass = '\yii\web\Application')
27 | {
28 | new $appClass(ArrayHelper::merge([
29 | 'id' => 'testapp',
30 | 'basePath' => __DIR__,
31 | 'vendorPath' => dirname(__DIR__) . '/vendor',
32 | 'aliases' => [
33 | '@bower' => '@vendor/bower-asset',
34 | '@npm' => '@vendor/npm-asset',
35 | ],
36 | 'components' => [
37 | 'request' => [
38 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq',
39 | 'scriptFile' => __DIR__ . '/index.php',
40 | 'scriptUrl' => '/index.php',
41 | ],
42 | 'rest' => [
43 | 'class' => 'simialbi\yii2\rest\Connection',
44 | 'baseUrl' => 'https://api.site.com/',
45 | 'isTestMode' => true,
46 | ],
47 | 'log' => [
48 | 'traceLevel' => 3,
49 | 'targets' => [
50 | [
51 | 'class' => 'yiiunit\extensions\rest\log\ArrayTarget',
52 | ],
53 | ],
54 | 'flushInterval' => 0,
55 | ],
56 | ],
57 | ], $config));
58 | }
59 |
60 | /**
61 | * Destroys application in Yii::$app by setting it to null.
62 | */
63 | protected function destroyApplication()
64 | {
65 | Yii::$app = null;
66 | Yii::$container = new Container();
67 | }
68 |
69 | /**
70 | * Parse log from index and returns data
71 | */
72 | protected function parseLogs(): array
73 | {
74 | $method = '';
75 | $url = '';
76 | $data = [];
77 | $headers = [];
78 |
79 | $profile = false;
80 | for (; $this->_index <= count(Yii::$app->log->logger->messages); $this->_index++) {
81 | $message = Yii::$app->log->logger->messages[$this->_index];
82 | if ($message[2] === 'simialbi\yii2\rest\Connection::request-method') {
83 | $method = $message[0];
84 | } elseif ($message[2] === 'simialbi\yii2\rest\Connection::request-url') {
85 | $url = $message[0];
86 | } elseif ($message[2] === 'simialbi\yii2\rest\Connection::request-data') {
87 | $data = $message[0];
88 | } elseif ($message[2] === 'simialbi\yii2\rest\Connection::request-headers') {
89 | $data = $message[0];
90 | } elseif ($message[2] === 'simialbi\yii2\rest\Connection::request') {
91 | if ($profile) {
92 | break;
93 | }
94 | $profile = true;
95 | }
96 | }
97 |
98 | return [
99 | 'method' => $method,
100 | 'url' => $url,
101 | 'data' => $data,
102 | 'headers' => $headers,
103 | ];
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/ActiveFixture.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright Copyright © 2019 Simon Karlen
8 | */
9 |
10 | namespace simialbi\yii2\rest;
11 |
12 | use yii\base\InvalidConfigException;
13 | use yii\helpers\Inflector;
14 | use yii\helpers\StringHelper;
15 | use yii\test\BaseActiveFixture;
16 |
17 | class ActiveFixture extends BaseActiveFixture
18 | {
19 | /**
20 | * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
21 | * After the DbFixture object is created, if you want to change this property, you should only assign it
22 | * with a DB connection object.
23 | * Starting from version 2.0.2, this can also be a configuration array for creating the object.
24 | */
25 | public $db = 'rest';
26 | /**
27 | * @var string the name of the model that this fixture is about. If this property is not set,
28 | * the model name will be determined via [[modelClass]].
29 | * @see modelClass
30 | */
31 | public $modelName;
32 |
33 | /**
34 | * @var \yii\db\ActiveRecord[] the loaded AR models
35 | */
36 | private $_models = [];
37 |
38 |
39 | /**
40 | * @throws InvalidConfigException
41 | */
42 | public function init()
43 | {
44 | parent::init();
45 | if ($this->modelClass === null && $this->modelName === null) {
46 | throw new InvalidConfigException('Either "modelClass" or "modelName" must be set.');
47 | }
48 | if ($this->modelName === null) {
49 | $this->modelName = Inflector::camel2id(StringHelper::basename($this->modelClass), '-');
50 | }
51 | }
52 |
53 | /**
54 | * @throws InvalidConfigException
55 | * @throws \ReflectionException
56 | */
57 | public function load()
58 | {
59 | $this->data = [];
60 | foreach ($this->getData() as $alias => $row) {
61 | $this->data[$alias] = $row;
62 | }
63 | }
64 |
65 | /**
66 | * @throws InvalidConfigException
67 | */
68 | protected function getData(): array
69 | {
70 | if ($this->dataFile === null) {
71 | if ($this->dataDirectory !== null) {
72 | $dataFile = $this->modelName . '.php';
73 | } else {
74 | $class = new \ReflectionClass($this);
75 | $dataFile = dirname($class->getFileName()) . '/data/' . $this->modelName . '.php';
76 | }
77 |
78 | return $this->loadData($dataFile, false);
79 | }
80 | return parent::getData();
81 | }
82 |
83 | public function getModel($name)
84 | {
85 | if (!isset($this->data[$name])) {
86 | return null;
87 | }
88 | if (array_key_exists($name, $this->_models)) {
89 | return $this->_models[$name];
90 | }
91 |
92 | if ($this->modelClass === null) {
93 | throw new InvalidConfigException('The "modelClass" property must be set.');
94 | }
95 | $row = $this->data[$name];
96 | /** @var ActiveRecord $modelClass */
97 | $modelClass = $this->modelClass;
98 | $keys = [];
99 | foreach ($modelClass::primaryKey() as $key) {
100 | $keys[$key] = $row[$key] ?? null;
101 | }
102 |
103 | /** @var ActiveRecord $model */
104 | $model = new $modelClass();
105 | $model->setOldAttributes($row);
106 | $model->setAttributes($row, false);
107 |
108 | return $this->_models[$name] = $model;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | modelClass = $modelClass;
38 | parent::__construct($config);
39 | }
40 |
41 | /**
42 | * @param Query $from
43 | */
44 | public static function create($from): self
45 | {
46 | $modelClass = ($from->hasProperty('modelClass')) ? $from->modelClass : null;
47 |
48 | return new self($modelClass, [
49 | 'where' => $from->where,
50 | 'limit' => $from->limit,
51 | 'offset' => $from->offset,
52 | 'orderBy' => $from->orderBy,
53 | 'indexBy' => $from->indexBy,
54 | 'select' => $from->select,
55 | 'selectOption' => $from->selectOption,
56 | 'distinct' => $from->distinct,
57 | 'from' => $from->from,
58 | 'groupBy' => $from->groupBy,
59 | 'join' => $from->join,
60 | 'having' => $from->having,
61 | 'union' => $from->union,
62 | 'params' => $from->params,
63 | ]);
64 | }
65 |
66 | /**
67 | * Prepares for building query.
68 | * This method is called by [[QueryBuilder]] when it starts to build SQL from a query object.
69 | * You may override this method to do some final preparation work when converting a query into a SQL statement.
70 | *
71 | * @param QueryBuilder $builder
72 | *
73 | * @return $this a prepared query instance which will be used by [[QueryBuilder]] to build the SQL
74 | */
75 | public function prepare($builder): self
76 | {
77 | return $this;
78 | }
79 |
80 | /**
81 | * Returns the number of records.
82 | *
83 | * @param string $q the COUNT expression. Defaults to '*'.
84 | * @param Connection|null $db the database connection used to execute the query.
85 | * If this parameter is not given, the `db` application component will be used.
86 | *
87 | * @return int number of records.
88 | * @throws \yii\base\InvalidConfigException
89 | * @throws \yii\db\Exception
90 | * @throws \yii\base\NotSupportedException
91 | */
92 | public function count($q = '*', $db = null): int
93 | {
94 | if ($this->emulateExecution) {
95 | return 0;
96 | }
97 |
98 | $result = $this->createCommand($db)->execute('head');
99 |
100 | /** @var \yii\web\HeaderCollection $result */
101 | return (int) $result->get('x-pagination-total-count');
102 | }
103 |
104 | /**
105 | * Creates a DB command that can be used to execute this query.
106 | *
107 | * @param Connection|null $db the connection used to generate the statement.
108 | * If this parameter is not given, the `rest` application component will be used.
109 | *
110 | * @return Command the created DB command instance.
111 | * @throws \yii\base\InvalidConfigException
112 | * @throws \yii\db\Exception
113 | * @throws \yii\base\NotSupportedException
114 | */
115 | public function createCommand($db = null): Command
116 | {
117 | if ($db === null) {
118 | $db = Yii::$app->get(Connection::getDriverName());
119 | }
120 |
121 | $commandConfig = $db->getQueryBuilder()->build($this);
122 | $command = $db->createCommand($commandConfig);
123 | $this->setCommandCache($command);
124 |
125 | return $command;
126 | }
127 |
128 | /**
129 | * @throws \yii\base\InvalidConfigException
130 | * @throws \yii\db\Exception
131 | * @throws \yii\base\NotSupportedException
132 | */
133 | public function exists($db = null): bool
134 | {
135 | if ($this->emulateExecution) {
136 | return false;
137 | }
138 |
139 | $result = $this->createCommand($db)->execute('head');
140 |
141 | /** @var \yii\web\HeaderCollection $result */
142 | return $result->get('x-pagination-total-count', 0) > 0;
143 | }
144 |
145 | /**
146 | * Sets the model to read from / write to
147 | *
148 | * @param string $tables
149 | *
150 | * @return $this the query object itself
151 | */
152 | public function from($tables): Query
153 | {
154 | $this->from = $tables;
155 |
156 | return $this;
157 | }
158 |
159 | /**
160 | * Getter for modelClass
161 | * @return mixed
162 | */
163 | public function getModelClass()
164 | {
165 | return $this->_modelClass;
166 | }
167 |
168 | /**
169 | * Setter for modelClass
170 | *
171 | * @param mixed $modelClass
172 | */
173 | public function setModelClass($modelClass)
174 | {
175 | $this->_modelClass = $modelClass;
176 | }
177 |
178 | /**
179 | * @param Command $command
180 | */
181 | protected function setCommandCache($command): Command
182 | {
183 | /** @var \yii\db\Command $command */
184 | $command = parent::setCommandCache($command);
185 | /** @var Command $command */
186 | return $command;
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # REST Client for Yii 2 (ActiveRecord-like model)
2 | This extension provides an interface to work with RESTful API via ActiveRecord-like model in Yii 2.
3 | It is based on [ApexWire's](https://github.com/ApexWire) [yii2-restclient](https://github.com/ApexWire/yii2-restclient).
4 |
5 |
6 | [](https://packagist.org/packages/simialbi/yii2-rest-client)
7 | [](https://packagist.org/packages/simialbi/yii2-rest-client)
8 | [](https://packagist.org/packages/simialbi/yii2-rest-client)
9 | 
10 |
11 | ## Resources
12 | * [yii2-restclient](https://github.com/ApexWire/yii2-restclient)
13 | * [\yii\db\ActiveRecord](http://www.yiiframework.com/doc-2.0/guide-db-active-record.html)
14 |
15 | ## Installation
16 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
17 |
18 | Either run
19 |
20 | ```
21 | $ php composer.phar require --prefer-dist simialbi/yii2-rest-client
22 | ```
23 |
24 | or add
25 |
26 | ```
27 | "simialbi/yii2-rest-client": "*"
28 | ```
29 |
30 | to the `require` section of your `composer.json`.
31 |
32 | ## Configuration
33 | To use this extension, configure restclient component in your application config:
34 |
35 | ```php
36 | 'components' => [
37 | 'rest' => [
38 | 'class' => 'simialbi\yii2\rest\Connection',
39 | 'baseUrl' => 'https://api.site.com/',
40 | // 'auth' => function (simialbi\yii2\rest\Connection $db) {
41 | // return 'Bearer: ';
42 | // },
43 | // 'auth' => 'Bearer: ',
44 | // 'usePluralisation' => false,
45 | // 'useFilterKeyword' => false,
46 | // 'enableExceptions' => true,
47 | // 'itemsProperty' => 'items'
48 | ],
49 | ],
50 | ```
51 |
52 | | Parameter | Default | Description |
53 | | ------------------- | -----------| ------------------------------------------------------------------------------------------------------------- |
54 | | `baseUrl` | `''` | The location of the api. E.g. for http://api.site.com/v1/users the `baseUrl` would be http://api.site.com/v1/ (required) |
55 | | `auth` | | Either a Closure which returns a `string` or a `string`. The rest connection will be passed as parameter. |
56 | | `usePluralisation` | `true` | Whether to use plural version for lists (index action) or not (e.g. http://api.site.com/users instead of `user`) |
57 | | `useFilterKeyword` | `true` | Whether to use "filter" key word in url parameters when filtering (e.g. ?filter[name]=user instead of ?name=user |
58 | | `enableExceptions` | `false` | Whether the connection should throw an exception if response is not 200 or not |
59 | | `itemsProperty` | | If your items are wrapped inside a property (e.g. `items`), set it's name here |
60 | | `requestConfig` | `[]` | Client request configuration |
61 | | `responseConfig` | `[]` | Client response configuration |
62 | | `updateMethod` | `'put'` | The method to use for update operations. |
63 | | `isTestMode` | `false` | Whether we are in test mode or not (prevent execution) |
64 | | `enableQueryCache` | `false` | Whether to enable query caching |
65 | | `queryCacheDuration`| `3600` | The default number of seconds that query results can remain valid in cache |
66 | | `queryCache` | `'cache'` | The cache object or the ID of the cache application component |
67 |
68 | ## Usage
69 | Define your Model
70 |
71 | ```php
72 | Important: Be sure to either define the properties of the object like in the example above (`@property` syntax in phpdoc)
129 | > or override the `attributes()` method to return the allowed attributes as array
130 |
131 | > The same about relations. Be sure to either define them via `@property-read` phpdoc comment or override the `getRelations`
132 | > method. If the related class has not the same namespace as the main class, be sure to use the fully qualified class name
133 | > (e.g. `@property-read \app\models\OtherModel[] $otherModels`)
134 |
135 | ## License
136 |
137 | **yii2-rest-client** is released under MIT license. See bundled [LICENSE](LICENSE) for details.
138 |
139 | ## Acknowledgments
140 | * [ApexWire's](https://github.com/ApexWire) [yii2-restclient](https://github.com/ApexWire/yii2-restclient)
141 | * [Yii2 HiArt](https://github.com/hiqdev/yii2-hiart).
142 | * [mikolajzieba](https://github.com/mikolajzieba)
143 |
--------------------------------------------------------------------------------
/src/ActiveRecord.php:
--------------------------------------------------------------------------------
1 | get(Connection::getDriverName());
53 | }
54 |
55 | /**
56 | * Declares the name of the url path associated with this AR class.
57 | *
58 | * By default this method returns the class name as the path by calling [[Inflector::camel2id()]].
59 | * For example:
60 | * `Customer` becomes `customer`, and `OrderItem` becomes `order-item`. You may override this method
61 | * if the path is not named after this convention.
62 | *
63 | * @return string the url path
64 | * @throws InvalidConfigException
65 | */
66 | public static function modelName(): string
67 | {
68 | $path = Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
69 | return static::getDb()->usePluralisation ? Inflector::pluralize($path) : $path;
70 | }
71 |
72 | public function attributes(): array
73 | {
74 | if (empty($this->_attributeFields)) {
75 | $regex = '#^@property(?:-(read|write))?(?:\s+([^\s]+))?\s+\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)#';
76 | $typeRegex = '#^(bool(ean)?|int(eger)?|float|double|string|array)$#';
77 | $reflection = new \ReflectionClass($this);
78 | $docLines = preg_split('~\R~u', $reflection->getDocComment());
79 | foreach ($docLines as $docLine) {
80 | $matches = [];
81 | $docLine = ltrim($docLine, "\t* ");
82 | if (preg_match($regex, $docLine, $matches) && isset($matches[3])) {
83 | if ($matches[1] === 'read' || (!empty($matches[2]) && !preg_match($typeRegex, $matches[2]))) {
84 | continue;
85 | }
86 | $this->_attributeFields[] = $matches[3];
87 | }
88 | }
89 | }
90 |
91 | return $this->_attributeFields;
92 | }
93 |
94 | /**
95 | * @throws InvalidConfigException
96 | * @throws Exception
97 | */
98 | public function insert($runValidation = true, $attributes = null): bool
99 | {
100 | if ($runValidation && !$this->validate($attributes)) {
101 | Yii::info('Model not inserted due to validation error.', __METHOD__);
102 |
103 | return false;
104 | }
105 |
106 | return $this->insertInternal($attributes);
107 | }
108 |
109 | /**
110 | * @throws InvalidConfigException
111 | */
112 | public function update($runValidation = true, $attributeNames = null)
113 | {
114 | if ($runValidation && !$this->validate($attributeNames)) {
115 | Yii::info('Model not inserted due to validation error.', __METHOD__);
116 |
117 | return false;
118 | }
119 |
120 | return $this->updateInternal($attributeNames);
121 | }
122 |
123 | /**
124 | * @throws InvalidConfigException
125 | */
126 | public function delete()
127 | {
128 | $result = false;
129 | if ($this->beforeDelete()) {
130 | $command = static::getDb()->createCommand();
131 | $result = $command->delete(static::modelName(), $this->getOldPrimaryKey());
132 |
133 | $this->setOldAttributes(null);
134 | $this->afterDelete();
135 | }
136 |
137 | return $result;
138 | }
139 |
140 | /**
141 | * @throws NotSupportedException
142 | */
143 | public function unlinkAll($name, $delete = false)
144 | {
145 | throw new NotSupportedException('unlinkAll() is not supported by RestClient, use unlink() instead.');
146 | }
147 |
148 | /**
149 | * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface
150 | */
151 | public function hasOne($class, $link)
152 | {
153 | return parent::hasOne($class, $link);
154 | }
155 |
156 | /**
157 | * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface
158 | */
159 | public function hasMany($class, $link)
160 | {
161 | return parent::hasMany($class, $link);
162 | }
163 |
164 | /**
165 | * Inserts an ActiveRecord.
166 | *
167 | * @param array|null $attributes list of attributes that need to be saved. Defaults to `null`,
168 | * meaning all attributes that are loaded from DB will be saved.
169 | *
170 | * @return boolean whether the record is inserted successfully.
171 | * @throws InvalidConfigException
172 | * @throws Exception
173 | */
174 | protected function insertInternal(?array $attributes): bool
175 | {
176 | if (!$this->beforeSave(true)) {
177 | return false;
178 | }
179 | $values = $this->getDirtyAttributes($attributes);
180 | if (false === ($data = static::getDb()->createCommand()->insert(static::modelName(), $values))) {
181 | return false;
182 | }
183 | foreach ($data as $name => $value) {
184 | $this->setAttribute($name, $value);
185 | $this->setOldAttribute($name, $value);
186 | }
187 |
188 | $changedAttributes = array_fill_keys(array_keys($values), null);
189 | $this->afterSave(true, $changedAttributes);
190 |
191 | return true;
192 | }
193 |
194 | /**
195 | * @throws InvalidConfigException
196 | * @throws \yii\db\Exception
197 | */
198 | protected function updateInternal($attributes = null)
199 | {
200 | if (!$this->beforeSave(false)) {
201 | return false;
202 | }
203 | $values = $this->getDirtyAttributes($attributes);
204 | if (empty($values)) {
205 | $this->afterSave(false, $values);
206 |
207 | return 0;
208 | }
209 |
210 | $command = static::getDb()->createCommand();
211 | $rows = $command->update(static::modelName(), $values, $this->getOldPrimaryKey(false));
212 |
213 | $changedAttributes = [];
214 | foreach ($values as $name => $value) {
215 | $changedAttributes[$name] = $this->getOldAttribute($name);
216 | $this->setOldAttribute($name, $value);
217 | }
218 | $this->afterSave(false, $changedAttributes);
219 |
220 | return $rows;
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/src/QueryBuilder.php:
--------------------------------------------------------------------------------
1 | 'buildAndCondition',
46 | 'IN' => 'buildInCondition',
47 | ];
48 |
49 | /**
50 | * @param mixed $connection the database connection.
51 | * @param array $config name-value pairs that will be used to initialize the object properties
52 | */
53 | public function __construct($connection, array $config = [])
54 | {
55 | parent::__construct($connection, $config);
56 | }
57 |
58 | /**
59 | * Build query data
60 | *
61 | * @param Query $query
62 | * @param array $params
63 | *
64 | * @throws \yii\db\Exception
65 | */
66 | public function build($query, $params = []): array
67 | {
68 | $query = $query->prepare($this);
69 |
70 | $params = empty($params) ? $query->params : array_merge($params, $query->params);
71 |
72 | $clauses = [
73 | 'fields' => $this->buildSelect($query->select, $params),
74 | 'pathInfo' => $this->buildFrom($query->from, $params),
75 | 'expand' => $this->buildJoin($query->join, $params),
76 | 'filter' => $this->buildWhere($query->where, $params),
77 | 'sort' => $this->buildOrderBy($query->orderBy),
78 | ];
79 |
80 | $clauses = array_merge($clauses, $this->buildLimit($query->limit, $query->offset));
81 |
82 | return [
83 | 'modelClass' => ArrayHelper::getValue($query, 'modelClass', ''),
84 | 'pathInfo' => ArrayHelper::remove($clauses, 'pathInfo'),
85 | 'queryParams' => array_filter($clauses, function ($value) {
86 | return $value !== '' && $value !== [] && $value !== null && (!is_string($value) || trim($value) !== '');
87 | }),
88 | ];
89 | }
90 |
91 | public function buildSelect($columns, &$params, $distinct = false, $selectOption = null): string
92 | {
93 | if (!empty($columns) && is_array($columns)) {
94 | return implode($this->separator, $columns);
95 | }
96 |
97 | return '';
98 | }
99 |
100 | /**
101 | * @param string $tables
102 | * @param array $params the binding parameters to be populated
103 | *
104 | * @return string the model name
105 | */
106 | public function buildFrom($tables, &$params): string
107 | {
108 | if (!is_string($tables)) {
109 | return '';
110 | }
111 |
112 | return trim($tables);
113 | }
114 |
115 | public function buildJoin($joins, &$params): string
116 | {
117 | if (empty($joins)) {
118 | return '';
119 | }
120 |
121 | $expand = [];
122 | foreach ($joins as $i => $join) {
123 | if (empty($join)) {
124 | continue;
125 | }
126 | if (is_array($join)) {
127 | $expand[] = $join[1];
128 | continue;
129 | }
130 | $expand[] = $join;
131 | }
132 |
133 | return implode($this->separator, $expand);
134 | }
135 |
136 | /**
137 | * @param string|array $condition
138 | * @param array $params the binding parameters to be populated
139 | *
140 | * @return array the WHERE clause built from [[Query::$where]].
141 | */
142 | public function buildWhere($condition, &$params): array
143 | {
144 | return $this->buildCondition($condition, $params);
145 | }
146 |
147 | public function buildCondition($condition, &$params): array
148 | {
149 | if (empty($condition) || !is_array($condition)) {
150 | return [];
151 | }
152 |
153 | $condition = $this->createConditionFromArray($condition);
154 | /** @var \yii\db\conditions\SimpleCondition $condition */
155 |
156 | return $this->buildExpression($condition, $params);
157 | }
158 |
159 | public function buildExpression(ExpressionInterface $expression, &$params = []): array
160 | {
161 | return (array) parent::buildExpression($expression, $params);
162 | }
163 |
164 | public function buildOrderBy($columns): string
165 | {
166 | if (empty($columns)) {
167 | return '';
168 | }
169 |
170 | $orders = [];
171 | foreach ($columns as $name => $direction) {
172 | if ($direction instanceof Expression) {
173 | $orders[] = $direction->expression;
174 | } else {
175 | $orders[] = ($direction === SORT_DESC ? '-' : '') . $name;
176 | }
177 | }
178 |
179 | return implode($this->separator, $orders);
180 | }
181 |
182 | public function buildLimit($limit, $offset): array
183 | {
184 | $clauses = [];
185 | if ($this->hasLimit($limit)) {
186 | $clauses['per-page'] = (string) $limit;
187 | }
188 | if ($this->hasOffset($offset)) {
189 | $offset = intval((string) $offset);
190 | $clauses['page'] = ceil($offset / $limit) + 1;
191 | }
192 |
193 | return $clauses;
194 | }
195 |
196 | /**
197 | * @return mixed
198 | */
199 | public function bindParam($value, &$params)
200 | {
201 | return $value;
202 | }
203 |
204 | /**
205 | * @return string[]
206 | */
207 | protected function defaultExpressionBuilders(): array
208 | {
209 | return [
210 | 'yii\db\Query' => 'yii\db\QueryExpressionBuilder',
211 | 'yii\db\PdoValue' => 'yii\db\PdoValueBuilder',
212 | 'yii\db\Expression' => 'yii\db\ExpressionBuilder',
213 | 'yii\db\conditions\ConjunctionCondition' => 'simialbi\yii2\rest\conditions\ConjunctionConditionBuilder',
214 | 'yii\db\conditions\NotCondition' => 'simialbi\yii2\rest\conditions\NotConditionBuilder',
215 | 'yii\db\conditions\AndCondition' => 'simialbi\yii2\rest\conditions\ConjunctionConditionBuilder',
216 | 'yii\db\conditions\OrCondition' => 'simialbi\yii2\rest\conditions\ConjunctionConditionBuilder',
217 | 'yii\db\conditions\BetweenCondition' => 'simialbi\yii2\rest\conditions\BetweenConditionBuilder',
218 | 'yii\db\conditions\InCondition' => 'simialbi\yii2\rest\conditions\InConditionBuilder',
219 | 'yii\db\conditions\LikeCondition' => 'simialbi\yii2\rest\conditions\LikeConditionBuilder',
220 | // 'yii\db\conditions\ExistsCondition' => 'yii\db\conditions\ExistsConditionBuilder',
221 | 'yii\db\conditions\SimpleCondition' => 'simialbi\yii2\rest\conditions\SimpleConditionBuilder',
222 | 'yii\db\conditions\HashCondition' => 'simialbi\yii2\rest\conditions\HashConditionBuilder',
223 | // 'yii\db\conditions\BetweenColumnsCondition' => 'yii\db\conditions\BetweenColumnsConditionBuilder'
224 | ];
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/Command.php:
--------------------------------------------------------------------------------
1 | queryCacheDuration = $duration === null ? $this->db->queryCacheDuration : $duration;
73 | $this->queryCacheDependency = $dependency;
74 | return $this;
75 | }
76 |
77 | /**
78 | * Disables query cache for this command.
79 | * @return $this the command object itself
80 | */
81 | public function noCache(): Command
82 | {
83 | $this->queryCacheDuration = -1;
84 | return $this;
85 | }
86 |
87 | /**
88 | * Returns the raw url by inserting parameter values into the corresponding placeholders.
89 | * Note that the return value of this method should mainly be used for logging purpose.
90 | * It is likely that this method returns an invalid URL due to improper replacement of parameter placeholders.
91 | * @return string the raw URL with parameter values inserted into the corresponding placeholders.
92 | */
93 | public function getRawUrl(): string
94 | {
95 | return $this->db->handler->get($this->pathInfo, $this->queryParams)->fullUrl;
96 | }
97 |
98 | /**
99 | * Executes the SQL statement and returns ALL rows at once.
100 | *
101 | * @param int|null $fetchMode for compatibility with [[\yii\db\Command]]
102 | *
103 | * @return array all rows of the query result. Each array element is an array representing a row of data.
104 | * An empty array is returned if the query results in nothing.
105 | * @throws InvalidConfigException
106 | */
107 | public function queryAll(?int $fetchMode = null): array
108 | {
109 | return $this->queryInternal();
110 | }
111 |
112 | /**
113 | * Executes the SQL statement and returns the first row of the result.
114 | * This method is best used when only the first row of result is needed for a query.
115 | *
116 | * @param int|null $fetchMode for compatibility with [[\yii\db\Command]]
117 | *
118 | * @return array|false the first row (in terms of an array) of the query result. False is returned if the query
119 | * results in nothing.
120 | * @throws \yii\base\InvalidConfigException
121 | */
122 | public function queryOne(?int $fetchMode = null)
123 | {
124 | $class = $this->modelClass;
125 |
126 | if (!empty($class) && class_exists($class)) {
127 | /** @var ActiveRecord $class */
128 | $pks = $class::primaryKey();
129 |
130 | if (count($pks) === 1 && isset($this->queryParams['filter'])) {
131 | $primaryKey = current($pks);
132 | $currentKey = ArrayHelper::remove($this->queryParams['filter'], $primaryKey);
133 | if ($currentKey) {
134 | $this->pathInfo .= '/' . $currentKey;
135 | }
136 | }
137 | }
138 |
139 | return $this->queryInternal();
140 | }
141 |
142 | /**
143 | * Make request and check for error.
144 | *
145 | * @return mixed
146 | * @throws InvalidConfigException
147 | */
148 | public function execute(string $method = 'get')
149 | {
150 | return $this->queryInternal($method);
151 | }
152 |
153 | /**
154 | * Creates a new record
155 | *
156 | * @return array|false
157 | * @throws Exception
158 | */
159 | public function insert(string $model, array $columns)
160 | {
161 | $this->pathInfo = $model;
162 |
163 | return $this->db->post($this->pathInfo, $columns);
164 | }
165 |
166 | /**
167 | * Updates an existing record
168 | *
169 | * @return mixed
170 | */
171 | public function update(string $model, array $data = [], ?string $id = null)
172 | {
173 | $method = $this->db->updateMethod;
174 | $this->pathInfo = $model;
175 | if ($id) {
176 | $this->pathInfo .= '/' . $id;
177 | }
178 |
179 | return $this->db->$method($this->pathInfo, $data);
180 | }
181 |
182 | /**
183 | * Deletes a record
184 | *
185 | * @return array|false
186 | * @throws Exception
187 | */
188 | public function delete(string $model, $id = null)
189 | {
190 | $id = (string) $id;
191 | $this->pathInfo = $model;
192 | if ($id) {
193 | $this->pathInfo .= '/' . $id;
194 | }
195 |
196 | return $this->db->delete($this->pathInfo);
197 | }
198 |
199 | /**
200 | * Performs the actual statement
201 | *
202 | * @return mixed
203 | * @throws InvalidConfigException
204 | */
205 | protected function queryInternal(string $method = 'get')
206 | {
207 | if ($this->db->usePluralisation && strpos($this->pathInfo, '/') === false) {
208 | $this->pathInfo = Inflector::pluralize($this->pathInfo);
209 | }
210 | if (!$this->db->useFilterKeyword) {
211 | $filter = ArrayHelper::remove($this->queryParams, 'filter', []);
212 | $this->queryParams = array_merge($this->queryParams, $filter);
213 | }
214 | $info = $this->db->getQueryCacheInfo($this->queryCacheDuration, $this->queryCacheDependency);
215 | if (is_array($info)) {
216 | /** @var \yii\caching\CacheInterface $cache */
217 | $cache = $info[0];
218 | $cacheKey = $this->getCacheKey($method);
219 | $result = $cache->get($cacheKey);
220 | if (is_array($result) && isset($result[0])) {
221 | Yii::debug('Query result served from cache', 'simialbi\yii2\rest\Command::query');
222 | return $result[0];
223 | }
224 | }
225 |
226 | $result = $this->db->$method($this->pathInfo, $this->queryParams);
227 | if ($this->db->itemsProperty) {
228 | $result = ArrayHelper::getValue($result, $this->db->itemsProperty, []);
229 | }
230 | if (isset($cache, $cacheKey, $info)) {
231 | $cache->set($cacheKey, [$result], $info[1], $info[2]);
232 | Yii::debug('Saved query result in cache', 'simialbi\yii2\rest\Command::query');
233 | }
234 |
235 | return $result;
236 | }
237 |
238 | /**
239 | * Returns the cache key for the query.
240 | *
241 | * @return array the cache key
242 | * @since 2.0.16
243 | */
244 | protected function getCacheKey(string $method): array
245 | {
246 | return [
247 | __CLASS__,
248 | $method,
249 | $this->pathInfo,
250 | $this->queryParams,
251 | ];
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/ActiveQuery.php:
--------------------------------------------------------------------------------
1 | modelClass;
44 |
45 | if ($db === null) {
46 | $db = $modelClass::getDb();
47 | }
48 |
49 | if ($this->from === null) {
50 | $this->from($modelClass::modelName());
51 | }
52 |
53 | return parent::createCommand($db);
54 | }
55 |
56 | /**
57 | * @throws InvalidConfigException
58 | */
59 | public function prepare($builder): Query
60 | {
61 | if (!empty($this->joinWith)) {
62 | $this->buildJoinWith();
63 | $this->joinWith = null;
64 | }
65 |
66 | if ($this->primaryModel === null) {
67 | // eager loading
68 | $query = Query::create($this);
69 | } else {
70 | // lazy loading of a relation
71 | $where = $this->where;
72 |
73 | if ($this->via instanceof self) {
74 | // via junction table
75 | $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
76 | $this->filterByModels($viaModels);
77 | } elseif (is_array($this->via)) {
78 | // via relation
79 | /** @var ActiveQuery $viaQuery */
80 | [$viaName, $viaQuery] = $this->via;
81 | if ($viaQuery->multiple) {
82 | if ($this->primaryModel->isRelationPopulated($viaName)) {
83 | $viaModels = $this->primaryModel->$viaName;
84 | } else {
85 | $viaModels = $viaQuery->all();
86 | $this->primaryModel->populateRelation($viaName, $viaModels);
87 | }
88 | } else {
89 | if ($this->primaryModel->isRelationPopulated($viaName)) {
90 | $model = $this->primaryModel->$viaName;
91 | } else {
92 | $model = $viaQuery->one();
93 | $this->primaryModel->populateRelation($viaName, $model);
94 | }
95 | $viaModels = $model === null ? [] : [$model];
96 | }
97 | $this->filterByModels($viaModels);
98 | } else {
99 | $this->filterByModels([$this->primaryModel]);
100 | }
101 |
102 | $query = Query::create($this);
103 | $this->andWhere($where);
104 | }
105 |
106 | return $query;
107 | }
108 |
109 | public function all($db = null): array
110 | {
111 | return parent::all($db);
112 | }
113 |
114 | /**
115 | * @throws InvalidConfigException
116 | */
117 | public function one($db = null)
118 | {
119 | $row = parent::one($db);
120 | if ($row !== false) {
121 | $models = $this->populate(isset($row[0]) ? $row : [$row]);
122 |
123 | return reset($models) ?: null;
124 | }
125 |
126 | return null;
127 | }
128 |
129 | /**
130 | * @throws InvalidConfigException
131 | */
132 | public function populate($rows): array
133 | {
134 | if (empty($rows)) {
135 | return [];
136 | }
137 |
138 | $models = $this->createModels($rows);
139 | if (!empty($this->join) && $this->indexBy === null) {
140 | $models = $this->removeDuplicatedModels($models);
141 | }
142 | if (!empty($this->with)) {
143 | $this->findWith($this->with, $models);
144 | }
145 | if (!$this->asArray) {
146 | foreach ($models as $model) {
147 | $model->afterFind();
148 | }
149 | } elseif ($this->indexBy !== null) {
150 | $models = ArrayHelper::index($models, $this->indexBy);
151 | }
152 |
153 | return $models;
154 | }
155 |
156 | /**
157 | * Joins with the specified relations.
158 | *
159 | * This method allows you to reuse existing relation definitions to perform JOIN queries.
160 | * Based on the definition of the specified relation(s), the method will append one or multiple
161 | * JOIN statements to the current query.
162 | *
163 | * @param string|array $with the relations to be joined. This can either be a string, representing a relation name
164 | * or an array with the following semantics:
165 | *
166 | * - Each array element represents a single relation.
167 | * - You may specify the relation name as the array key and provide an anonymous functions that
168 | * can be used to modify the relation queries on-the-fly as the array value.
169 | * - If a relation query does not need modification, you may use the relation name as the array value.
170 | *
171 | * Sub-relations can also be specified, see [[with()]] for the syntax.
172 | *
173 | * In the following you find some examples:
174 | *
175 | * ```php
176 | * // find all orders that contain books, and eager loading "books"
177 | * Order::find()->joinWith('books')->all();
178 | * // find all orders, eager loading "books", and sort the orders and books by the book names.
179 | * Order::find()->joinWith([
180 | * 'books' => function (\simialbi\yii2\rest\ActiveQuery $query) {
181 | * $query->orderBy('item.name');
182 | * }
183 | * ])->all();
184 | * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table
185 | * Order::find()->joinWith(['books b'])->where(['b.category' => 'Science fiction'])->all();
186 | * ```
187 | *
188 | * @return $this the query object itself
189 | */
190 | public function joinWith($with): ActiveQuery
191 | {
192 | $this->joinWith[] = (array) $with;
193 | return $this;
194 | }
195 |
196 | /**
197 | * Modifies the current query by adding join fragments based on the given relations.
198 | *
199 | * @param ActiveRecord $model the primary model
200 | * @param array $with the relations to be joined
201 | */
202 | protected function joinWithRelations(ActiveRecord $model, array $with)
203 | {
204 | foreach ($with as $name => $callback) {
205 | if (is_int($name)) {
206 | $name = $callback;
207 | $callback = null;
208 | }
209 | $primaryModel = $model;
210 | $parent = $this;
211 | if (!isset($relations[$name])) {
212 | $relations[$name] = $relation = $primaryModel->getRelation($name);
213 | /** @var static $relation */
214 | if ($callback !== null) {
215 | call_user_func($callback, $relation);
216 | }
217 | if (!empty($relation->joinWith)) {
218 | $relation->buildJoinWith();
219 | }
220 | $this->joinWithRelation($parent, $relation);
221 | }
222 | }
223 | }
224 |
225 | protected function createModels($rows): array
226 | {
227 | if ($this->asArray) {
228 | return $rows;
229 | } else {
230 | $models = [];
231 | /** @var ActiveRecord $class */
232 | $class = $this->modelClass;
233 | foreach ($rows as $row) {
234 | $model = $class::instantiate($row);
235 | /** @var ActiveRecord $modelClass */
236 | $modelClass = get_class($model);
237 | $modelClass::populateRecord($model, $row);
238 | if (!empty($this->join)) {
239 | foreach ($this->join as $join) {
240 | if (isset($join[1], $row[$join[1]])) {
241 | $relation = $model->getRelation($join[1]);
242 | $rows = (ArrayHelper::isAssociative($row[$join[1]])) ? [$row[$join[1]]] : $row[$join[1]];
243 | $relations = $relation->populate($rows);
244 | $model->populateRelation($join[1], $relation->multiple ? $relations : $relations[0]);
245 | }
246 | }
247 | }
248 | $models[] = $model;
249 | }
250 | return $models;
251 | }
252 | }
253 |
254 | /**
255 | * Builds join with clauses
256 | */
257 | private function buildJoinWith()
258 | {
259 | $join = $this->join;
260 | $this->join = [];
261 | $model = new $this->modelClass();
262 | foreach ($this->joinWith as $with) {
263 | $this->joinWithRelations($model, $with);
264 | foreach ($with as $name => $callback) {
265 | $this->innerJoin(is_int($name) ? $callback : [
266 | $name => $callback,
267 | ]);
268 | unset($with[$name]);
269 | }
270 | }
271 | if (!empty($join)) {
272 | // append explicit join to joinWith()
273 | // https://github.com/yiisoft/yii2/issues/2880
274 | $this->join = empty($this->join) ? $join : array_merge($this->join, $join);
275 | }
276 | }
277 |
278 | /**
279 | * Joins a parent query with a child query.
280 | * The current query object will be modified accordingly.
281 | */
282 | private function joinWithRelation(ActiveQuery $parent, ActiveQuery $child)
283 | {
284 | if (!empty($child->join)) {
285 | foreach ($child->join as $join) {
286 | $this->join[] = $join;
287 | }
288 | }
289 | }
290 |
291 | /**
292 | * Removes duplicated models by checking their primary key values.
293 | * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
294 | *
295 | * @param array $models the models to be checked
296 | *
297 | * @return array the distinctive models
298 | * @throws InvalidConfigException if model primary key is empty
299 | */
300 | private function removeDuplicatedModels(array $models): array
301 | {
302 | $hash = [];
303 | /** @var ActiveRecord $class */
304 | $class = $this->modelClass;
305 | $pks = $class::primaryKey();
306 |
307 | if (count($pks) > 1) {
308 | // composite primary key
309 | foreach ($models as $i => $model) {
310 | $key = [];
311 | foreach ($pks as $pk) {
312 | if (!isset($model[$pk])) {
313 | // do not continue if the primary key is not part of the result set
314 | break 2;
315 | }
316 | $key[] = $model[$pk];
317 | }
318 | $key = serialize($key);
319 | if (isset($hash[$key])) {
320 | unset($models[$i]);
321 | } else {
322 | $hash[$key] = true;
323 | }
324 | }
325 | } elseif (empty($pks)) {
326 | /** @var string $class */
327 | throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
328 | } else {
329 | // single column primary key
330 | $pk = reset($pks);
331 | foreach ($models as $i => $model) {
332 | if (!isset($model[$pk])) {
333 | // do not continue if the primary key is not part of the result set
334 | break;
335 | }
336 | $key = $model[$pk];
337 | if (isset($hash[$key])) {
338 | unset($models[$i]);
339 | } elseif ($key !== null) {
340 | $hash[$key] = true;
341 | }
342 | }
343 | }
344 |
345 | return array_values($models);
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | [
28 | * 'restclient' => [
29 | * 'class' => 'simialbi\yii2\rest\Connection',
30 | * 'baseUrl' => 'https://api.site.com/',
31 | * ],
32 | * ],
33 | * ```
34 | *
35 | * @property Client $handler
36 | * @property-read array $queryCacheInfo
37 | * @property-write string|Closure $auth
38 | */
39 | class Connection extends Component
40 | {
41 | /**
42 | * @event Event an event that is triggered after a DB connection is established
43 | */
44 | public const EVENT_AFTER_OPEN = 'afterOpen';
45 |
46 | /**
47 | * @var Client
48 | */
49 | protected static $_handler = null;
50 |
51 | /**
52 | * @var string base request URL.
53 | */
54 | public $baseUrl = '';
55 |
56 | /**
57 | * @var array request object configuration.
58 | */
59 | public $requestConfig = [];
60 |
61 | /**
62 | * @var array response config configuration.
63 | */
64 | public $responseConfig = [];
65 |
66 | /**
67 | * @var boolean Whether to use pluralisation or not
68 | */
69 | public $usePluralisation = true;
70 |
71 | /**
72 | * @var boolean Whether to use filter keyword or not
73 | */
74 | public $useFilterKeyword = true;
75 |
76 | /**
77 | * @var string The method to use for update operations. Defaults to [[put]].
78 | */
79 | public $updateMethod = 'put';
80 |
81 | /**
82 | * @var boolean Whether the connection should throw an exception if response is not 200 or not
83 | */
84 | public $enableExceptions = false;
85 |
86 | /**
87 | * @var boolean Whether we are in test mode or not (prevent execution)
88 | */
89 | public $isTestMode = false;
90 |
91 | /**
92 | * @var bool whether to enable query caching.
93 | * Note that in order to enable query caching, a valid cache component as specified
94 | * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true.
95 | * Also, only the results of the queries enclosed within [[cache()]] will be cached.
96 | * @see queryCache
97 | * @see cache()
98 | * @see noCache()
99 | */
100 | public $enableQueryCache = false;
101 |
102 | /**
103 | * @var int the default number of seconds that query results can remain valid in cache.
104 | * Defaults to 3600, meaning 3600 seconds, or one hour. Use 0 to indicate that the cached data will never expire.
105 | * The value of this property will be used when [[cache()]] is called without a cache duration.
106 | * @see enableQueryCache
107 | * @see cache()
108 | */
109 | public $queryCacheDuration = 3600;
110 |
111 | /**
112 | * @var CacheInterface|string the cache object or the ID of the cache application component
113 | * that is used for query caching.
114 | * @see enableQueryCache
115 | */
116 | public $queryCache = 'cache';
117 |
118 | /**
119 | * @var string|null the name of the property in the response where the items are wrapped. If not set, there is no
120 | * wrapping property.
121 | */
122 | public $itemsProperty;
123 |
124 | /**
125 | * @var string|Closure authorization config
126 | */
127 | protected $_auth;
128 |
129 | /**
130 | * @var Response
131 | */
132 | protected $_response;
133 |
134 | /**
135 | * @var array query cache parameters for the [[cache()]] calls
136 | */
137 | private $_queryCacheInfo = [];
138 |
139 | /**
140 | * Returns the name of the DB driver. Based on the the current [[dsn]], in case it was not set explicitly
141 | * by an end user.
142 | * @return string name of the DB driver
143 | */
144 | public static function getDriverName(): string
145 | {
146 | return 'rest';
147 | }
148 |
149 | /**
150 | * Uses query cache for the queries performed with the callable.
151 | *
152 | * When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
153 | * queries performed within the callable will be cached and their results will be fetched from cache if available.
154 | * For example,
155 | *
156 | * ```php
157 | * // The customer will be fetched from cache if available.
158 | * // If not, the query will be made against DB and cached for use next time.
159 | * $customer = $db->cache(function (Connection $db) {
160 | * return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
161 | * });
162 | * ```
163 | *
164 | * Note that query cache is only meaningful for queries that return results. For queries performed with
165 | * [[Command::execute()]], query cache will not be used.
166 | *
167 | * @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
168 | * The signature of the callable is `function (Connection $db)`.
169 | * @param int|null $duration the number of seconds that query results can remain valid in the cache. If this is
170 | * not set, the value of [[queryCacheDuration]] will be used instead.
171 | * Use 0 to indicate that the cached data will never expire.
172 | * @param \yii\caching\Dependency|null $dependency the cache dependency associated with the cached query results.
173 | *
174 | * @return mixed the return result of the callable
175 | * @throws \Exception|\Throwable if there is any exception during query
176 | * @see enableQueryCache
177 | * @see queryCache
178 | * @see noCache()
179 | */
180 | public function cache(callable $callable, ?int $duration = null, ?\yii\caching\Dependency $dependency = null)
181 | {
182 | $this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
183 | try {
184 | $result = call_user_func($callable, $this);
185 | array_pop($this->_queryCacheInfo);
186 | return $result;
187 | } catch (\Exception|\Throwable $e) {
188 | array_pop($this->_queryCacheInfo);
189 | throw $e;
190 | }
191 | }
192 |
193 | /**
194 | * Disables query cache temporarily.
195 | *
196 | * Queries performed within the callable will not use query cache at all. For example,
197 | *
198 | * ```php
199 | * $db->cache(function (Connection $db) {
200 | *
201 | * // ... queries that use query cache ...
202 | *
203 | * return $db->noCache(function (Connection $db) {
204 | * // this query will not use query cache
205 | * return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
206 | * });
207 | * });
208 | * ```
209 | *
210 | * @param callable $callable a PHP callable that contains DB queries which should not use query cache.
211 | * The signature of the callable is `function (Connection $db)`.
212 | *
213 | * @return mixed the return result of the callable
214 | * @throws \Exception|\Throwable if there is any exception during query
215 | * @see enableQueryCache
216 | * @see queryCache
217 | * @see cache()
218 | */
219 | public function noCache(callable $callable)
220 | {
221 | $this->_queryCacheInfo[] = false;
222 | try {
223 | $result = call_user_func($callable, $this);
224 | array_pop($this->_queryCacheInfo);
225 | return $result;
226 | } catch (\Exception|\Throwable $e) {
227 | array_pop($this->_queryCacheInfo);
228 | throw $e;
229 | }
230 | }
231 |
232 | /**
233 | * Returns the current query cache information.
234 | * This method is used internally by [[Command]].
235 | *
236 | * @param int|null $duration the preferred caching duration. If null, it will be ignored.
237 | * @param Dependency|null $dependency the preferred caching dependency. If null, it will be ignored.
238 | *
239 | * @return array the current query cache information, or null if query cache is not enabled.
240 | * @throws InvalidConfigException
241 | * @internal
242 | */
243 | public function getQueryCacheInfo(?int $duration = null, ?\yii\caching\Dependency $dependency = null): ?array
244 | {
245 | if (!$this->enableQueryCache) {
246 | return null;
247 | }
248 |
249 | $info = end($this->_queryCacheInfo);
250 | if (is_array($info)) {
251 | if ($duration === null) {
252 | $duration = $info[0];
253 | }
254 | if ($dependency === null) {
255 | $dependency = $info[1];
256 | }
257 | }
258 |
259 | if ($duration === 0 || $duration > 0) {
260 | if (is_string($this->queryCache) && Yii::$app) {
261 | $cache = Yii::$app->get($this->queryCache, false);
262 | } else {
263 | $cache = $this->queryCache;
264 | }
265 | if ($cache instanceof CacheInterface) {
266 | return [$cache, $duration, $dependency];
267 | }
268 | }
269 |
270 | return null;
271 | }
272 |
273 | /**
274 | * @throws InvalidConfigException
275 | */
276 | public function init()
277 | {
278 | if (!$this->baseUrl) {
279 | throw new InvalidConfigException('The `baseUrl` config option must be set');
280 | }
281 |
282 | $this->baseUrl = rtrim($this->baseUrl, '/');
283 |
284 | parent::init();
285 | }
286 |
287 | /**
288 | * Closes the connection when this component is being serialized.
289 | * @return array
290 | */
291 | public function __sleep()
292 | {
293 | return array_keys(get_object_vars($this));
294 | }
295 |
296 | /**
297 | * Creates a command for execution.
298 | *
299 | * @param array $config the configuration for the Command class
300 | *
301 | * @return Command the DB command
302 | */
303 | public function createCommand(array $config = []): Command
304 | {
305 | $config['db'] = $this;
306 | return new Command($config);
307 | }
308 |
309 | /**
310 | * Creates new query builder instance.
311 | */
312 | public function getQueryBuilder(): QueryBuilder
313 | {
314 | return new QueryBuilder($this);
315 | }
316 |
317 | /**
318 | * Performs GET HTTP request.
319 | *
320 | * @param string|array $url URL
321 | * @param array $data request body
322 | *
323 | * @return array|false response
324 | * @throws Exception
325 | */
326 | public function get($url, array $data = [])
327 | {
328 | array_unshift($data, $url);
329 | return $this->request('get', $data);
330 | }
331 |
332 | /**
333 | * Performs HEAD HTTP request.
334 | *
335 | * @param string|array $url URL
336 | * @param array $data request body
337 | *
338 | * @return HeaderCollection response
339 | * @throws Exception
340 | */
341 | public function head($url, array $data = []): HeaderCollection
342 | {
343 | array_unshift($data, $url);
344 | $this->request('head', $data);
345 |
346 | return $this->_response->headers;
347 | }
348 |
349 | /**
350 | * Performs POST HTTP request.
351 | *
352 | * @param string|array $url URL
353 | * @param array $data request body
354 | *
355 | * @return array|false response
356 | * @throws Exception
357 | */
358 | public function post($url, array $data = [])
359 | {
360 | return $this->request('post', $url, $data);
361 | }
362 |
363 | /**
364 | * Performs PUT HTTP request.
365 | *
366 | * @param string|array $url URL
367 | * @param array $data request body
368 | *
369 | * @return array|false response
370 | * @throws Exception
371 | */
372 | public function put($url, array $data = [])
373 | {
374 | return $this->request('put', $url, $data);
375 | }
376 |
377 | /**
378 | * Performs DELETE HTTP request.
379 | *
380 | * @param string|array $url URL
381 | * @param array $data request body
382 | *
383 | * @return array|false response
384 | * @throws Exception
385 | */
386 | public function delete($url, array $data = [])
387 | {
388 | return $this->request('delete', $url, $data);
389 | }
390 |
391 | /**
392 | * Returns the request handler (Guzzle client for the moment).
393 | * Creates and setups handler if not set.
394 | */
395 | public function getHandler(): Client
396 | {
397 | if (static::$_handler === null) {
398 | $requestConfig = $this->requestConfig;
399 | $responseConfig = array_merge([
400 | 'class' => 'yii\httpclient\Response',
401 | 'format' => Client::FORMAT_JSON,
402 | ], $this->responseConfig);
403 | static::$_handler = new Client([
404 | 'baseUrl' => $this->baseUrl,
405 | 'requestConfig' => $requestConfig,
406 | 'responseConfig' => $responseConfig,
407 | ]);
408 | }
409 |
410 | return static::$_handler;
411 | }
412 |
413 | /**
414 | * Returns the authorization config.
415 | *
416 | * @return string authorization config
417 | */
418 | protected function getAuth()
419 | {
420 | if ($this->_auth instanceof Closure) {
421 | $this->_auth = call_user_func($this->_auth, $this);
422 | }
423 |
424 | return $this->_auth;
425 | }
426 |
427 | /**
428 | * Changes the current authorization config.
429 | *
430 | * @param string|Closure $auth authorization config
431 | */
432 | public function setAuth($auth)
433 | {
434 | $this->_auth = $auth;
435 | }
436 |
437 | /**
438 | * Handles the request with handler.
439 | * Returns array or raw response content, if $raw is true.
440 | *
441 | * @param string $method POST, GET, etc
442 | * @param string|array $url the URL for request, not including proto and site
443 | * @param array $data the request data
444 | *
445 | * @return array|false
446 | * @throws Exception
447 | */
448 | protected function request(string $method, $url, array $data = [])
449 | {
450 | if (is_array($url)) {
451 | $path = array_shift($url);
452 | $query = http_build_query($url);
453 |
454 | array_unshift($url, $path);
455 |
456 | $path .= '?' . $query;
457 | } else {
458 | $path = $url;
459 | }
460 |
461 | $headers = [];
462 | $method = strtoupper($method);
463 | $profile = $method . ' ' . $this->handler->baseUrl . '/' . $path . '#' . (is_array($data) ? http_build_query($data) : $data);
464 |
465 | if ($auth = $this->getAuth()) {
466 | $headers['Authorization'] = $auth;
467 | }
468 | if ($method === 'head') {
469 | $data = $headers;
470 | $headers = [];
471 | }
472 |
473 | Yii::beginProfile($profile, __METHOD__);
474 | /* @var $request \yii\httpclient\Request */
475 |
476 | Yii::debug($method, __METHOD__ . '-method');
477 | Yii::debug($this->handler->baseUrl . '/' . $path, __METHOD__ . '-url');
478 | Yii::debug($data, __METHOD__ . '-data');
479 | Yii::debug($headers, __METHOD__ . '-headers');
480 |
481 | $request = call_user_func([$this->handler, $method], $url, $data, $headers);
482 | try {
483 | $this->_response = $this->isTestMode ? [] : $request->send();
484 | } catch (\yii\httpclient\Exception $e) {
485 | throw new RestException('Request failed', [], 1, $e);
486 | }
487 | Yii::endProfile($profile, __METHOD__);
488 |
489 | if (!$this->isTestMode && !$this->_response->isOk) {
490 | if ($this->enableExceptions) {
491 | throw new RestException($this->_response->content, $this->_response->headers->toArray());
492 | }
493 | return false;
494 | }
495 |
496 | return $this->isTestMode ? [] : $this->_response->data;
497 | }
498 | }
499 |
--------------------------------------------------------------------------------