diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fdea7e2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: composer + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..df3dfd1 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,14 @@ +name: update changelog + +on: + release: + types: [released] + +permissions: {} + +jobs: + changelog: + permissions: + contents: write + secrets: inherit + uses: bavix/.github/.github/workflows/changelog.yml@0.2.4 diff --git a/.github/workflows/fixer.yaml b/.github/workflows/fixer.yaml new file mode 100644 index 0000000..263c9bc --- /dev/null +++ b/.github/workflows/fixer.yaml @@ -0,0 +1,103 @@ +name: fixer + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + autofix: + runs-on: ubuntu-latest + + services: + clickhouse: + image: clickhouse/clickhouse-server + env: + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 + ports: + - 8123:8123 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: mbstring, pgsql, mysql, sqlite, redis, memcached, bcmath + coverage: pcov + env: + runner: self-hosted + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run rector-fix + run: composer rector-fix + + - name: Run ecs-fix + run: composer ecs-fix + + - name: Run rector-fix + run: composer rector-fix + + - name: Run ecs-fix + run: composer ecs-fix + + - name: Run rector + run: composer rector + + - name: Run ecs + run: composer ecs + + - name: Run parabench + run: composer parabench + + - name: "Check if build has changed" + if: success() + id: has-changes + run: | + echo "stdout<> $GITHUB_OUTPUT + echo "$(git diff --stat)" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + - name: Import GPG key + if: ${{ steps.has-changes.outputs.stdout }} + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_BOT }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + fingerprint: ${{ secrets.GPG_FINGERPRINT }} + git_config_global: true + git_user_signingkey: true + git_commit_gpgsign: true + git_committer_name: Github bot + git_committer_email: bot@babichev.net + + - name: "Commit files" + if: ${{ steps.has-changes.outputs.stdout }} + env: + GH_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + gh pr checkout ${{ github.event.pull_request.number }} + git commit -S -m "autofix" -a + + - name: "Push changes" + if: ${{ steps.has-changes.outputs.stdout }} + env: + GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} + run: git push -u origin HEAD \ No newline at end of file diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml new file mode 100644 index 0000000..5673e51 --- /dev/null +++ b/.github/workflows/phpstan.yaml @@ -0,0 +1,38 @@ +name: phpstan + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + phpstan: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run phpstan + run: composer phpstan diff --git a/.github/workflows/phpunits.yaml b/.github/workflows/phpunits.yaml new file mode 100644 index 0000000..aa35bc3 --- /dev/null +++ b/.github/workflows/phpunits.yaml @@ -0,0 +1,85 @@ +name: phpunits + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + units: + runs-on: ubuntu-latest + + strategy: + matrix: + php-versions: [8.2, 8.3, 8.4] + laravel-versions: [^10.0, ^11.0, ^12.0] + + services: + clickhouse: + image: clickhouse/clickhouse-server + env: + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 + ports: + - 8123:8123 + + steps: + - name: Checkout + id: git-checkout + uses: actions/checkout@v4 + + - name: Setup PHP + id: php-install + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, pgsql, mysql, sqlite, redis, memcached + coverage: pcov + + - name: Validate composer.json and composer.lock + id: composer-validate + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + id: composer-dependencies + run: composer req --dev laravel/framework:${{ matrix.laravel-versions }} -W || composer install + + - name: Check codeclimate + id: codeclimate-check + run: echo "execute=${{ matrix.php-versions == '8.2' && matrix.caches == 'array' && matrix.databases == 'testing' }}" >> $GITHUB_OUTPUT + + - name: Prepare codeclimate + id: codeclimate-prepare + run: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + ./cc-test-reporter before-build + if: ${{ steps.codeclimate-check.outputs.execute == 'true' }} + + - name: Prepare run test suite + id: unit-prepare + run: | + mkdir build + + - name: Run test suite + id: unit-run + run: composer parabench + + - name: Send coverage + id: codeclimate-send + run: | + ./cc-test-reporter after-build --coverage-input-type clover --exit-code 0 + bash <(curl -s https://codecov.io/bash) + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + if: ${{ steps.codeclimate-check.outputs.execute == 'true' }} \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..b55be58 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,16 @@ +name: Mark stale issues +on: + schedule: + - cron: "0 */8 * * *" +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open 7 days with no activity.' + days-before-stale: 7 + days-before-close: 3 + exempt-issue-labels: 'bug,in-developing' + exempt-pr-labels: 'bug,frozen,in-developing' diff --git a/.gitignore b/.gitignore index cf13d71..2525d15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -/vendor -/.idea +composer.phar +/vendor/ composer.lock -tests/coverage +.idea/ +build/ +.phpunit.result.cache +.php_cs_cache +.phpunit.cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 82a7ddd..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: php - -php: - - 7.1 - -before_install: - - composer require php-coveralls/php-coveralls - -before_script: - - composer self-update - - composer install --prefer-source --dev - -script: vendor/bin/phpunit --coverage-clover ./tests/logs/clover.xml - -after_script: - - php vendor/bin/php-coveralls -v \ No newline at end of file diff --git a/.whitesource b/.whitesource new file mode 100644 index 0000000..9c7ae90 --- /dev/null +++ b/.whitesource @@ -0,0 +1,14 @@ +{ + "scanSettings": { + "baseBranches": [] + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure", + "displayMode": "diff", + "useMendCheckNames": true + }, + "issueSettings": { + "minSeverityLevel": "LOW", + "issueType": "DEPENDENCY" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 9194de7..ff8c73a 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,35 @@ -# laravel-clickhouse -[![Build Status](https://travis-ci.org/esazykin/laravel-clickhouse.svg?branch=master)](https://travis-ci.org/esazykin/laravel-clickhouse) -[![StyleCI](https://styleci.io/repos/112756298/shield?branch=master)](https://styleci.io/repos/112756298) -[![Coverage Status](https://coveralls.io/repos/github/esazykin/laravel-clickhouse/badge.svg)](https://coveralls.io/github/esazykin/laravel-clickhouse) +# Laravel Clickhouse -Eloquent model for ClickHouse +[![Latest Stable Version](https://poser.pugx.org/bavix/laravel-clickhouse/v/stable)](https://packagist.org/packages/bavix/laravel-clickhouse) +[![License](https://poser.pugx.org/bavix/laravel-clickhouse/license)](https://packagist.org/packages/bavix/laravel-clickhouse) +[![composer.lock](https://poser.pugx.org/bavix/laravel-clickhouse/composerlock)](https://packagist.org/packages/bavix/laravel-clickhouse) -## Prerequisites -- php 7.1 -- clickhouse server +Laravel Clickhouse - Eloquent model for ClickHouse. -## Installation +* **Vendor**: bavix +* **Package**: laravel-clickhouse +* **[Composer](https://getcomposer.org/):** `composer require bavix/laravel-wallet-uuid` + +> [!IMPORTANT] +> I recommend using the standard postgres/mysql interface for clickhouse. More details here: https://clickhouse.com/docs/en/interfaces/mysql + +The implementation is provided as is. Further work with the library only through contributors. Added linters, tests and much more. To make it easier for you to send PR. + +## Get started ```sh -$ composer require esazykin/laravel-clickhouse +$ composer require bavix/laravel-clickhouse ``` Then add the code above into your config/app.php file providers section ```php -Esazykin\LaravelClickHouse\ClickHouseServiceProvider::class, +Bavix\LaravelClickHouse\ClickHouseServiceProvider::class, ``` + And add new connection into your config/database.php file. Something like this: ```php 'connections' => [ - 'clickhouse' => [ - 'driver' => 'clickhouse', + 'bavix::clickhouse' => [ + 'driver' => 'bavix::clickhouse', 'host' => '', 'port' => '', 'database' => '', @@ -35,14 +42,15 @@ And add new connection into your config/database.php file. Something like this: ] ] ``` + Or like this, if clickhouse runs in cluster ```php 'connections' => [ - 'clickhouse' => [ - 'driver' => 'clickhouse', - 'cluster' => [ - 'server-1' => [ - 'host' => '', + 'bavix::clickhouse' => [ + 'driver' => 'bavix::clickhouse', + 'servers' => [ + [ + 'host' => 'ch-00.domain.com', 'port' => '', 'database' => '', 'username' => '', @@ -52,8 +60,8 @@ Or like this, if clickhouse runs in cluster 'protocol' => 'https' ] ], - 'server-2' => [ - 'host' => '', + [ + 'host' => 'ch-01.domain.com', 'port' => '', 'database' => '', 'username' => '', @@ -65,14 +73,14 @@ Or like this, if clickhouse runs in cluster ] ] ] -] +], ``` Then create model ```php get(); ``` - -## Roadmap -- more tests -- Model::with() method -- relations - diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..233032c --- /dev/null +++ b/changelog.md @@ -0,0 +1,98 @@ +# Changelog + +## 3.0.5 - 2025-05-27 + +### What's Changed + +* req bavix/clickhouse-php-client by [@rez1dent3](https://github.com/rez1dent3) in https://github.com/bavix/laravel-clickhouse/pull/15 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/3.0.4...3.0.5 + +## 3.0.3 - 2024-04-01 + +### What's Changed + +* init fixes by [@rez1dent3](https://github.com/rez1dent3) in https://github.com/bavix/laravel-clickhouse/pull/11 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/3.0.2...3.0.3 + +## 3.0.2 - 2024-04-01 + +### What's Changed + +* Method Bavix\LaravelClickHouse\Database\Query\Builder::getCountForPagination does not exist by [@rez1dent3](https://github.com/rez1dent3) in https://github.com/bavix/laravel-clickhouse/pull/10 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/3.0.1...3.0.2 + +## 3.0.1 - 2024-03-14 + +### What's Changed + +* Update README.md + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/3.0.0...3.0.1 + +## 3.0.0 - 2024-03-14 + +### What's Changed + +* Configure Mend Bolt for GitHub by [@mend-bolt-for-github](https://github.com/mend-bolt-for-github) in https://github.com/bavix/laravel-clickhouse/pull/8 +* [3.0] Add support laravel ^11.0 & ^10.0 by [@rez1dent3](https://github.com/rez1dent3) in https://github.com/bavix/laravel-clickhouse/pull/9 + +### New Contributors + +* [@mend-bolt-for-github](https://github.com/mend-bolt-for-github) made their first contribution in https://github.com/bavix/laravel-clickhouse/pull/8 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.3.4...3.0.0 + +## 2.3.4 - 2024-01-17 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.3.3...2.3.4 + +## 2.3.3 - 2023-02-21 + +## What's Changed + +* Suppress deprecation warning by [@evgeek](https://github.com/evgeek) in https://github.com/bavix/laravel-clickhouse/pull/5 +* Fix Eloquent\Builder::where to pass along AND/OR by [@glmdev](https://github.com/glmdev) in https://github.com/bavix/laravel-clickhouse/pull/4 + +## New Contributors + +* [@evgeek](https://github.com/evgeek) made their first contribution in https://github.com/bavix/laravel-clickhouse/pull/5 +* [@glmdev](https://github.com/glmdev) made their first contribution in https://github.com/bavix/laravel-clickhouse/pull/4 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.3.2...2.3.3 + +## 2.3.2 - 2023-01-23 + +Add support laravel 10 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.3.1...2.3.2 + +## 2.3.1 - 2022-10-27 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.3.0...2.3.1 + +## 2.3.0 - 2022-10-16 + +add support laravel 9 + +**Full Changelog**: https://github.com/bavix/laravel-clickhouse/compare/2.2.0...2.3.0 + +## 2.2.0 - 2021-03-15 + +## 2.1.1 - 2021-03-15 + +## 2.1.0 - 2020-09-24 + +## 2.0.0 - 2020-09-24 + +## 1.1.0 - 2020-08-31 + +## 1.0.1 - 2020-07-09 + +Add support `laravel/telescope` + +## 1.0.0 - 2020-07-09 + +First version diff --git a/composer.json b/composer.json index 7c115ad..ff9ba6c 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { - "name": "esazykin/laravel-clickhouse", + "name": "bavix/laravel-clickhouse", + "description": "Eloquent model for ClickHouse", "type": "library", "license": "MIT", "keywords": [ @@ -8,26 +9,57 @@ "eloquent" ], "require": { - "php": ">=7.1", - "laravel/framework": "5.5.*", - "esazykin/clickhouse-builder": "^1.1" + "php": "^8.2", + "laravel/framework": "^10.0|^11.0|^12.0", + "the-tinderbox/clickhouse-builder": "^6.1", + "bavix/clickhouse-php-client": "^3.1", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^6.5", - "mockery/mockery": "^1.0", - "fzaninotto/faker": "^1.7" + "driftingly/rector-laravel": "^1.0|^2.0", + "ergebnis/phpstan-rules": "^2.1", + "infection/infection": "~0.27", + "larastan/larastan": "^2.0|^3.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.0|^2.0", + "phpunit/phpunit": "^10.5|^11.0", + "rector/rector": "^1.0|^2.0", + "symplify/easy-coding-standard": "^12.1", + "mockery/mockery": "^1.6", + "fakerphp/faker": "^1.23" }, "autoload": { "psr-4": { - "Esazykin\\LaravelClickHouse\\": "src/" + "Bavix\\LaravelClickHouse\\": "src/" } }, "autoload-dev": { "psr-4": { - "Esazykin\\LaravelClickHouse\\Tests\\": "tests/" + "Bavix\\LaravelClickHouse\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Bavix\\LaravelClickHouse\\ClickHouseServiceProvider" + ] } }, "scripts": { - "test": "vendor/bin/phpunit --stop-on-failure tests/" + "parabench":"@php ./vendor/bin/testbench package:test --coverage-xml=build/coverage-xml --log-junit=build/junit.xml", + "infect": "@php vendor/bin/infection --coverage=build --min-msi=50 -j$(nproc) --only-covering-test-cases", + "phpstan": "@php vendor/bin/phpstan analyse -vvv --memory-limit 2G -c phpstan.neon", + "phpstan-baseline": "@php vendor/bin/phpstan analyse -vvv --memory-limit 2G -c phpstan.neon --generate-baseline phpstan.baseline.neon", + "ecs": "@php vendor/bin/ecs check", + "ecs-fix": "@php vendor/bin/ecs check --fix", + "ecs-cc": "@php vendor/bin/ecs --clear-cache", + "rector": "@php vendor/bin/rector process --dry-run", + "rector-fix": "@php vendor/bin/rector process" + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + } } } diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..c6f339c --- /dev/null +++ b/ecs.php @@ -0,0 +1,31 @@ +paths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $config->skip([ + GeneralPhpdocAnnotationRemoveFixer::class, + ]); + + $config->sets([ + SetList::CLEAN_CODE, + SetList::SYMPLIFY, + SetList::ARRAY, + SetList::COMMON, + SetList::PSR_12, + SetList::CONTROL_STRUCTURES, + SetList::NAMESPACES, + SetList::STRICT, + SetList::PHPUNIT, + SetList::LARAVEL, + ]); +}; \ No newline at end of file diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..14a49e9 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,17 @@ +{ + "timeout": 10, + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "build/infection.log", + "badge": { + "branch": "master" + } + }, + "mutators": { + "@default": true + } +} diff --git a/phpstan.baseline.neon b/phpstan.baseline.neon new file mode 100644 index 0000000..6d57a49 --- /dev/null +++ b/phpstan.baseline.neon @@ -0,0 +1,1267 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot access offset ''db'' on Illuminate\\Contracts\\Foundation\\Application\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/ClickHouseServiceProvider.php + + - + message: '#^Cannot access offset ''events'' on Illuminate\\Contracts\\Foundation\\Application\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/ClickHouseServiceProvider.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\ClickHouseServiceProvider is neither abstract nor final\.$#' + identifier: ergebnis.final + count: 1 + path: src/ClickHouseServiceProvider.php + + - + message: '#^PHPDoc tag @throws has invalid value \(\)\: Unexpected token "\\n ", expected type at offset 18 on line 2$#' + identifier: phpDoc.parseError + count: 1 + path: src/ClickHouseServiceProvider.php + + - + message: '#^Class "Bavix\\LaravelClickHouse\\Database\\Connection" is not allowed to extend "Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Connection"\.$#' + identifier: ergebnis.noExtends + count: 1 + path: src/Database/Connection.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Connection is neither abstract nor final\.$#' + identifier: ergebnis.final + count: 1 + path: src/Database/Connection.php + + - + message: '#^Return type \(Bavix\\LaravelClickHouse\\Database\\Query\\Builder\) of method Bavix\\LaravelClickHouse\\Database\\Connection\:\:query\(\) should be compatible with return type \(Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Builder\) of method Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Connection\:\:query\(\)$#' + identifier: method.childReturnType + count: 1 + path: src/Database/Connection.php + + - + message: '#^Access to protected property Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:\$wheres\.$#' + identifier: property.protected + count: 5 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Call to an undefined method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:forNestedWhere\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder is neither abstract nor final\.$#' + identifier: ergebnis.final + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder uses generic trait Illuminate\\Database\\Concerns\\BuildsQueries but does not specify its types\: TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Language construct isset\(\) should not be used\.$#' + identifier: ergebnis.noIsset + count: 5 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:__call\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:__callStatic\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:addNestedWiths\(\) has parameter \$results with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:addNestedWiths\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:addNewWheresWithinGroup\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:callScope\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:chunkById\(\) has parameter \$column with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:create\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:create\(\) has parameter \$attributes with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:createNestedWhere\(\) has parameter \$whereSlice with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:createNestedWhere\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:createSelectWithConstraint\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:eagerLoadRelation\(\) has parameter \$models with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:eagerLoadRelation\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:eagerLoadRelations\(\) has parameter \$models with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:eagerLoadRelations\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:find\(\) should return array\\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|static\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\)\|null but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:findMany\(\) has parameter \$ids with generic interface Illuminate\\Contracts\\Support\\Arrayable but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:findMany\(\) has parameter \$ids with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:findOrFail\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model but returns array\\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|static\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\)\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:findOrFail\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model but returns array\\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|static\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\)\|null\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:fromQuery\(\) has parameter \$bindings with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:getEagerLoads\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:getMacro\(\) should return Closure but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:getModels\(\) should return array\ but returns array\\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:getRelation\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\Relation does not specify its types\: TRelatedModel, TDeclaringModel, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:groupWhereSliceForScope\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:groupWhereSliceForScope\(\) has parameter \$whereSlice with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:hydrate\(\) has parameter \$items with no value type specified in iterable type iterable\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:newModelInstance\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:orWhere\(\) has parameter \$column with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:paginate\(\) has parameter \$columns with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:paginate\(\) return type with generic interface Illuminate\\Contracts\\Pagination\\LengthAwarePaginator does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:parseWithRelations\(\) has parameter \$relations with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:parseWithRelations\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:pluck\(\) return type with generic class Illuminate\\Support\\Collection does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:relationsNestedUnder\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:scopes\(\) has parameter \$scopes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:setEagerLoads\(\) has parameter \$eagerLoad with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:setModel\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:setQuery\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:simplePaginate\(\) has parameter \$columns with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:simplePaginate\(\) return type with generic interface Illuminate\\Contracts\\Pagination\\Paginator does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:toBase\(\) should return Illuminate\\Database\\Query\\Builder but returns Bavix\\LaravelClickHouse\\Database\\Query\\Builder\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:where\(\) has parameter \$column with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^PHPDoc tag @param for parameter \$query with type Illuminate\\Database\\Query\\Builder is incompatible with native type Bavix\\LaravelClickHouse\\Database\\Query\\Builder\.$#' + identifier: parameter.phpDocType + count: 2 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$array of function array_flip expects array\, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$builder of method Illuminate\\Database\\Eloquent\\Scope\:\:apply\(\) expects Illuminate\\Database\\Eloquent\\Builder\, Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$column of method Tinderbox\\ClickhouseBuilder\\Query\\BaseBuilder\:\:where\(\) expects Closure\|string\|Tinderbox\\ClickhouseBuilder\\Query\\BaseBuilder\|Tinderbox\\ClickhouseBuilder\\Query\\TwoElementsLogicExpression, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$model of method Illuminate\\Database\\Eloquent\\ModelNotFoundException\\:\:setModel\(\) expects class\-string\, class\-string\ given\.$#' + identifier: argument.type + count: 2 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$name of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setConnection\(\) expects string, string\|null given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$relations of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:parseWithRelations\(\) expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$scope of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:callScope\(\) expects callable\(\)\: mixed, array\{Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model, non\-falsy\-string\} given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$value of function count expects array\|Countable, array\\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\|Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|static\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\)\|null given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#1 \$value of function count expects array\|Countable, array\|Illuminate\\Contracts\\Support\\Arrayable given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#2 \$boolean of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:createNestedWhere\(\) expects string, TValue\|null given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#2 \$ids of method Illuminate\\Database\\Eloquent\\ModelNotFoundException\\:\:setModel\(\) expects array\\|int\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#2 \$model of method Illuminate\\Database\\Eloquent\\Scope\:\:apply\(\) expects Illuminate\\Database\\Eloquent\\Model, Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Parameter \#3 \$tables of method Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Connection\:\:select\(\) expects array, true given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:\$eagerLoad type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:\$localMacros type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:\$macros type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:\$passthru type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:\$scopes type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Property Tinderbox\\ClickhouseBuilder\\Query\\BaseBuilder\:\:\$wheres \(array\\) does not accept array\\.$#' + identifier: assign.propertyType + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Strict comparison using \=\=\= between Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Strict comparison using \=\=\= between array and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: src/Database/Eloquent/Builder.php + + - + message: '#^Class "Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection" is not allowed to extend "Illuminate\\Database\\Eloquent\\Collection"\.$#' + identifier: ergebnis.noExtends + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection extends generic class Illuminate\\Database\\Eloquent\\Collection but does not specify its types\: TKey, TModel$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection is neither abstract nor final\.$#' + identifier: ergebnis.final + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\:\:find\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|static\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\) but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Collection\:\:map\(\) return type with generic class Illuminate\\Support\\Collection does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Parameter \#1 \$key of method Illuminate\\Support\\Collection\<\(int\|string\),Illuminate\\Database\\Eloquent\\Model\>\:\:contains\(\) expects \(callable\(Illuminate\\Database\\Eloquent\\Model, int\|string\)\: bool\)\|Illuminate\\Database\\Eloquent\\Model\|string, Closure\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\)\: bool given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Parameter \#1 \$key of method Illuminate\\Support\\Collection\<\(int\|string\),Illuminate\\Database\\Eloquent\\Model\>\:\:contains\(\) expects \(callable\(Illuminate\\Database\\Eloquent\\Model, int\|string\)\: bool\)\|Illuminate\\Database\\Eloquent\\Model\|string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Parameter \#2 \$callback of static method Illuminate\\Support\\Arr\:\:first\(\) expects \(callable\(Illuminate\\Database\\Eloquent\\Model, int\|string\)\: bool\)\|null, Closure\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\)\: bool given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: src/Database/Eloquent/Collection.php + + - + message: '#^Call to an undefined method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder\:\:withTrashed\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Call to an undefined method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|Illuminate\\Database\\Eloquent\\Relations\\Relation\:\:first\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Call to an undefined method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|Illuminate\\Database\\Eloquent\\Relations\\Relation\:\:where\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Call to an undefined method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|Illuminate\\Database\\Eloquent\\Relations\\Relation\:\:withTrashed\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Call to an undefined static method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:insert\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model implements generic interface ArrayAccess but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model implements generic interface Illuminate\\Contracts\\Support\\Arrayable but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Constructor in Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model has parameter \$attributes with default value\.$#' + identifier: ergebnis.noConstructorParameterWithDefaultValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Language construct isset\(\) should not be used\.$#' + identifier: ergebnis.noIsset + count: 2 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__call\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__call\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__callStatic\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__callStatic\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__construct\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__get\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__isset\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__set\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:__unset\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:all\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:boot\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:boot\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:bootIfNotBooted\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:bootIfNotBooted\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:bootTraits\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:bootTraits\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:childRouteBindingRelationshipName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:clearBootedModels\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:clearBootedModels\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:fill\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:fill\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:forceFill\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:forceFill\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:forceFill\(\) should return \$this\(Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\) but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getCasts\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getCasts\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getConnection\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getConnectionName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getConnectionResolver\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getDateFormat\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getDates\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getDates\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getForeignKey\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getKey\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getKeyName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getKeyType\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getPerPage\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getQualifiedKeyName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getRouteKey\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getRouteKeyName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getTable\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:jsonSerialize\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:jsonSerialize\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newBaseQueryBuilder\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newCollection\(\) has parameter \$models with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newCollection\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newEloquentBuilder\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newFromBuilder\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newFromBuilder\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newInstance\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newInstance\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newQuery\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newQueryWithoutScope\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newQueryWithoutScope\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:newQueryWithoutScopes\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:offsetExists\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:offsetGet\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:offsetSet\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:offsetUnset\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:preventsAccessingMissingAttributes\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:query\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:removeTableFromKey\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:removeTableFromKey\(\) should return string but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveChildRouteBinding\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveChildRouteBinding\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|null but returns Illuminate\\Database\\Eloquent\\Model\|null\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveChildRouteBindingQuery\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveChildRouteBindingQuery\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\Relation does not specify its types\: TRelatedModel, TDeclaringModel, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveConnection\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveRouteBinding\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveRouteBinding\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\|null but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveRouteBindingQuery\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Relations\\Relation but does not specify its types\: TRelatedModel, TDeclaringModel, TResult$#' + identifier: missingType.generics + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveRouteBindingQuery\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveRouteBindingQuery\(\) should return Bavix\\LaravelClickHouse\\Database\\Eloquent\\Builder but returns Illuminate\\Database\\Eloquent\\Relations\\Relation\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveSoftDeletableChildRouteBinding\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:resolveSoftDeletableRouteBinding\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:save\(\) has parameter \$options with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:save\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setConnection\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setConnectionResolver\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setKeyName\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setKeyType\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setPerPage\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setTable\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:toArray\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:toJson\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:toJson\(\) should return string but returns string\|false\.$#' + identifier: return.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:whenBooted\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:whenBooted\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:with\(\) has parameter \$relations with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:with\(\) is not final, but since the containing class is abstract, it should be\.$#' + identifier: ergebnis.finalInAbstractClass + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Parameter \#1 \$callback of function forward_static_call expects callable\(\)\: mixed, array\{class\-string\, non\-falsy\-string\} given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Parameter \#1 \$key of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:getAttribute\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 2 + path: src/Database/Eloquent/Model.php + + - + message: '#^Parameter \#1 \$key of method Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:setAttribute\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:\$booted type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:\$casts \(array\) on left side of \?\? is not nullable\.$#' + identifier: nullCoalesce.property + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:\$table \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:\$with type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Property Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model\:\:\$withCount type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Trait Illuminate\\Database\\Eloquent\\Concerns\\HasRelationships requires using class to extend Illuminate\\Database\\Eloquent\\Model, but Bavix\\LaravelClickHouse\\Database\\Eloquent\\Model does not\.$#' + identifier: class.missingExtends + count: 1 + path: src/Database/Eloquent/Model.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 5 + path: src/Database/Eloquent/Model.php + + - + message: '#^Cannot access offset ''count'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Cannot call method getTable\(\) on Tinderbox\\ClickhouseBuilder\\Query\\From\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Cannot cast mixed to int\.$#' + identifier: cast.int + count: 2 + path: src/Database/Query/Builder.php + + - + message: '#^Class "Bavix\\LaravelClickHouse\\Database\\Query\\Builder" is not allowed to extend "Tinderbox\\ClickhouseBuilder\\Query\\BaseBuilder"\.$#' + identifier: ergebnis.noExtends + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Class Bavix\\LaravelClickHouse\\Database\\Query\\Builder is neither abstract nor final\.$#' + identifier: ergebnis.final + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:forPage\(\) should return \$this\(Bavix\\LaravelClickHouse\\Database\\Query\\Builder\) but returns static\(Bavix\\LaravelClickHouse\\Database\\Query\\Builder\)\.$#' + identifier: return.type + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:get\(\) return type with generic class Illuminate\\Support\\Collection does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:getCountForPagination\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:insert\(\) has parameter \$values with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:insertFiles\(\) has parameter \$columns with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:insertFiles\(\) has parameter \$files with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Method Bavix\\LaravelClickHouse\\Database\\Query\\Builder\:\:insertFiles\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Parameter \#1 \$table of method Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Connection\:\:table\(\) expects Closure\|string\|Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Builder, static\(Bavix\\LaravelClickHouse\\Database\\Query\\Builder\) given\.$#' + identifier: argument.type + count: 1 + path: src/Database/Query/Builder.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: src/Database/Query/Builder.php diff --git a/phpstan.common.neon b/phpstan.common.neon new file mode 100644 index 0000000..5e64751 --- /dev/null +++ b/phpstan.common.neon @@ -0,0 +1,8 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/ergebnis/phpstan-rules/rules.neon + +parameters: + level: 9 + fileExtensions: + - php \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0d78f1e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,23 @@ +includes: + - phpstan.common.neon + - phpstan.baseline.neon + +parameters: + level: 9 + fileExtensions: + - php + ergebnis: + noParameterWithNullableTypeDeclaration: + enabled: false + noNullableReturnTypeDeclaration: + enabled: false + noParameterWithNullDefaultValue: + enabled: false + final: + allowAbstractClasses: true + noExtends: + classesAllowedToBeExtended: + # laravel + - Illuminate\Support\ServiceProvider + paths: + - src/ \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 2dee96b..716d0a0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,24 +1,27 @@ - - - - ./tests/Unit - - - - - src/Database/ - - - - - + + + + + + + + + + tests + + + + + + + + + + + + + src/ + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..c34009d --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ +parallel(); + $config->paths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $config->skip([ + IssetOnPropertyObjectToPropertyExistsRector::class, + ]); + + // Define what rule sets will be applied + $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); + $config->import(LaravelLevelSetList::UP_TO_LARAVEL_110); + $config->import(PHPUnitSetList::PHPUNIT_100); + $config->import(SetList::STRICT_BOOLEANS); + $config->import(SetList::PRIVATIZATION); + $config->import(SetList::EARLY_RETURN); + $config->import(SetList::INSTANCEOF); + $config->import(SetList::CODE_QUALITY); + $config->import(SetList::DEAD_CODE); + $config->import(SetList::PHP_82); +}; diff --git a/src/ClickHouseServiceProvider.php b/src/ClickHouseServiceProvider.php index 16c3080..77e8fd3 100644 --- a/src/ClickHouseServiceProvider.php +++ b/src/ClickHouseServiceProvider.php @@ -2,26 +2,35 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse; +namespace Bavix\LaravelClickHouse; -use Illuminate\Support\ServiceProvider; +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Eloquent\Model; +use Bavix\LaravelClickHouse\Database\Query\Pdo; +use Bavix\LaravelClickHouse\Database\Query\PdoInterface; use Illuminate\Database\DatabaseManager; -use Esazykin\LaravelClickHouse\Database\Connection; -use Esazykin\LaravelClickHouse\Database\Eloquent\Model; +use Illuminate\Support\ServiceProvider; class ClickHouseServiceProvider extends ServiceProvider { + /** + * @throws + */ public function boot(): void { - /** @var DatabaseManager $db */ - $db = $this->app->get('db'); - - $db->extend('clickhouse', function ($config, $name) { - $config['name'] = $name; + Model::setConnectionResolver($this->app['db']); + Model::setEventDispatcher($this->app['events']); + } - return new Connection($config); + public function register(): void + { + $this->app->singleton(PdoInterface::class, Pdo::class); + $this->app->resolving('db', static function (DatabaseManager $db) { + $db->extend('bavix::clickhouse', static function ($config, $name) { + return new Connection(\array_merge($config, [ + 'name' => $name, + ])); + }); }); - - Model::setConnectionResolver($db); } } diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 5752d19..b9c1f76 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -2,15 +2,21 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Database; +namespace Bavix\LaravelClickHouse\Database; +use Bavix\LaravelClickHouse\Database\Query\Builder; +use Bavix\LaravelClickHouse\Database\Query\PdoInterface; use Tinderbox\ClickhouseBuilder\Query\Grammar; -use Esazykin\LaravelClickHouse\Database\Query\Builder; class Connection extends \Tinderbox\ClickhouseBuilder\Integrations\Laravel\Connection { - public function query() + public function query(): Builder { return new Builder($this, new Grammar()); } + + public function getPdo(): PdoInterface + { + return app(PdoInterface::class); + } } diff --git a/src/Database/Eloquent/Builder.php b/src/Database/Eloquent/Builder.php index 064975c..90e9638 100755 --- a/src/Database/Eloquent/Builder.php +++ b/src/Database/Eloquent/Builder.php @@ -1,18 +1,23 @@ query = $query; } + /** + * Dynamically handle calls into the query instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if ($method === 'macro') { + $this->localMacros[$parameters[0]] = $parameters[1]; + + return null; + } + + if (isset($this->localMacros[$method])) { + array_unshift($parameters, $this); + + return $this->localMacros[$method](...$parameters); + } + + if (isset(static::$macros[$method])) { + if (static::$macros[$method] instanceof Closure) { + return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters); + } + + return call_user_func_array(static::$macros[$method], $parameters); + } + + if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { + return $this->callScope([$this->model, $scope], $parameters); + } + + if (in_array($method, $this->passthru, true)) { + return $this->toBase() + ->{$method}(...$parameters); + } + + $this->query->{$method}(...$parameters); + + return $this; + } + + /** + * Dynamically handle calls into the query instance. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws BadMethodCallException + */ + public static function __callStatic($method, $parameters) + { + if ($method === 'macro') { + static::$macros[$parameters[0]] = $parameters[1]; + + return null; + } + + if (! isset(static::$macros[$method])) { + throw new BadMethodCallException("Method {$method} does not exist."); + } + + if (static::$macros[$method] instanceof Closure) { + return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters); + } + + return call_user_func_array(static::$macros[$method], $parameters); + } + + /** + * Force a clone of the underlying query builder when cloning. + */ + public function __clone() + { + $this->query = clone $this->query; + } + /** * Add a where clause on the primary key to the query. * - * @param mixed $id + * @param mixed $id * @return $this */ - public function whereKey($id) + public function whereKey($id): self { if (is_array($id) || $id instanceof Arrayable) { $this->query->whereIn($this->model->getQualifiedKeyName(), $id); @@ -106,10 +190,10 @@ public function whereKey($id) /** * Add a where clause on the primary key to the query. * - * @param mixed $id + * @param mixed $id * @return $this */ - public function whereKeyNot($id) + public function whereKeyNot($id): self { if (is_array($id) || $id instanceof Arrayable) { $this->query->whereNotIn($this->model->getQualifiedKeyName(), $id); @@ -123,10 +207,9 @@ public function whereKeyNot($id) /** * Add a basic where clause to the query. * - * @param string|array|\Closure $column - * @param string $operator - * @param mixed $value - * @param string $boolean + * @param string|array|Closure $column + * @param string $operator + * @param mixed $value * @return $this */ public function where($column, $operator = null, $value = null, string $boolean = 'AND'): self @@ -135,7 +218,7 @@ public function where($column, $operator = null, $value = null, string $boolean $this->query->where(function (QueryBuilder $queryBuilder) use ($column) { $eloquentBuilder = $this->model->newEloquentBuilder($queryBuilder); $column($eloquentBuilder); - }); + }, null, null, $boolean); } else { $this->query->where(...func_get_args()); } @@ -146,49 +229,45 @@ public function where($column, $operator = null, $value = null, string $boolean /** * Add an "or where" clause to the query. * - * @param \Closure|array|string $column - * @param string $operator - * @param mixed $value - * @return Builder|static + * @param Closure|array|string $column + * @param string $operator + * @param mixed $value + * @return $this */ - public function orWhere($column, $operator = null, $value = null) + public function orWhere($column, $operator = null, $value = null): self { return $this->where($column, $operator, $value, 'OR'); } /** * Create a collection of models from plain arrays. - * - * @param array $items - * @return Collection */ - public function hydrate(array $items) + public function hydrate(iterable $items): Collection { $instance = $this->newModelInstance(); + $elements = []; + foreach ($items as $item) { + $elements[] = $instance->newFromBuilder($item); + } - return $instance->newCollection(array_map(function ($item) use ($instance) { - return $instance->newFromBuilder($item); - }, $items)); + return $instance->newCollection($elements); } /** * Create a collection of models from a raw query. * - * @param string $query - * @param array $bindings - * @return Collection + * @param string $query + * @param array $bindings */ - public function fromQuery($query, $bindings = []) + public function fromQuery($query, $bindings = []): Collection { - return $this->hydrate( - $this->query->getConnection()->select($query, $bindings) - ); + return $this->hydrate($this->query->getConnection()->select($query, $bindings, true)); } /** * Find a model by its primary key. * - * @param mixed $id + * @param mixed $id * @return Model|Collection|static[]|static|null */ public function find($id) @@ -197,14 +276,14 @@ public function find($id) return $this->findMany($id); } - return $this->whereKey($id)->first(); + return $this->whereKey($id) + ->first(); } /** * Find multiple models by their primary keys. * - * @param \Illuminate\Contracts\Support\Arrayable|array $ids - * @return Collection + * @param Arrayable|array $ids */ public function findMany($ids): Collection { @@ -212,19 +291,21 @@ public function findMany($ids): Collection return $this->model->newCollection(); } - return $this->whereKey($ids)->get(); + return $this->whereKey($ids) + ->get(); } /** * Find a model by its primary key or throw an exception. * - * @param mixed $id + * @param mixed $id * @return Model|Collection * * @throws ModelNotFoundException */ public function findOrFail($id) { + // fixme Arrayable::class $result = $this->find($id); if (is_array($id)) { @@ -235,35 +316,32 @@ public function findOrFail($id) return $result; } - throw (new ModelNotFoundException)->setModel( - get_class($this->model), $id - ); + throw (new ModelNotFoundException())->setModel(get_class($this->model), $id); } /** * Execute the query and get the first result or throw an exception. * - * @return Model|static - * * @throws ModelNotFoundException */ - public function firstOrFail() + public function firstOrFail(): ?Model { + /** @var Model $model */ $model = $this->first(); - if ($model !== null) { - return $model; + if ($model === null) { + throw (new ModelNotFoundException()) + ->setModel(get_class($this->model)); } - throw (new ModelNotFoundException)->setModel(get_class($this->model)); + return $model; } /** * Execute the query as a "select" statement. * - * @return Collection|static[] - * @throws \Tinderbox\Clickhouse\Exceptions\ClientException + * @throws ClientException */ - public function get() + public function get(): Collection { $builder = $this->applyScopes(); @@ -274,29 +352,30 @@ public function get() $models = $builder->eagerLoadRelations($models); } - return $builder->getModel()->newCollection($models); + return $builder->getModel() + ->newCollection($models); } /** * Get the hydrated models without eager loading. * * @return Model[] - * @throws \Tinderbox\Clickhouse\Exceptions\ClientException + * + * @throws ClientException */ public function getModels(): array { - $result = $this->query->get()->all(); + $result = $this->query->get() + ->all(); - return $this->hydrate($result)->all(); + return $this->hydrate($result) + ->all(); } /** * Eager load the relationships for the models. - * - * @param array $models - * @return array */ - public function eagerLoadRelations(array $models) + public function eagerLoadRelations(array $models): array { foreach ($this->eagerLoad as $name => $constraints) { // For nested eager loads we'll skip loading them here and they will be set as an @@ -310,48 +389,18 @@ public function eagerLoadRelations(array $models) return $models; } - /** - * Eagerly load the relationship on a set of models. - * - * @param array $models - * @param string $name - * @param \Closure $constraints - * @return array - */ - protected function eagerLoadRelation(array $models, $name, Closure $constraints) - { - // First we will "back up" the existing where conditions on the query so we can - // add our eager constraints. Then we will merge the wheres that were on the - // query back to it in order that any where conditions might be specified. - $relation = $this->getRelation($name); - - $relation->addEagerConstraints($models); - - $constraints($relation); - - // Once we have the results, we just match those back up to their parent models - // using the relationship instance. Then we just return the finished arrays - // of models which have been eagerly hydrated and are readied for return. - return $relation->match( - $relation->initRelation($models, $name), - $relation->getEager(), $name - ); - } - /** * Get the relation instance for the given relation name. - * - * @param string $name - * @return Relations\Relation */ - public function getRelation($name) + public function getRelation(string $name): Relation { // We want to run a relationship query without any constrains so that we will // not have to remove these where clauses manually which gets really hacky // and error prone. We don't want constraints because we add eager ones. $relation = Relation::noConstraints(function () use ($name) { try { - return $this->getModel()->{$name}(); + return $this->getModel() + ->{$name}(); } catch (BadMethodCallException $e) { throw RelationNotFoundException::make($this->getModel(), $name); } @@ -362,62 +411,24 @@ public function getRelation($name) // If there are nested relationships set on the query, we will put those onto // the query instances so that they can be handled after this relationship // is loaded. In this way they will all trickle down as they are loaded. - if (count($nested) > 0) { - $relation->getQuery()->with($nested); + if ($nested !== []) { + $relation->getQuery() + ->with($nested); } return $relation; } - /** - * Get the deeply nested relations for a given top-level relation. - * - * @param string $relation - * @return array - */ - protected function relationsNestedUnder($relation) - { - $nested = []; - - // We are basically looking for any relationships that are nested deeper than - // the given top-level relationship. We will just check for any relations - // that start with the given top relations and adds them to our arrays. - foreach ($this->eagerLoad as $name => $constraints) { - if ($this->isNestedUnder($relation, $name)) { - $nested[substr($name, strlen($relation.'.'))] = $constraints; - } - } - - return $nested; - } - - /** - * Determine if the relationship is nested. - * - * @param string $relation - * @param string $name - * @return bool - */ - protected function isNestedUnder($relation, $name) - { - return Str::contains($name, '.') && Str::startsWith($name, $relation.'.'); - } - /** * Chunk the results of a query by comparing numeric IDs. * - * @param int $count - * @param callable $callback - * @param string $column - * @param string|null $alias - * @return bool + * @param string|null $alias */ - public function chunkById($count, callable $callback, $column = null, $alias = null) + public function chunkById(int $count, callable $callback, $column = null, $alias = null): bool { - $column = is_null($column) ? $this->getModel()->getKeyName() : $column; - - $alias = is_null($alias) ? $column : $alias; - + $column = $column === null ? $this->getModel() + ->getKeyName() : $column; + $alias = $alias === null ? $column : $alias; $lastId = 0; do { @@ -426,11 +437,12 @@ public function chunkById($count, callable $callback, $column = null, $alias = n // We'll execute the query for the given page and get the results. If there are // no results we can just break and return from here. When there are results // we will call the callback with the current chunk of these results here. - $results = $clone->forPageAfterId($count, $lastId, $column)->get(); + $results = $clone->forPageAfterId($count, $lastId, $column) + ->get(); - $countResults = $results->count(); + $countResults = (int) $results->count(); - if ($countResults == 0) { + if ($countResults === 0) { break; } @@ -441,10 +453,11 @@ public function chunkById($count, callable $callback, $column = null, $alias = n return false; } - $lastId = $results->last()->{$alias}; + $lastId = $results->last() + ->{$alias}; unset($results); - } while ($countResults == $count); + } while ($countResults === $count); return true; } @@ -452,47 +465,47 @@ public function chunkById($count, callable $callback, $column = null, $alias = n /** * Get an array with the values of a given column. * - * @param string $column - * @param string|null $key - * @return \Illuminate\Support\Collection + * @param string|null $key */ - public function pluck($column, $key = null) + public function pluck(string $column, $key = null): \Illuminate\Support\Collection { - $results = $this->toBase()->pluck($column, $key); + $results = $this->toBase() + ->pluck($column, $key); // If the model has a mutator for the requested column, we will spin through // the results and mutate the values so that the mutated version of these // columns are returned as you would expect from these Eloquent models. - if (!$this->model->hasGetMutator($column) && - !$this->model->hasCast($column) && - !in_array($column, $this->model->getDates())) { + if (! $this->model->hasGetMutator($column) && + ! $this->model->hasCast($column) && + ! in_array($column, $this->model->getDates(), true)) { return $results; } return $results->map(function ($value) use ($column) { - return $this->model->newFromBuilder([$column => $value])->{$column}; + return $this->model->newFromBuilder([ + $column => $value, + ])->{$column}; }); } /** * Paginate the given query. * - * @param int $perPage - * @param array $columns - * @param string $pageName - * @param int|null $page - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - * - * @throws \InvalidArgumentException + * @param array $columns + * @param string $pageName + * @param int|null $page */ - public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) - { + public function paginate( + ?int $perPage = null, + $columns = ['*'], + $pageName = 'page', + $page = null + ): LengthAwarePaginator { $page = $page ?: Paginator::resolveCurrentPage($pageName); - - $perPage = $perPage ?: $this->model->getPerPage(); - + $perPage = $perPage !== null && $perPage !== 0 ? $perPage : $this->model->getPerPage(); $results = ($total = $this->toBase()->getCountForPagination()) - ? $this->forPage($page, $perPage)->get($columns) + ? $this->forPage($page, $perPage) + ->get() : $this->model->newCollection(); return $this->paginator($results, $total, $perPage, $page, [ @@ -504,24 +517,29 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', /** * Paginate the given query into a simple paginator. * - * @param int $perPage - * @param array $columns - * @param string $pageName - * @param int|null $page - * @return \Illuminate\Contracts\Pagination\Paginator + * @param array $columns + * @param string $pageName + * @param int|null $page + * + * @throws ClientException */ - public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) - { + public function simplePaginate( + ?int $perPage = null, + $columns = ['*'], + $pageName = 'page', + $page = null + ): \Illuminate\Contracts\Pagination\Paginator { $page = $page ?: Paginator::resolveCurrentPage($pageName); - $perPage = $perPage ?: $this->model->getPerPage(); + $perPage = $perPage !== null && $perPage !== 0 ? $perPage : $this->model->getPerPage(); // Next we will set the limit and offset for this query so that when we get the // results we get the proper section of results. Then, we'll create the full // paginator instances for these results with the given page and per page. - $this->skip(($page - 1) * $perPage)->take($perPage + 1); + $this->skip(($page - 1) * $perPage) + ->take($perPage + 1); - return $this->simplePaginator($this->get($columns), $perPage, $page, [ + return $this->simplePaginator($this->get(), $perPage, $page, [ 'path' => Paginator::resolveCurrentPath(), 'pageName' => $pageName, ]); @@ -530,7 +548,6 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p /** * Call the given local model scopes. * - * @param array $scopes * @return mixed */ public function scopes(array $scopes) @@ -542,16 +559,13 @@ public function scopes(array $scopes) // the parameter list is empty, so we will format the scope name and these // parameters here. Then, we'll be ready to call the scope on the model. if (is_int($scope)) { - list($scope, $parameters) = [$parameters, []]; + [$scope, $parameters] = [$parameters, []]; } // Next we'll pass the scope callback to the callScope method which will take // care of grouping the "wheres" properly so the logical order doesn't get // messed up when adding scopes. Then we'll return back out the builder. - $builder = $builder->callScope( - [$this->model, 'scope'.ucfirst($scope)], - (array) $parameters - ); + $builder = $builder->callScope([$this->model, 'scope'.ucfirst($scope)], (array) $parameters); } return $builder; @@ -564,14 +578,14 @@ public function scopes(array $scopes) */ public function applyScopes() { - if (!$this->scopes) { + if (! $this->scopes) { return $this; } $builder = clone $this; foreach ($this->scopes as $identifier => $scope) { - if (!isset($builder->scopes[$identifier])) { + if (! isset($builder->scopes[$identifier])) { continue; } @@ -595,11 +609,176 @@ public function applyScopes() return $builder; } + /** + * Set the relationships that should be eager loaded. + * + * @param mixed $relations + * @return $this + */ + public function with($relations) + { + $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); + + $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); + + return $this; + } + + /** + * Prevent the specified relations from being eager loaded. + * + * @param mixed $relations + * @return $this + */ + public function without($relations) + { + $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip( + is_string($relations) ? func_get_args() : $relations + )); + + return $this; + } + + /** + * Create a new instance of the model being queried. + * + * @param array $attributes + * @return Model + */ + public function newModelInstance($attributes = []) + { + return $this->model->newInstance($attributes) + ->setConnection($this->query->getConnection()->getName()); + } + + public function create($attributes = []) + { + return tap($this->newModelInstance($attributes), static function ($instance) { + $instance->save(); + }); + } + + public function getQuery(): QueryBuilder + { + return $this->query; + } + + public function setQuery(QueryBuilder $query) + { + $this->query = $query; + + return $this; + } + + /** + * Get a base query builder instance. + * + * @return \Illuminate\Database\Query\Builder + */ + public function toBase() + { + return $this->applyScopes() + ->getQuery(); + } + + /** + * Get the relationships being eagerly loaded. + * + * @return array + */ + public function getEagerLoads() + { + return $this->eagerLoad; + } + + /** + * Set the relationships being eagerly loaded. + * + * @return $this + */ + public function setEagerLoads(array $eagerLoad) + { + $this->eagerLoad = $eagerLoad; + + return $this; + } + + public function getModel(): Model + { + return $this->model; + } + + public function setModel(Model $model) + { + $this->model = $model; + + $this->query->from($model->getTable()); + + return $this; + } + + /** + * Get the given macro by name. + * + * @param string $name + * @return Closure + */ + public function getMacro($name) + { + return Arr::get($this->localMacros, $name); + } + + /** + * Eagerly load the relationship on a set of models. + */ + protected function eagerLoadRelation(array $models, string $name, Closure $constraints): array + { + // First we will "back up" the existing where conditions on the query so we can + // add our eager constraints. Then we will merge the wheres that were on the + // query back to it in order that any where conditions might be specified. + $relation = $this->getRelation($name); + + $relation->addEagerConstraints($models); + + $constraints($relation); + + // Once we have the results, we just match those back up to their parent models + // using the relationship instance. Then we just return the finished arrays + // of models which have been eagerly hydrated and are readied for return. + return $relation->match($relation->initRelation($models, $name), $relation->getEager(), $name); + } + + /** + * Get the deeply nested relations for a given top-level relation. + */ + protected function relationsNestedUnder(string $relation): array + { + $nested = []; + + // We are basically looking for any relationships that are nested deeper than + // the given top-level relationship. We will just check for any relations + // that start with the given top relations and adds them to our arrays. + foreach ($this->eagerLoad as $name => $constraints) { + if ($this->isNestedUnder($relation, $name)) { + $nested[substr($name, strlen($relation.'.'))] = $constraints; + } + } + + return $nested; + } + + /** + * Determine if the relationship is nested. + */ + protected function isNestedUnder(string $relation, string $name): bool + { + return Str::contains($name, '.') && Str::startsWith($name, $relation.'.'); + } + /** * Apply the given scope on the current builder instance. * - * @param callable $scope - * @param array $parameters + * @param array $parameters * @return mixed */ protected function callScope(callable $scope, $parameters = []) @@ -616,7 +795,7 @@ protected function callScope(callable $scope, $parameters = []) $result = $scope(...array_values($parameters)) ?? $this; - if (count((array) $query->getWheres()) > $originalWhereCount) { + if (count($query->getWheres()) > $originalWhereCount) { $this->addNewWheresWithinGroup($query, $originalWhereCount); } @@ -626,9 +805,8 @@ protected function callScope(callable $scope, $parameters = []) /** * Nest where conditions by slicing them at the given where count. * - * @param \Illuminate\Database\Query\Builder $query - * @param int $originalWhereCount - * @return void + * @param \Illuminate\Database\Query\Builder $query + * @param int $originalWhereCount */ protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCount) { @@ -639,33 +817,27 @@ protected function addNewWheresWithinGroup(QueryBuilder $query, $originalWhereCo $query->wheres = []; - $this->groupWhereSliceForScope( - $query, array_slice($allWheres, 0, $originalWhereCount) - ); + $this->groupWhereSliceForScope($query, array_slice($allWheres, 0, $originalWhereCount)); - $this->groupWhereSliceForScope( - $query, array_slice($allWheres, $originalWhereCount) - ); + $this->groupWhereSliceForScope($query, array_slice($allWheres, $originalWhereCount)); } /** * Slice where conditions at the given offset and add them to the query as a nested condition. * - * @param \Illuminate\Database\Query\Builder $query - * @param array $whereSlice - * @return void + * @param \Illuminate\Database\Query\Builder $query + * @param array $whereSlice */ protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) { - $whereBooleans = collect($whereSlice)->pluck('boolean'); + $whereBooleans = collect($whereSlice) + ->pluck('boolean'); // Here we'll check if the given subset of where clauses contains any "or" // booleans and in this case create a nested where expression. That way // we don't add any unnecessary nesting thus keeping the query clean. if ($whereBooleans->contains('or')) { - $query->wheres[] = $this->createNestedWhere( - $whereSlice, $whereBooleans->first() - ); + $query->wheres[] = $this->createNestedWhere($whereSlice, $whereBooleans->first()); } else { $query->wheres = array_merge($query->wheres, $whereSlice); } @@ -674,66 +846,27 @@ protected function groupWhereSliceForScope(QueryBuilder $query, $whereSlice) /** * Create a where array with nested where conditions. * - * @param array $whereSlice - * @param string $boolean + * @param array $whereSlice + * @param string $boolean * @return array */ protected function createNestedWhere($whereSlice, $boolean = 'and') { - $whereGroup = $this->getQuery()->forNestedWhere(); + $whereGroup = $this->getQuery() + ->forNestedWhere(); $whereGroup->wheres = $whereSlice; - return ['type' => 'Nested', 'query' => $whereGroup, 'boolean' => $boolean]; - } - - /** - * Set the relationships that should be eager loaded. - * - * @param mixed $relations - * @return $this - */ - public function with($relations) - { - $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); - - $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); - - return $this; - } - - /** - * Prevent the specified relations from being eager loaded. - * - * @param mixed $relations - * @return $this - */ - public function without($relations) - { - $this->eagerLoad = array_diff_key($this->eagerLoad, array_flip( - is_string($relations) ? func_get_args() : $relations - )); - - return $this; - } - - /** - * Create a new instance of the model being queried. - * - * @param array $attributes - * @return Model - */ - public function newModelInstance($attributes = []) - { - return $this->model->newInstance($attributes)->setConnection( - $this->query->getConnection()->getName() - ); + return [ + 'type' => 'Nested', + 'query' => $whereGroup, + 'boolean' => $boolean, + ]; } /** * Parse a list of relations into individuals. * - * @param array $relations * @return array */ protected function parseWithRelations(array $relations) @@ -747,7 +880,7 @@ protected function parseWithRelations(array $relations) if (is_numeric($name)) { $name = $constraints; - list($name, $constraints) = Str::contains($name, ':') + [$name, $constraints] = Str::contains($name, ':') ? $this->createSelectWithConstraint($name) : [ $name, @@ -771,7 +904,7 @@ function () { /** * Create a constraint to select the given columns for the relation. * - * @param string $name + * @param string $name * @return array */ protected function createSelectWithConstraint($name) @@ -787,8 +920,8 @@ function ($query) use ($name) { /** * Parse the nested relationships in a relation. * - * @param string $name - * @param array $results + * @param string $name + * @param array $results * @return array */ protected function addNestedWiths($name, $results) @@ -801,7 +934,7 @@ protected function addNestedWiths($name, $results) foreach (explode('.', $name) as $segment) { $progress[] = $segment; - if (!isset($results[$last = implode('.', $progress)])) { + if (! isset($results[$last = implode('.', $progress)])) { $results[$last] = function () { // }; @@ -810,154 +943,4 @@ protected function addNestedWiths($name, $results) return $results; } - - public function getQuery(): QueryBuilder - { - return $this->query; - } - - public function setQuery(QueryBuilder $query) - { - $this->query = $query; - - return $this; - } - - /** - * Get a base query builder instance. - * - * @return \Illuminate\Database\Query\Builder - */ - public function toBase() - { - return $this->applyScopes()->getQuery(); - } - - /** - * Get the relationships being eagerly loaded. - * - * @return array - */ - public function getEagerLoads() - { - return $this->eagerLoad; - } - - /** - * Set the relationships being eagerly loaded. - * - * @param array $eagerLoad - * @return $this - */ - public function setEagerLoads(array $eagerLoad) - { - $this->eagerLoad = $eagerLoad; - - return $this; - } - - public function getModel(): Model - { - return $this->model; - } - - public function setModel(Model $model) - { - $this->model = $model; - - $this->query->from($model->getTable()); - - return $this; - } - - /** - * Get the given macro by name. - * - * @param string $name - * @return \Closure - */ - public function getMacro($name) - { - return Arr::get($this->localMacros, $name); - } - - /** - * Dynamically handle calls into the query instance. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - if ($method === 'macro') { - $this->localMacros[$parameters[0]] = $parameters[1]; - - return; - } - - if (isset($this->localMacros[$method])) { - array_unshift($parameters, $this); - - return $this->localMacros[$method](...$parameters); - } - - if (isset(static::$macros[$method])) { - if (static::$macros[$method] instanceof Closure) { - return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters); - } - - return call_user_func_array(static::$macros[$method], $parameters); - } - - if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { - return $this->callScope([$this->model, $scope], $parameters); - } - - if (in_array($method, $this->passthru)) { - return $this->toBase()->{$method}(...$parameters); - } - - $this->query->{$method}(...$parameters); - - return $this; - } - - /** - * Dynamically handle calls into the query instance. - * - * @param string $method - * @param array $parameters - * @return mixed - * - * @throws \BadMethodCallException - */ - public static function __callStatic($method, $parameters) - { - if ($method === 'macro') { - static::$macros[$parameters[0]] = $parameters[1]; - - return; - } - - if (!isset(static::$macros[$method])) { - throw new BadMethodCallException("Method {$method} does not exist."); - } - - if (static::$macros[$method] instanceof Closure) { - return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters); - } - - return call_user_func_array(static::$macros[$method], $parameters); - } - - /** - * Force a clone of the underlying query builder when cloning. - * - * @return void - */ - public function __clone() - { - $this->query = clone $this->query; - } } diff --git a/src/Database/Eloquent/Collection.php b/src/Database/Eloquent/Collection.php index 6c73cff..10d4f97 100644 --- a/src/Database/Eloquent/Collection.php +++ b/src/Database/Eloquent/Collection.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Database\Eloquent; +namespace Bavix\LaravelClickHouse\Database\Eloquent; use Illuminate\Support\Arr; use Illuminate\Support\Collection as SupportCollection; @@ -12,9 +12,9 @@ class Collection extends \Illuminate\Database\Eloquent\Collection /** * Find a model in the collection by key. * - * @param mixed $key - * @param mixed $default - * @return \Illuminate\Database\Eloquent\Model|static + * @param mixed $key + * @param mixed $default + * @return Model|static */ public function find($key, $default = null) { @@ -24,7 +24,7 @@ public function find($key, $default = null) if (is_array($key)) { if ($this->isEmpty()) { - return new static; + return new static(); } return $this->whereIn($this->first()->getKeyName(), $key); @@ -38,10 +38,9 @@ public function find($key, $default = null) /** * Determine if a key exists in the collection. * - * @param mixed $key - * @param mixed $operator - * @param mixed $value - * @return bool + * @param mixed $key + * @param mixed $operator + * @param mixed $value */ public function contains($key, $operator = null, $value = null): bool { @@ -57,7 +56,6 @@ public function contains($key, $operator = null, $value = null): bool /** * Run a map over each of the items. * - * @param callable $callback * @return SupportCollection|static */ public function map(callable $callback) @@ -65,7 +63,7 @@ public function map(callable $callback) $result = SupportCollection::map($callback); return $result->contains(function ($item) { - return !$item instanceof Model; + return ! $item instanceof Model; }) ? $result->toBase() : $result; } } diff --git a/src/Database/Eloquent/Concerns/Common.php b/src/Database/Eloquent/Concerns/Common.php new file mode 100644 index 0000000..9aa6ee0 --- /dev/null +++ b/src/Database/Eloquent/Concerns/Common.php @@ -0,0 +1,16 @@ +toArray()); + } +} diff --git a/src/Database/Eloquent/Concerns/HasAttributes.php b/src/Database/Eloquent/Concerns/HasAttributes.php index 8110767..6b28a8e 100644 --- a/src/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Database/Eloquent/Concerns/HasAttributes.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Database\Eloquent\Concerns; +namespace Bavix\LaravelClickHouse\Database\Eloquent\Concerns; use Illuminate\Database\Eloquent\Concerns\HasAttributes as BaseHasAttributes; @@ -12,12 +12,12 @@ trait HasAttributes public function getDates(): array { - return $this->dates; + return $this->dates ?? []; } public function getCasts(): array { - return $this->casts; + return $this->casts ?? []; } protected function getDateFormat(): string diff --git a/src/Database/Eloquent/Model.php b/src/Database/Eloquent/Model.php index 7b9fffd..36b8a9b 100644 --- a/src/Database/Eloquent/Model.php +++ b/src/Database/Eloquent/Model.php @@ -2,42 +2,61 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Database\Eloquent; +namespace Bavix\LaravelClickHouse\Database\Eloquent; use ArrayAccess; -use JsonSerializable; -use Illuminate\Support\Str; -use Illuminate\Contracts\Support\Jsonable; +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Query\Builder as QueryBuilder; +use Closure; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Contracts\Support\Arrayable; -use Tinderbox\ClickhouseBuilder\Query\Grammar; -use Esazykin\LaravelClickHouse\Database\Connection; +use Illuminate\Contracts\Support\Jsonable; +use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\Eloquent\Concerns\GuardsAttributes; use Illuminate\Database\Eloquent\Concerns\HasEvents; +use Illuminate\Database\Eloquent\Concerns\HasRelationships; +use Illuminate\Database\Eloquent\Concerns\HidesAttributes; use Illuminate\Database\Eloquent\JsonEncodingException; use Illuminate\Database\Eloquent\MassAssignmentException; -use Illuminate\Database\Eloquent\Concerns\HidesAttributes; -use Illuminate\Database\Eloquent\Concerns\GuardsAttributes; -use Illuminate\Database\Eloquent\Concerns\HasRelationships; -use Illuminate\Database\ConnectionResolverInterface as Resolver; -use Esazykin\LaravelClickHouse\Database\Query\Builder as QueryBuilder; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Str; +use JsonSerializable; +use Tinderbox\ClickhouseBuilder\Query\Grammar; -/** - * @mixin \Eloquent - */ -abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable +abstract class Model implements ArrayAccess, UrlRoutable, Arrayable, Jsonable, JsonSerializable { - use Concerns\HasAttributes, - HasEvents, - HasRelationships, - HidesAttributes, - GuardsAttributes; + use Concerns\HasAttributes; + use Concerns\Common; + use HasEvents; + use HasRelationships; + use HidesAttributes; + use GuardsAttributes; + + /** + * Indicates if the model exists. + */ + public bool $exists = false; + + public bool $wasRecentlyCreated = false; + + /** + * Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model. + * + * @var bool + */ + protected static $modelsShouldPreventAccessingMissingAttributes = false; /** * The connection name for the model. * * @var string */ - protected $connection = 'clickhouse'; + protected $connection = 'bavix::clickhouse'; /** * The table associated with the model. @@ -81,26 +100,9 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab */ protected $perPage = 15; - /** - * Indicates if the model exists. - * - * @var bool - */ - public $exists = false; - - /** - * The connection resolver instance. - * - * @var \Illuminate\Database\ConnectionResolverInterface - */ - protected static $resolver; + protected static ConnectionResolverInterface $resolver; - /** - * The event dispatcher instance. - * - * @var \Illuminate\Contracts\Events\Dispatcher - */ - protected static $dispatcher; + protected static Dispatcher $dispatcher; /** * The array of booted models. @@ -111,9 +113,6 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab /** * Create a new Eloquent model instance. - * - * @param array $attributes - * @return void */ public function __construct(array $attributes = []) { @@ -125,53 +124,75 @@ public function __construct(array $attributes = []) } /** - * Check if the model needs to be booted and if so, do it. + * Dynamically retrieve attributes on the model. * - * @return void + * @param string $key + * @return mixed */ - protected function bootIfNotBooted() + public function __get($key) { - if (!isset(static::$booted[static::class])) { - static::$booted[static::class] = true; - - $this->fireModelEvent('booting', false); + return $this->getAttribute($key); + } - static::boot(); + /** + * Dynamically set attributes on the model. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) + { + $this->setAttribute($key, $value); + } - $this->fireModelEvent('booted', false); - } + /** + * Determine if an attribute or relation exists on the model. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + return $this->offsetExists($key); } /** - * The "booting" method of the model. + * Unset an attribute on the model. * - * @return void + * @param string $key */ - protected static function boot() + public function __unset($key) { - static::bootTraits(); + $this->offsetUnset($key); } /** - * Boot all of the bootable traits on the model. + * Handle dynamic method calls into the model. * - * @return void + * @param string $method + * @param array $parameters + * @return mixed */ - protected static function bootTraits() + public function __call($method, $parameters) { - $class = static::class; + return $this->newQuery() + ->{$method}(...$parameters); + } - foreach (class_uses_recursive($class) as $trait) { - if (method_exists($class, $method = 'boot'.class_basename($trait))) { - forward_static_call([$class, $method]); - } - } + /** + * Handle dynamic static method calls into the method. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public static function __callStatic($method, $parameters) + { + return (new static())->{$method}(...$parameters); } /** * Clear the list of booted models so they will be re-booted. - * - * @return void */ public static function clearBootedModels() { @@ -181,10 +202,9 @@ public static function clearBootedModels() /** * Fill the model with an array of attributes. * - * @param array $attributes * @return $this * - * @throws \Illuminate\Database\Eloquent\MassAssignmentException + * @throws MassAssignmentException */ public function fill(array $attributes) { @@ -209,7 +229,6 @@ public function fill(array $attributes) /** * Fill the model with an array of attributes. Force mass assignment. * - * @param array $attributes * @return $this */ public function forceFill(array $attributes) @@ -219,22 +238,11 @@ public function forceFill(array $attributes) }); } - /** - * Remove the table name from a given key. - * - * @param string $key - * @return string - */ - protected function removeTableFromKey($key) - { - return Str::contains($key, '.') ? last(explode('.', $key)) : $key; - } - /** * Create a new instance of the given model. * - * @param array $attributes - * @param bool $exists + * @param array $attributes + * @param bool $exists * @return static */ public function newInstance($attributes = [], $exists = false) @@ -246,9 +254,7 @@ public function newInstance($attributes = [], $exists = false) $model->exists = $exists; - $model->setConnection( - $this->getConnectionName() - ); + $model->setConnection($this->getConnectionName()); return $model; } @@ -256,8 +262,6 @@ public function newInstance($attributes = [], $exists = false) /** * Create a new model instance that is existing. * - * @param array $attributes - * @param string|null $connection * @return static */ public function newFromBuilder(array $attributes = [], string $connection = null) @@ -266,7 +270,9 @@ public function newFromBuilder(array $attributes = [], string $connection = null $model->setRawAttributes($attributes, true); - $model->setConnection($connection ?: $this->getConnectionName()); + $model->setConnection( + $connection !== null && $connection !== '' && $connection !== '0' ? $connection : $this->getConnectionName() + ); $model->fireModelEvent('retrieved', false); @@ -280,25 +286,25 @@ public function newFromBuilder(array $attributes = [], string $connection = null */ public static function all() { - return (new static)->newQuery()->get(); + return (new static())->newQuery() + ->get(); } /** * Begin querying a model with eager loading. * - * @param array|string $relations + * @param array|string $relations * @return Builder|static */ public static function with($relations) { - return (new static)->newQuery()->with( - is_string($relations) ? func_get_args() : $relations - ); + return (new static())->newQuery() + ->with(is_string($relations) ? func_get_args() : $relations); } public static function query(): Builder { - return (new static)->newQuery(); + return (new static())->newQuery(); } public function newQuery(): Builder @@ -327,19 +333,8 @@ public function newEloquentBuilder(QueryBuilder $query): Builder return new Builder($query); } - protected function newBaseQueryBuilder(): QueryBuilder - { - /** @var Connection $connection */ - $connection = $this->getConnection(); - - return new QueryBuilder($connection, new Grammar()); - } - /** * Create a new Eloquent Collection instance. - * - * @param array $models - * @return Collection */ public function newCollection(array $models = []): Collection { @@ -348,8 +343,6 @@ public function newCollection(array $models = []): Collection /** * Convert the model instance to an array. - * - * @return array */ public function toArray(): array { @@ -359,16 +352,15 @@ public function toArray(): array /** * Convert the model instance to JSON. * - * @param int $options - * @return string + * @param int $options * - * @throws \Illuminate\Database\Eloquent\JsonEncodingException + * @throws JsonEncodingException */ public function toJson($options = 0): string { $json = json_encode($this->jsonSerialize(), $options); - if (JSON_ERROR_NONE !== json_last_error()) { + if (json_last_error() !== JSON_ERROR_NONE) { throw JsonEncodingException::forModel($this, json_last_error_msg()); } @@ -377,8 +369,6 @@ public function toJson($options = 0): string /** * Convert the object into something JSON serializable. - * - * @return array */ public function jsonSerialize(): array { @@ -387,18 +377,14 @@ public function jsonSerialize(): array /** * Get the database connection for the model. - * - * @return \Illuminate\Database\Connection */ - public function getConnection(): \Illuminate\Database\Connection + public function getConnection(): ConnectionInterface { return static::resolveConnection($this->getConnectionName()); } /** * Get the current connection name for the model. - * - * @return string */ public function getConnectionName(): string { @@ -408,7 +394,6 @@ public function getConnectionName(): string /** * Set the connection associated with the model. * - * @param string $name * @return $this */ public function setConnection(string $name) @@ -420,19 +405,14 @@ public function setConnection(string $name) /** * Resolve a connection instance. - * - * @param string|null $connection - * @return Connection|\Illuminate\Database\ConnectionInterface */ - public static function resolveConnection(string $connection = null) + public static function resolveConnection(string $connection = null): ConnectionInterface { - return static::$resolver->connection($connection); + return static::getConnectionResolver()->connection($connection); } /** * Get the connection resolver instance. - * - * @return ConnectionResolverInterface */ public static function getConnectionResolver(): ConnectionResolverInterface { @@ -441,9 +421,6 @@ public static function getConnectionResolver(): ConnectionResolverInterface /** * Set the connection resolver instance. - * - * @param \Illuminate\Database\ConnectionResolverInterface $resolver - * @return void */ public static function setConnectionResolver(Resolver $resolver): void { @@ -452,15 +429,11 @@ public static function setConnectionResolver(Resolver $resolver): void /** * Get the table associated with the model. - * - * @return string */ public function getTable(): string { - if (!isset($this->table)) { - $this->setTable(str_replace( - '\\', '', Str::snake(Str::plural(class_basename($this))) - )); + if (! isset($this->table)) { + $this->setTable(str_replace('\\', '', Str::snake(Str::plural(class_basename($this))))); } return $this->table; @@ -469,7 +442,6 @@ public function getTable(): string /** * Set the table associated with the model. * - * @param string $table * @return $this */ public function setTable(string $table): self @@ -479,10 +451,42 @@ public function setTable(string $table): self return $this; } + public function resolveRouteBinding($value, $field = null): ?Model + { + return $this->resolveRouteBindingQuery($this, $value, $field) + ->first(); + } + + public function resolveSoftDeletableRouteBinding(mixed $value, ?string $field = null): ?Model + { + return $this->resolveRouteBindingQuery($this, $value, $field) + ->withTrashed() + ->first(); + } + + public function resolveChildRouteBinding($childType, $value, $field): ?Model + { + return $this->resolveChildRouteBindingQuery($childType, $value, $field) + ->first(); + } + + public function resolveSoftDeletableChildRouteBinding( + string $childType, + mixed $value, + ?string $field = null + ): ?Model { + return $this->resolveChildRouteBindingQuery($childType, $value, $field) + ->withTrashed() + ->first(); + } + + public function resolveRouteBindingQuery(Relation|Model $query, mixed $value, ?string $field = null): Builder + { + return $query->where($field ?? $this->getRouteKeyName(), $value); + } + /** * Get the primary key for the model. - * - * @return string */ public function getKeyName(): string { @@ -492,7 +496,6 @@ public function getKeyName(): string /** * Set the primary key for the model. * - * @param string $key * @return $this */ public function setKeyName(string $key): self @@ -504,8 +507,6 @@ public function setKeyName(string $key): self /** * Get the table qualified key name. - * - * @return string */ public function getQualifiedKeyName(): string { @@ -514,8 +515,6 @@ public function getQualifiedKeyName(): string /** * Get the pk key type. - * - * @return string */ public function getKeyType(): string { @@ -525,7 +524,6 @@ public function getKeyType(): string /** * Set the data type for the primary key. * - * @param string $type * @return $this */ public function setKeyType(string $type) @@ -535,6 +533,16 @@ public function setKeyType(string $type) return $this; } + public function getRouteKey(): mixed + { + return $this->getAttribute($this->getRouteKeyName()); + } + + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + /** * Get the value of the model's primary key. * @@ -547,8 +555,6 @@ public function getKey() /** * Get the default foreign key name for the model. - * - * @return string */ public function getForeignKey(): string { @@ -557,8 +563,6 @@ public function getForeignKey(): string /** * Get the number of models to return per page. - * - * @return int */ public function getPerPage(): int { @@ -567,45 +571,18 @@ public function getPerPage(): int /** * Set the number of models to return per page. - * - * @param int $perPage - * @return $this */ - public function setPerPage(int $perPage): self + public function setPerPage(int $perPage): static { $this->perPage = $perPage; return $this; } - /** - * Dynamically retrieve attributes on the model. - * - * @param string $key - * @return mixed - */ - public function __get($key) - { - return $this->getAttribute($key); - } - - /** - * Dynamically set attributes on the model. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function __set($key, $value) - { - $this->setAttribute($key, $value); - } - /** * Determine if the given attribute exists. * - * @param mixed $offset - * @return bool + * @param mixed $offset */ public function offsetExists($offset): bool { @@ -615,9 +592,10 @@ public function offsetExists($offset): bool /** * Get the value for a given offset. * - * @param mixed $offset + * @param mixed $offset * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->getAttribute($offset); @@ -626,9 +604,8 @@ public function offsetGet($offset) /** * Set the value for a given offset. * - * @param mixed $offset - * @param mixed $value - * @return void + * @param mixed $offset + * @param mixed $value */ public function offsetSet($offset, $value): void { @@ -638,57 +615,103 @@ public function offsetSet($offset, $value): void /** * Unset the value for a given offset. * - * @param mixed $offset - * @return void + * @param mixed $offset */ public function offsetUnset($offset): void { unset($this->attributes[$offset]); } + public static function preventsAccessingMissingAttributes(): bool + { + return static::$modelsShouldPreventAccessingMissingAttributes; + } + + protected static function whenBooted(Closure $callback) + { + } + + protected function resolveChildRouteBindingQuery( + string $childType, + mixed $value, + ?string $field = null + ): Relation|Model { + $relationship = $this->{$this->childRouteBindingRelationshipName($childType)}(); + + $field = $field !== null && $field !== '' && $field !== '0' ? $field : $relationship->getRelated() + ->getRouteKeyName(); + + if ($relationship instanceof HasManyThrough || + $relationship instanceof BelongsToMany) { + $field = $relationship->getRelated() + ->getTable().'.'.$field; + } + + return $relationship instanceof Model + ? $relationship->resolveRouteBindingQuery($relationship, $value, $field) + : $relationship->getRelated() + ->resolveRouteBindingQuery($relationship, $value, $field); + } + + protected function childRouteBindingRelationshipName(string $childType): string + { + return Str::plural(Str::camel($childType)); + } + /** - * Determine if an attribute or relation exists on the model. - * - * @param string $key - * @return bool + * Check if the model needs to be booted and if so, do it. */ - public function __isset($key) + protected function bootIfNotBooted() { - return $this->offsetExists($key); + if (! isset(static::$booted[static::class])) { + static::$booted[static::class] = true; + + $this->fireModelEvent('booting', false); + + static::boot(); + + $this->fireModelEvent('booted', false); + } } /** - * Unset an attribute on the model. - * - * @param string $key - * @return void + * The "booting" method of the model. */ - public function __unset($key) + protected static function boot() { - $this->offsetUnset($key); + static::bootTraits(); } /** - * Handle dynamic method calls into the model. - * - * @param string $method - * @param array $parameters - * @return mixed + * Boot all of the bootable traits on the model. */ - public function __call($method, $parameters) + protected static function bootTraits() { - return $this->newQuery()->$method(...$parameters); + $class = static::class; + + foreach (class_uses_recursive($class) as $trait) { + if (method_exists($class, $method = 'boot'.class_basename($trait))) { + forward_static_call([$class, $method]); + } + } } /** - * Handle dynamic static method calls into the method. + * Remove the table name from a given key. * - * @param string $method - * @param array $parameters - * @return mixed + * @param string $key + * @return string */ - public static function __callStatic($method, $parameters) + protected function removeTableFromKey($key) { - return (new static)->$method(...$parameters); + return Str::contains($key, '.') ? last(explode('.', $key)) : $key; + } + + protected function newBaseQueryBuilder(): QueryBuilder + { + /** @var Connection $connection */ + $connection = $this->getConnection(); + + return new QueryBuilder($connection, new Grammar()); } } diff --git a/src/Database/Query/Builder.php b/src/Database/Query/Builder.php index 7d24f27..58e8419 100644 --- a/src/Database/Query/Builder.php +++ b/src/Database/Query/Builder.php @@ -2,28 +2,27 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Database\Query; +namespace Bavix\LaravelClickHouse\Database\Query; +use Bavix\LaravelClickHouse\Database\Connection; use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Tinderbox\Clickhouse\Common\Format; use Illuminate\Support\Traits\Macroable; -use Tinderbox\ClickhouseBuilder\Query\Grammar; +use Tinderbox\Clickhouse\Common\Format; use Tinderbox\ClickhouseBuilder\Query\BaseBuilder; -use Esazykin\LaravelClickHouse\Database\Connection; +use Tinderbox\ClickhouseBuilder\Query\Expression; +use Tinderbox\ClickhouseBuilder\Query\Grammar; class Builder extends BaseBuilder { - use Macroable { + use Macroable { __call as macroCall; } - protected $connection; + protected Connection $connection; - public function __construct( - Connection $connection, - Grammar $grammar - ) { + public function __construct(Connection $connection, Grammar $grammar) + { $this->connection = $connection; $this->grammar = $grammar; } @@ -32,12 +31,10 @@ public function __construct( * Perform compiled from builder sql query and getting result. * * @throws \Tinderbox\Clickhouse\Exceptions\ClientException - * - * @return Collection */ public function get(): Collection { - if (!empty($this->async)) { + if ($this->async !== []) { $result = $this->connection->selectAsync($this->toAsyncSqls()); } else { $result = $this->connection->select($this->toSql(), [], $this->getFiles()); @@ -53,12 +50,10 @@ public function get(): Collection * @param string $column Column to pass into count() aggregate function * * @throws \Tinderbox\Clickhouse\Exceptions\ClientException - * - * @return int */ public function count($column = '*'): int { - $builder = $this->getCountQuery($column); + $builder = $this->getCountQuery(); $result = $builder->get(); if (count($this->groups) > 0) { @@ -71,19 +66,18 @@ public function count($column = '*'): int /** * Perform query and get first row. * - * @throws \Tinderbox\Clickhouse\Exceptions\ClientException - * * @return mixed|null + * + * @throws \Tinderbox\Clickhouse\Exceptions\ClientException */ public function first() { - return $this->get()->first(); + return $this->get() + ->first(); } /** * Makes clean instance of builder. - * - * @return self */ public function newQuery(): self { @@ -93,19 +87,13 @@ public function newQuery(): self /** * Insert in table data from files. * - * @param array $columns - * @param array $files - * @param string $format - * @param int $concurrency - * * @throws \Tinderbox\Clickhouse\Exceptions\ClientException - * - * @return array */ public function insertFiles(array $columns, array $files, string $format = Format::CSV, int $concurrency = 5): array { return $this->connection->insertFiles( - (string) $this->getFrom()->getTable(), + (string) $this->getFrom() + ->getTable(), $columns, $files, $format, @@ -115,35 +103,52 @@ public function insertFiles(array $columns, array $files, string $format = Forma /** * Performs insert query. - * - * @param array $values - * - * @return bool */ public function insert(array $values): bool { - if (empty($values)) { + if ($values === []) { return false; } - if (!is_array(reset($values))) { + if (! is_array(reset($values))) { $values = [$values]; } // Here, we will sort the insert keys for every record so that each insert is // in the same order for the record. We need to make sure this is the case // so there are not any errors or problems when inserting these records. - else { - foreach ($values as $key => $value) { - ksort($value); - $values[$key] = $value; - } + foreach ($values as &$value) { + ksort($value); } - return $this->connection->insert( - $this->grammar->compileInsert($this, $values), - Arr::flatten($values) - ); + return $this->connection->insert($this->grammar->compileInsert($this, $values), Arr::flatten($values)); + } + + public function getCountForPagination() + { + return (int) $this->getConnection() + ->table( + $this + ->cloneWithout([ + 'columns' => [], + 'orders' => [], + 'limit' => null, + ]) + ->select(new Expression('1')), null + ) + ->count(); + } + + /** + * Set the limit and offset for a given page. + * + * @param int $page + * @param int $perPage + * @return $this + */ + public function forPage($page, $perPage = 15) + { + return $this->limit($perPage, ($page - 1) * $perPage); } public function getConnection(): Connection diff --git a/src/Database/Query/Pdo.php b/src/Database/Query/Pdo.php new file mode 100644 index 0000000..f256834 --- /dev/null +++ b/src/Database/Query/Pdo.php @@ -0,0 +1,13 @@ + 'timestamp', ]; - protected $dates = [ - 'payed_at', - ]; + protected $dates = ['payed_at']; public function jsonAttributeValue() { diff --git a/tests/EloquentModelWithTest.php b/tests/BaseEloquentModelWith.php similarity index 71% rename from tests/EloquentModelWithTest.php rename to tests/BaseEloquentModelWith.php index af0abe5..4948df3 100644 --- a/tests/EloquentModelWithTest.php +++ b/tests/BaseEloquentModelWith.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests; +namespace Bavix\LaravelClickHouse\Tests; +use Bavix\LaravelClickHouse\Database\Eloquent\Builder; use Mockery\MockInterface; -use Esazykin\LaravelClickHouse\Database\Eloquent\Builder; -class EloquentModelWithTest extends EloquentModelTest +class BaseEloquentModelWith extends BaseEloquentModel { use Helpers; diff --git a/tests/FirstTableEntry.php b/tests/FirstTableEntry.php new file mode 100644 index 0000000..5dcb97b --- /dev/null +++ b/tests/FirstTableEntry.php @@ -0,0 +1,17 @@ +set('database.connections.bavix::clickhouse', [ + 'host' => env('CLICKHOUSE_HOST', 'localhost'), + 'port' => env('CLICKHOUSE_PORT', '8123'), + 'driver' => env('CLICKHOUSE_DRIVER', 'bavix::clickhouse'), + 'database' => env('CLICKHOUSE_DATABASE', 'default'), + 'username' => env('CLICKHOUSE_USERNAME', 'default'), + 'password' => env('CLICKHOUSE_PASSWORD', ''), + ]); + } +} diff --git a/tests/Unit/Database/ConnectionTest.php b/tests/Unit/Database/ConnectionTest.php index c2ac8a5..c5904ae 100644 --- a/tests/Unit/Database/ConnectionTest.php +++ b/tests/Unit/Database/ConnectionTest.php @@ -2,18 +2,86 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests\Database; +namespace Bavix\LaravelClickHouse\Tests\Unit\Database; -use PHPUnit\Framework\TestCase; -use Esazykin\LaravelClickHouse\Database\Connection; -use Esazykin\LaravelClickHouse\Database\Query\Builder; +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Query\Builder; +use Bavix\LaravelClickHouse\Tests\TestCase; +use Tinderbox\Clickhouse\Exceptions\ClientException; class ConnectionTest extends TestCase { - public function testQuery() + /** + * @var Connection + */ + protected $connection; + + protected function setUp(): void { - $connection = new Connection(['host' => 'localhost']); + parent::setUp(); + $this->connection = $this->getConnection('bavix::clickhouse'); + } + + public function testQuery(): void + { + self::assertInstanceOf(Builder::class, $this->connection->query()); + } + + /** + * @throws ClientException + */ + public function testSystemEvents(): void + { + self::assertIsNumeric($this->connection->query()->table('system.events')->count()); + } + + /** + * @throws ClientException + */ + public function testMyDatabase(): void + { + $result = $this->connection->statement('CREATE DATABASE IF NOT EXISTS tests'); + self::assertTrue($result); + + $result = $this->connection->statement('CREATE TABLE IF NOT EXISTS tests.dt +( + `timestamp` DateTime(\'Europe/Moscow\'), + `event_id` UInt8 +) +ENGINE = TinyLog;'); + + self::assertTrue($result); + + $result = $this->connection->statement('CREATE DATABASE IF NOT EXISTS tests'); + self::assertTrue($result); + + $result = $this->connection->statement('TRUNCATE tests.dt'); + self::assertTrue($result); + + $values = [ + [ + 'timestamp' => '2019-01-01 00:00:00', + 'event_id' => 1, + ], + [ + 'event_id' => 2, + 'timestamp' => '2020-01-01 00:00:00', + ], + [ + 'event_id' => 3, + 'timestamp' => 1546300800, + ], + ]; + $this->connection->query() + ->table('tests.dt') + ->insert($values); + + self::assertEquals(3, $this->connection->query()->table('tests.dt')->count()); + + $result = $this->connection->statement('DROP TABLE IF EXISTS tests.dt'); + self::assertTrue($result); - $this->assertInstanceOf(Builder::class, $connection->query()); + $result = $this->connection->statement('DROP DATABASE IF EXISTS tests'); + self::assertTrue($result); } } diff --git a/tests/Unit/Database/Eloquent/BuilderTest.php b/tests/Unit/Database/Eloquent/BuilderTest.php index 462178b..2371fd5 100644 --- a/tests/Unit/Database/Eloquent/BuilderTest.php +++ b/tests/Unit/Database/Eloquent/BuilderTest.php @@ -2,119 +2,134 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests\Unit\Database\Eloquent; - +namespace Bavix\LaravelClickHouse\Tests\Unit\Database\Eloquent; + +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Eloquent\Builder; +use Bavix\LaravelClickHouse\Database\Eloquent\Collection; +use Bavix\LaravelClickHouse\Database\Query\Builder as QueryBuilder; +use Bavix\LaravelClickHouse\Tests\BaseEloquentModelCasting; +use Bavix\LaravelClickHouse\Tests\Helpers; +use Illuminate\Database\DatabaseManager; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Mockery\Mock; +use Mockery\MockInterface; use PHPUnit\Framework\TestCase; -use Illuminate\Database\DatabaseManager; -use Tinderbox\ClickhouseBuilder\Query\Tuple; -use Esazykin\LaravelClickHouse\Tests\Helpers; +use Tinderbox\ClickhouseBuilder\Query\Enums\Operator; use Tinderbox\ClickhouseBuilder\Query\Grammar; use Tinderbox\ClickhouseBuilder\Query\Identifier; -use Esazykin\LaravelClickHouse\Database\Connection; -use Tinderbox\ClickhouseBuilder\Query\Enums\Operator; -use Illuminate\Database\Eloquent\ModelNotFoundException; -use Esazykin\LaravelClickHouse\Database\Eloquent\Builder; -use Esazykin\LaravelClickHouse\Database\Eloquent\Collection; -use Esazykin\LaravelClickHouse\Tests\EloquentModelCastingTest; +use Tinderbox\ClickhouseBuilder\Query\Tuple; use Tinderbox\ClickhouseBuilder\Query\TwoElementsLogicExpression; -use Esazykin\LaravelClickHouse\Database\Query\Builder as QueryBuilder; /** * @property Mock|Connection connection * @property Builder builder - * @property EloquentModelCastingTest model + * @property BaseEloquentModelCasting model */ class BuilderTest extends TestCase { use Helpers; - protected function setUp() + /** + * @var MockInterface&Connection + */ + private MockInterface $connection; + + private Builder $builder; + + private BaseEloquentModelCasting $model; + + protected function setUp(): void { parent::setUp(); $this->connection = $this->mock(Connection::class); - $this->model = new EloquentModelCastingTest(); + $this->model = new BaseEloquentModelCasting(); $this->builder = (new Builder(new QueryBuilder($this->connection, new Grammar()))) ->setModel($this->model); } - public function testWhereKey() + public function testWhereKey(): void { - $id = $this->faker()->numberBetween(1); + $id = $this->faker() + ->numberBetween(1); $this->builder->whereKey($id); - $wheres = $this->builder->getQuery()->getWheres(); + $wheres = $this->builder->getQuery() + ->getWheres(); - $this->assertCount(1, $wheres); + self::assertCount(1, $wheres); /** @var TwoElementsLogicExpression $expression */ $expression = $wheres[0]; - $this->assertInstanceOf(TwoElementsLogicExpression::class, $expression); + self::assertInstanceOf(TwoElementsLogicExpression::class, $expression); /** @var Identifier $first */ $first = $expression->getFirstElement(); - $this->assertInstanceOf(Identifier::class, $first); - $this->assertSame($this->model->getTable().'.'.$this->model->getKeyName(), (string) $first); - $this->assertSame($id, $expression->getSecondElement()); + self::assertInstanceOf(Identifier::class, $first); + self::assertSame($this->model->getTable().'.'.$this->model->getKeyName(), (string) $first); + self::assertSame($id, $expression->getSecondElement()); $operator = $expression->getOperator(); - $this->assertInstanceOf(Operator::class, $operator); - $this->assertSame('=', $operator->getValue()); + self::assertInstanceOf(Operator::class, $operator); + self::assertSame('=', $operator->getValue()); } - public function testWhereKeyNot() + public function testWhereKeyNot(): void { $ids = range(1, 5); $this->builder->whereKeyNot($ids); - $wheres = $this->builder->getQuery()->getWheres(); + $wheres = $this->builder->getQuery() + ->getWheres(); - $this->assertCount(1, $wheres); + self::assertCount(1, $wheres); /** @var TwoElementsLogicExpression $expression */ $expression = $wheres[0]; - $this->assertInstanceOf(TwoElementsLogicExpression::class, $expression); + self::assertInstanceOf(TwoElementsLogicExpression::class, $expression); /** @var Identifier $first */ $first = $expression->getFirstElement(); - $this->assertInstanceOf(Identifier::class, $first); - $this->assertSame($this->model->getTable().'.'.$this->model->getKeyName(), (string) $first); + self::assertInstanceOf(Identifier::class, $first); + self::assertSame($this->model->getTable().'.'.$this->model->getKeyName(), (string) $first); /** @var Tuple $second */ $second = $expression->getSecondElement(); - $this->assertSame($ids, $second->getElements()); + self::assertSame($ids, $second->getElements()); $operator = $expression->getOperator(); - $this->assertInstanceOf(Operator::class, $operator); - $this->assertSame('NOT IN', $operator->getValue()); + self::assertInstanceOf(Operator::class, $operator); + self::assertSame('NOT IN', $operator->getValue()); } - public function testWhereSimple() + public function testWhereSimple(): void { - $date = $this->faker()->date(); + $date = $this->faker() + ->date(); $this->builder->where('date_column', '>', $date); - $wheres = $this->builder->getQuery()->getWheres(); + $wheres = $this->builder->getQuery() + ->getWheres(); - $this->assertCount(1, $wheres); + self::assertCount(1, $wheres); /** @var TwoElementsLogicExpression $expression */ $expression = $wheres[0]; - $this->assertInstanceOf(TwoElementsLogicExpression::class, $expression); + self::assertInstanceOf(TwoElementsLogicExpression::class, $expression); /** @var Identifier $first */ $first = $expression->getFirstElement(); - $this->assertInstanceOf(Identifier::class, $first); - $this->assertSame('date_column', (string) $first); - $this->assertSame($date, $expression->getSecondElement()); + self::assertInstanceOf(Identifier::class, $first); + self::assertSame('date_column', (string) $first); + self::assertSame($date, $expression->getSecondElement()); $operator = $expression->getOperator(); - $this->assertInstanceOf(Operator::class, $operator); - $this->assertSame('>', $operator->getValue()); + self::assertInstanceOf(Operator::class, $operator); + self::assertSame('>', $operator->getValue()); } - public function testWhereClosure() + public function testWhereClosure(): void { /** @var Mock|DatabaseManager $resolver */ $resolver = $this->mock(DatabaseManager::class); $resolver->shouldReceive('connection') ->andReturn($this->connection); - EloquentModelCastingTest::setConnectionResolver($resolver); + BaseEloquentModelCasting::setConnectionResolver($resolver); $this->builder ->where(function (Builder $query) { @@ -125,27 +140,28 @@ public function testWhereClosure() $sql = $this->builder->toSql(); - $this->assertSame('SELECT * FROM `test_table` WHERE (`id` < 10 OR `id` = 15) AND `status` = 100', $sql); + self::assertSame('SELECT * FROM `test_table` WHERE (`id` < 10 OR `id` = 15) AND `status` = 100', $sql); } - public function testOrWhere() + public function testOrWhere(): void { - $id = $this->faker()->numberBetween(1); - $date = $this->faker()->date(); + $id = $this->faker() + ->numberBetween(1); + $date = $this->faker() + ->date(); $this->builder->where('id', $id); $this->builder->orWhere('date_column', '>', $date); $sql = $this->builder->toSql(); - $this->assertSame( - 'SELECT * FROM `test_table` WHERE `id` = '.$id.' OR `date_column` > \''.$date.'\'', - $sql - ); + self::assertSame('SELECT * FROM `test_table` WHERE `id` = '.$id.' OR `date_column` > \''.$date.'\'', $sql); } - public function testFind() + public function testFind(): void { - $id = $this->faker()->numberBetween(1); - $stringAttribute = $this->faker()->word; + $id = $this->faker() + ->numberBetween(1); + $stringAttribute = $this->faker() + ->word(); $this->connection ->shouldReceive('getName') @@ -154,19 +170,23 @@ public function testFind() $this->connection ->shouldReceive('select') ->andReturn([ - ['id' => $id, 'stringAttribute' => $stringAttribute], + [ + 'id' => $id, + 'stringAttribute' => $stringAttribute, + ], ]); $model = $this->builder->find($id); - $this->assertInstanceOf(EloquentModelCastingTest::class, $model); - $this->assertSame($id, $model->id); - $this->assertSame($stringAttribute, $model->stringAttribute); + self::assertInstanceOf(BaseEloquentModelCasting::class, $model); + self::assertSame($id, $model->id); + self::assertSame($stringAttribute, $model->stringAttribute); } - public function testFindMany() + public function testFindMany(): void { - $ids = collect()->times(5); + $ids = collect() + ->times(5); $this->connection ->shouldReceive('getName') @@ -174,21 +194,19 @@ public function testFindMany() $this->connection ->shouldReceive('select') - ->andReturn( - $ids - ->map(function ($id) { - return ['id' => $id]; - }) - ->toArray() - ); + ->andReturn($ids->map(function ($id) { + return [ + 'id' => $id, + ]; + })->toArray()); $models = $this->builder->findMany($ids->toArray()); - $this->assertInstanceOf(Collection::class, $models); - $this->assertCount($ids->count(), $models); + self::assertInstanceOf(Collection::class, $models); + self::assertCount($ids->count(), $models); } - public function testFindOrFail() + public function testFindOrFail(): void { $this->expectException(ModelNotFoundException::class); @@ -203,22 +221,31 @@ public function testFindOrFail() $this->builder->findOrFail($this->faker()->numberBetween()); } - public function testGet() + public function testGet(): void { $connectionResultRow = [ - 'id' => $this->faker()->randomDigit, - 'intAttribute' => (string) $this->faker()->randomDigit, - 'floatAttribute' => (string) $this->faker()->randomFloat(2), - 'stringAttribute' => $this->faker()->randomDigit, + 'id' => $this->faker() + ->randomDigit(), + 'intAttribute' => (string) $this->faker() + ->randomDigit(), + 'floatAttribute' => (string) $this->faker() + ->randomFloat(2), + 'stringAttribute' => $this->faker() + ->randomDigit(), 'boolAttribute' => 1, 'booleanAttribute' => 1, 'objectAttribute' => json_encode([ - $this->faker()->word => $this->faker()->randomLetter, + $this->faker() + ->word() => $this->faker() + ->randomLetter(), ]), 'arrayAttribute' => json_encode(range(1, 5)), - 'dateAttribute' => now()->toDateTimeString(), - 'datetimeAttribute' => now()->toDateString(), - 'timestampAttribute' => now()->toDateString(), + 'dateAttribute' => now() + ->toDateTimeString(), + 'datetimeAttribute' => now() + ->toDateString(), + 'timestampAttribute' => now() + ->toDateString(), ]; $connectionResultRow['jsonAttribute'] = json_encode($connectionResultRow['arrayAttribute']); @@ -228,24 +255,22 @@ public function testGet() $this->connection ->shouldReceive('select') - ->andReturn([ - $connectionResultRow, - ]); + ->andReturn([$connectionResultRow]); $collection = $this->builder->get(); - $this->assertInstanceOf(Collection::class, $collection); - $this->assertCount(1, $collection); + self::assertInstanceOf(Collection::class, $collection); + self::assertCount(1, $collection); $retrievedModel = $collection[0]; - $this->assertSame($connectionResultRow['id'], $retrievedModel->id); - $this->assertSame((int) $connectionResultRow['intAttribute'], $retrievedModel->intAttribute); - $this->assertSame((float) $connectionResultRow['floatAttribute'], $retrievedModel->floatAttribute); - $this->assertSame((string) $connectionResultRow['stringAttribute'], $retrievedModel->stringAttribute); - $this->assertTrue($retrievedModel->boolAttribute); - $this->assertTrue($retrievedModel->booleanAttribute); - $this->assertEquals(json_decode($connectionResultRow['objectAttribute']), $retrievedModel->objectAttribute); - $this->assertSame(json_decode($connectionResultRow['arrayAttribute'], true), $retrievedModel->arrayAttribute); - $this->assertSame($connectionResultRow['arrayAttribute'], $retrievedModel->jsonAttribute); + self::assertSame($connectionResultRow['id'], $retrievedModel->id); + self::assertSame((int) $connectionResultRow['intAttribute'], $retrievedModel->intAttribute); + self::assertSame((float) $connectionResultRow['floatAttribute'], $retrievedModel->floatAttribute); + self::assertSame((string) $connectionResultRow['stringAttribute'], $retrievedModel->stringAttribute); + self::assertTrue($retrievedModel->boolAttribute); + self::assertTrue($retrievedModel->booleanAttribute); + self::assertEquals(json_decode($connectionResultRow['objectAttribute']), $retrievedModel->objectAttribute); + self::assertSame(json_decode($connectionResultRow['arrayAttribute'], true), $retrievedModel->arrayAttribute); + self::assertSame($connectionResultRow['arrayAttribute'], $retrievedModel->jsonAttribute); } } diff --git a/tests/Unit/Database/Eloquent/CollectionTest.php b/tests/Unit/Database/Eloquent/CollectionTest.php index 5af08b7..da034f8 100644 --- a/tests/Unit/Database/Eloquent/CollectionTest.php +++ b/tests/Unit/Database/Eloquent/CollectionTest.php @@ -2,16 +2,18 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests\Unit\Database\Eloquent; +namespace Bavix\LaravelClickHouse\Tests\Unit\Database\Eloquent; -use Mockery\Mock; +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Eloquent\Collection; +use Bavix\LaravelClickHouse\Database\Query\Builder; +use Bavix\LaravelClickHouse\Tests\BaseEloquentModelCasting; +use Bavix\LaravelClickHouse\Tests\Helpers; use Carbon\Carbon; -use PHPUnit\Framework\TestCase; use Illuminate\Database\DatabaseManager; -use Esazykin\LaravelClickHouse\Tests\Helpers; -use Esazykin\LaravelClickHouse\Database\Connection; -use Esazykin\LaravelClickHouse\Database\Eloquent\Collection; -use Esazykin\LaravelClickHouse\Tests\EloquentModelCastingTest; +use Mockery\Mock; +use Mockery\MockInterface; +use PHPUnit\Framework\TestCase; /** * @property Mock|Connection connection @@ -20,7 +22,14 @@ class CollectionTest extends TestCase { use Helpers; - protected function setUp() + /** + * @var MockInterface&Connection + */ + private MockInterface $connection; + + private Builder $builder; + + protected function setUp(): void { parent::setUp(); @@ -28,21 +37,23 @@ protected function setUp() $this->connection ->shouldReceive('getName') - ->andReturn((new EloquentModelCastingTest())->getConnectionName()); + ->andReturn((new BaseEloquentModelCasting())->getConnectionName()); /** @var Mock|DatabaseManager $resolver */ $resolver = $this->mock(DatabaseManager::class); $resolver->shouldReceive('connection') ->andReturn($this->connection); - EloquentModelCastingTest::setConnectionResolver($resolver); + BaseEloquentModelCasting::setConnectionResolver($resolver); } - public function testMapModelToModel() + public function testMapModelToModel(): void { $connectionResult = collect() ->times(5, function (int $id) { - return ['id' => $id]; + return [ + 'id' => $id, + ]; }); $this->connection @@ -51,28 +62,30 @@ public function testMapModelToModel() $now = now(); - $models = EloquentModelCastingTest::all() - ->map(function (EloquentModelCastingTest $model) use ($now) { + $models = BaseEloquentModelCasting::all() + ->map(function (BaseEloquentModelCasting $model) use ($now) { $model->datetimeAttribute = $now; return $model; }); - $this->assertInstanceOf(Collection::class, $models); - $this->assertCount($connectionResult->count(), $models); + self::assertInstanceOf(Collection::class, $models); + self::assertCount($connectionResult->count(), $models); - $models->each(function (EloquentModelCastingTest $model, int $key) use ($now) { - $this->assertSame($key + 1, $model->id); - $this->assertInstanceOf(Carbon::class, $model->datetimeAttribute); - $this->assertSame($now->toDateTimeString(), $model->datetimeAttribute->toDateTimeString()); + $models->each(function (BaseEloquentModelCasting $model, int $key) use ($now) { + self::assertSame($key + 1, $model->id); + self::assertInstanceOf(Carbon::class, $model->datetimeAttribute); + self::assertSame($now->toDateTimeString(), $model->datetimeAttribute->toDateTimeString()); }); } - public function testMapModelToArray() + public function testMapModelToArray(): void { $connectionResult = collect() ->times(5, function (int $id) { - return ['id' => $id]; + return [ + 'id' => $id, + ]; }); $this->connection @@ -81,61 +94,56 @@ public function testMapModelToArray() $now = now(); - $collection = EloquentModelCastingTest::all() - ->map(function (EloquentModelCastingTest $model) use ($now) { + $collection = BaseEloquentModelCasting::all() + ->map(function (BaseEloquentModelCasting $model) use ($now) { return [ 'id' => $model->id, 'datetimeAttribute' => $now, ]; }); - $this->assertInstanceOf(\Illuminate\Support\Collection::class, $collection); - $this->assertCount($connectionResult->count(), $collection); + self::assertInstanceOf(\Illuminate\Support\Collection::class, $collection); + self::assertCount($connectionResult->count(), $collection); $collection->each(function (array $row, int $key) use ($now) { - $this->assertSame($key + 1, $row['id']); - $this->assertInstanceOf(Carbon::class, $row['datetimeAttribute']); - $this->assertSame($now->toDateTimeString(), $row['datetimeAttribute']->toDateTimeString()); + self::assertSame($key + 1, $row['id']); + self::assertInstanceOf(Carbon::class, $row['datetimeAttribute']); + self::assertSame($now->toDateTimeString(), $row['datetimeAttribute']->toDateTimeString()); }); } - /** - * @dataProvider findDataProvider - * @param $key - */ - public function testFind($key) + #[\PHPUnit\Framework\Attributes\DataProvider('findDataProvider')] + public function testFind($key): void { $connectionResult = collect() ->times(5, function (int $id) { - return ['id' => $id]; + return [ + 'id' => $id, + ]; }); $this->connection ->shouldReceive('select') ->andReturn($connectionResult->toArray()); - $found = EloquentModelCastingTest::all()->find($key); + $found = BaseEloquentModelCasting::all()->find($key); if (is_array($key)) { - $this->assertInstanceOf(Collection::class, $found); - $this->assertCount(count($key), $found); + self::assertInstanceOf(Collection::class, $found); + self::assertCount(count($key), $found); } else { - $this->assertInstanceOf(EloquentModelCastingTest::class, $found); + self::assertInstanceOf(BaseEloquentModelCasting::class, $found); } } - /** - * @dataProvider containsDataProvider - * @param bool $expected - * @param $key - * @param null $operator - * @param null $value - */ - public function testContains(bool $expected, $key, $operator = null, $value = null) + #[\PHPUnit\Framework\Attributes\DataProvider('containsDataProvider')] + public function testContains(bool $expected, $key, $operator = null, $value = null): void { $connectionResult = collect() ->times(5, function (int $id) { - return ['id' => $id]; + return [ + 'id' => $id, + ]; }); $this->connection @@ -143,21 +151,22 @@ public function testContains(bool $expected, $key, $operator = null, $value = nu ->andReturn($connectionResult->toArray()); if ($operator !== null && $value !== null) { - $contains = EloquentModelCastingTest::all()->contains($key, $operator, $value); + $contains = BaseEloquentModelCasting::all()->contains($key, $operator, $value); } else { - $contains = EloquentModelCastingTest::all()->contains($key); + $contains = BaseEloquentModelCasting::all()->contains($key); } - $this->assertSame($expected, $contains); + self::assertSame($expected, $contains); } - public function testGet() + public function testGet(): void { $connectionResult = collect() ->times(5, function (int $id) { return [ 'id' => $id, - 'floatAttribute' => (string) $this->faker()->randomFloat(2), + 'floatAttribute' => (string) $this->faker() + ->randomFloat(2), ]; }); @@ -165,15 +174,15 @@ public function testGet() ->shouldReceive('select') ->andReturn($connectionResult->toArray()); - $models = EloquentModelCastingTest::all(); + $models = BaseEloquentModelCasting::all(); - $this->assertInstanceOf(Collection::class, $models); - $this->assertCount($connectionResult->count(), $models); - $models = $models->map(function (EloquentModelCastingTest $model) { + self::assertInstanceOf(Collection::class, $models); + self::assertCount($connectionResult->count(), $models); + $models = $models->map(function (BaseEloquentModelCasting $model) { return $model->toArray(); }); - $this->assertSame( + self::assertSame( $connectionResult ->map(function (array $row) { $row['floatAttribute'] = (float) $row['floatAttribute']; @@ -185,26 +194,19 @@ public function testGet() ); } - public function findDataProvider(): array + public static function findDataProvider(): array { return [ [5], - [ - tap(new EloquentModelCastingTest, function (EloquentModelCastingTest $model) { - $model->id = 5; - }), - ], + [tap(new BaseEloquentModelCasting(), function (BaseEloquentModelCasting $model) { + $model->id = 5; + }), ], [1, 5], ]; } - public function containsDataProvider() + public static function containsDataProvider() { - return [ - [true, 5], - [false, 6], - [true, 'id', '>=', 5], - [false, 'id', '>=', 6], - ]; + return [[true, 5], [false, 6], [true, 'id', '>=', 5], [false, 'id', '>=', 6]]; } } diff --git a/tests/Unit/Database/Eloquent/FirstTableTest.php b/tests/Unit/Database/Eloquent/FirstTableTest.php new file mode 100644 index 0000000..ba220f7 --- /dev/null +++ b/tests/Unit/Database/Eloquent/FirstTableTest.php @@ -0,0 +1,48 @@ +connection = $this->getConnection('bavix::clickhouse'); + } + + public function testPaginate(): void + { + $this->connection->statement('DROP TABLE IF EXISTS my_first_table'); + + $result = $this->connection->statement('CREATE TABLE my_first_table +( + user_id UInt32, + message String, + timestamp DateTime, + metric Float32 +) +ENGINE = MergeTree() +PRIMARY KEY (user_id, timestamp)'); + self::assertTrue($result); + + self::assertTrue(FirstTableEntry::query()->insert([ + 'user_id' => 1, + 'message' => 'hello world', + 'timestamp' => new \DateTime(), + 'metric' => 42, + ])); + + self::assertCount(1, FirstTableEntry::query()->paginate()->items()); + } +} diff --git a/tests/Unit/Database/Eloquent/ModelTest.php b/tests/Unit/Database/Eloquent/ModelTest.php index bade4e8..c9160be 100644 --- a/tests/Unit/Database/Eloquent/ModelTest.php +++ b/tests/Unit/Database/Eloquent/ModelTest.php @@ -2,45 +2,49 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests\Unit\Database\Eloquent; +namespace Bavix\LaravelClickHouse\Tests\Unit\Database\Eloquent; +use Bavix\LaravelClickHouse\Tests\BaseEloquentModel; +use Bavix\LaravelClickHouse\Tests\BaseEloquentModelCasting; +use Bavix\LaravelClickHouse\Tests\BaseEloquentModelWith; +use Bavix\LaravelClickHouse\Tests\Helpers; +use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Support\Carbon; use PHPUnit\Framework\TestCase; -use Esazykin\LaravelClickHouse\Tests\Helpers; -use Esazykin\LaravelClickHouse\Tests\EloquentModelTest; -use Illuminate\Database\Eloquent\MassAssignmentException; -use Esazykin\LaravelClickHouse\Tests\EloquentModelWithTest; -use Esazykin\LaravelClickHouse\Tests\EloquentModelCastingTest; class ModelTest extends TestCase { use Helpers; - public function testAttributeManipulation() + public function testAttributeManipulation(): void { - $model = new EloquentModelTest(); + $model = new BaseEloquentModel(); $model->status = 'successful'; - $this->assertEquals('successful', $model->status); - $this->assertTrue(isset($model->status)); + self::assertEquals('successful', $model->status); + self::assertTrue(isset($model->status)); unset($model->status); - $this->assertFalse(isset($model->status)); + self::assertFalse(isset($model->status)); // test mutation $model->list_items = range(1, 5); - $this->assertEquals(range(1, 5), $model->list_items); + self::assertEquals(range(1, 5), $model->list_items); $attributes = $model->getAttributes(); - $this->assertEquals(json_encode(range(1, 5)), $attributes['list_items']); + self::assertEquals(json_encode(range(1, 5)), $attributes['list_items']); } - public function testDirtyAttributes() + public function testDirtyAttributes(): void { $this->expectException(MassAssignmentException::class); - new EloquentModelTest(['foo' => '1', 'bar' => 2, 'baz' => 3]); + new BaseEloquentModel([ + 'foo' => '1', + 'bar' => 2, + 'baz' => 3, + ]); } - public function testDirtyOnCastOrDateAttributes() + public function testDirtyOnCastOrDateAttributes(): void { - $model = new EloquentModelCastingTest(); + $model = new BaseEloquentModelCasting(); $model->setDateFormat('Y-m-d H:i:s'); $model->boolAttribute = 1; $model->foo = 1; @@ -53,40 +57,40 @@ public function testDirtyOnCastOrDateAttributes() $model->bar = '2017-03-18 00:00:00'; $model->dateAttribute = '2017-03-18 00:00:00'; $model->datetimeAttribute = null; - $this->assertTrue($model->isDirty()); - $this->assertTrue($model->isDirty('foo')); - $this->assertTrue($model->isDirty('bar')); - $this->assertFalse($model->isDirty('boolAttribute')); - $this->assertFalse($model->isDirty('dateAttribute')); - $this->assertTrue($model->isDirty('datetimeAttribute')); + self::assertTrue($model->isDirty()); + self::assertTrue($model->isDirty('foo')); + self::assertTrue($model->isDirty('bar')); + self::assertFalse($model->isDirty('boolAttribute')); + self::assertFalse($model->isDirty('dateAttribute')); + self::assertTrue($model->isDirty('datetimeAttribute')); } - public function testCalculatedAttributes() + public function testCalculatedAttributes(): void { - $model = new EloquentModelTest(); + $model = new BaseEloquentModel(); $model->password = 'secret'; $attributes = $model->getAttributes(); // ensure password attribute was not set to null - $this->assertArrayNotHasKey('password', $attributes); - $this->assertSame('******', $model->password); + self::assertArrayNotHasKey('password', $attributes); + self::assertSame('******', $model->password); $hash = 'e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4'; - $this->assertEquals($hash, $attributes['password_hash']); - $this->assertEquals($hash, $model->password_hash); + self::assertEquals($hash, $attributes['password_hash']); + self::assertEquals($hash, $model->password_hash); } - public function testWithMethodCallsQueryBuilderCorrectly() + public function testWithMethodCallsQueryBuilderCorrectly(): void { - $result = EloquentModelWithTest::with('foo', 'bar'); - $this->assertEquals('foo', $result); + $result = BaseEloquentModelWith::with('foo', 'bar'); + self::assertEquals('foo', $result); } - public function testTimestampsAreReturnedAsObjectsFromPlainDatesAndTimestamps() + public function testTimestampsAreReturnedAsObjectsFromPlainDatesAndTimestamps(): void { $datetime = '2012-12-04'; - $model = new EloquentModelCastingTest(); + $model = new BaseEloquentModelCasting(); $model->payed_at = $datetime; - $this->assertInstanceOf(Carbon::class, $model->payed_at); - $this->assertSame($datetime.' 00:00:00', $model->payed_at->toDateTimeString()); + self::assertInstanceOf(Carbon::class, $model->payed_at); + self::assertSame($datetime.' 00:00:00', $model->payed_at->toDateTimeString()); } } diff --git a/tests/Unit/Database/Query/BuilderTest.php b/tests/Unit/Database/Query/BuilderTest.php index cdc3ae7..7f8fd3e 100644 --- a/tests/Unit/Database/Query/BuilderTest.php +++ b/tests/Unit/Database/Query/BuilderTest.php @@ -2,38 +2,45 @@ declare(strict_types=1); -namespace Esazykin\LaravelClickHouse\Tests\Unit\Database\Query; +namespace Bavix\LaravelClickHouse\Tests\Unit\Database\Query; +use Bavix\LaravelClickHouse\Database\Connection; +use Bavix\LaravelClickHouse\Database\Query\Builder; +use Bavix\LaravelClickHouse\Tests\Helpers; +use Illuminate\Support\Collection; +use Mockery\MockInterface; use PHPUnit\Framework\TestCase; -use Esazykin\LaravelClickHouse\Tests\Helpers; -use Tinderbox\ClickhouseBuilder\Query\Grammar; -use Esazykin\LaravelClickHouse\Database\Connection; use Tinderbox\ClickhouseBuilder\Query\Enums\Format; -use Esazykin\LaravelClickHouse\Database\Query\Builder; +use Tinderbox\ClickhouseBuilder\Query\Grammar; /** - * @property \Mockery\MockInterface|Connection connection + * @property MockInterface|Connection connection * @property Builder builder */ class BuilderTest extends TestCase { use Helpers; - protected function setUp() + /** + * @var MockInterface&Connection + */ + private MockInterface $connection; + + private Builder $builder; + + protected function setUp(): void { parent::setUp(); $this->connection = $this->mock(Connection::class); - $this->builder = new Builder( - $this->connection, - new Grammar() - ); - $this->builder->from($this->faker()->word); + $this->builder = new Builder($this->connection, new Grammar()); + $this->builder->from($this->faker()->word()); } public function testGet(): void { - $connectionResult = $this->faker()->shuffle(range(1, 5)); + $connectionResult = $this->faker() + ->shuffle(range(1, 5)); $this->connection ->shouldReceive('select') @@ -41,8 +48,8 @@ public function testGet(): void $builderResult = $this->builder->get(); - $this->assertInstanceOf(\Illuminate\Support\Collection::class, $builderResult); - $this->assertSame($connectionResult, $builderResult->toArray()); + self::assertInstanceOf(Collection::class, $builderResult); + self::assertSame($connectionResult, $builderResult->toArray()); } public function testCount(): void @@ -51,16 +58,19 @@ public function testCount(): void $this->connection ->shouldReceive('select') - ->andReturn([['count' => count($connectionResult)]]); + ->andReturn([[ + 'count' => count($connectionResult), + ]]); $builderResult = $this->builder->count(); - $this->assertCount($builderResult, $connectionResult); + self::assertCount($builderResult, $connectionResult); } public function testFirst(): void { - $connectionResult = $this->faker()->shuffle(range(1, 5)); + $connectionResult = $this->faker() + ->shuffle(range(1, 5)); $this->connection ->shouldReceive('select') @@ -68,12 +78,12 @@ public function testFirst(): void $builderResult = $this->builder->first(); - $this->assertSame($connectionResult[0], $builderResult); + self::assertSame($connectionResult[0], $builderResult); } public function testNewQuery(): void { - $this->assertInstanceOf(Builder::class, $this->builder->newQuery()); + self::assertInstanceOf(Builder::class, $this->builder->newQuery()); } public function testInsertFiles(): void @@ -83,42 +93,58 @@ public function testInsertFiles(): void ->andReturn([]); $builderResult = $this->builder->insertFiles(['column_1', 'column_2'], []); - $this->assertSame([], $builderResult); + self::assertSame([], $builderResult); } public function testInsert(): void { - $this->assertFalse($this->builder->insert([])); + self::assertFalse($this->builder->insert([])); $insertedRow = [ - $this->faker()->word => $this->faker()->randomDigit, - $this->faker()->randomLetter => $this->faker()->randomDigit, - $this->faker()->numerify('column_#') => $this->faker()->randomLetter, + $this->faker() + ->word() => $this->faker() + ->randomDigit(), + $this->faker() + ->randomLetter() => $this->faker() + ->randomDigit(), + $this->faker() + ->numerify('column_#') => $this->faker() + ->randomLetter(), ]; - $inserted = [$insertedRow]; + \ksort($insertedRow); + $inserted = [$insertedRow]; $generatedSql = sprintf( - 'INSERT INTO `%s` (%s) FORMAT %s (?, ?, ?)', - $this->builder->getFrom()->getTable(), + 'INSERT INTO `%s` (%s) FORMAT %s (%s)', + $this->builder->getFrom() + ->getTable(), collect($insertedRow) ->keys() - ->sort() ->map(function (string $columnName) { return sprintf('`%s`', $columnName); }) ->implode(', '), - Format::VALUES + Format::VALUES, + collect($insertedRow) + ->values() + ->map(function ($value) { + if (is_numeric($value)) { + return $value; + } + + return sprintf('\'%s\'', $value); + }) + ->implode(', ') ); - ksort($insertedRow); + $values = collect($insertedRow) + ->values() + ->toArray(); $this->connection ->shouldReceive('insert') - ->withArgs([ - $generatedSql, - collect($insertedRow)->values()->toArray(), - ]) + ->withArgs([$generatedSql, $values]) ->andReturn(true); - $this->assertTrue($this->builder->insert($inserted)); + self::assertTrue($this->builder->insert($inserted)); } }