From 30a72d0fb233fbf32024f8ae98df123bf8d52a6b Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 28 Apr 2022 14:02:03 +0000 Subject: [PATCH 1/5] Apply CodeIgniter DevKit --- .github/workflows/analyze.yml | 81 ------------- .github/workflows/deptrac.yml | 73 ++++++++++++ .github/workflows/infection.yml | 72 ++++++++++++ .github/workflows/inspect.yml | 83 -------------- .github/workflows/phpcpd.yml | 36 ++++++ .github/workflows/phpcsfixer.yml | 60 ++++++++++ .github/workflows/phpstan.yml | 75 ++++++++++++ .github/workflows/{test.yml => phpunit.yml} | 60 +++++----- .github/workflows/rector.yml | 67 +++++++++++ .github/workflows/unused.yml | 59 ++++++++++ .php-cs-fixer.dist.php | 24 ++-- SECURITY.md | 26 +++++ composer-unused.php | 17 +++ composer.json | 29 +++-- depfile.yaml | 9 +- infection.json.dist | 4 +- phpstan.neon.dist | 14 +-- phpunit.xml.dist | 16 +-- rector.php | 119 ++++++++++++++++++++ 19 files changed, 684 insertions(+), 240 deletions(-) delete mode 100644 .github/workflows/analyze.yml create mode 100644 .github/workflows/deptrac.yml create mode 100644 .github/workflows/infection.yml delete mode 100644 .github/workflows/inspect.yml create mode 100644 .github/workflows/phpcpd.yml create mode 100644 .github/workflows/phpcsfixer.yml create mode 100644 .github/workflows/phpstan.yml rename .github/workflows/{test.yml => phpunit.yml} (53%) create mode 100644 .github/workflows/rector.yml create mode 100644 .github/workflows/unused.yml create mode 100644 SECURITY.md create mode 100644 composer-unused.php create mode 100644 rector.php diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml deleted file mode 100644 index 12c48c1..0000000 --- a/.github/workflows/analyze.yml +++ /dev/null @@ -1,81 +0,0 @@ -# When a PR is opened or a push is made, perform -# a static analysis check on the code using PHPStan. -name: PHPStan - -on: - pull_request: - branches: - - 'develop' - paths: - - 'src/**' - - 'tests/**' - - 'composer.**' - - 'phpstan*' - - '.github/workflows/analyze.yml' - push: - branches: - - 'develop' - paths: - - 'src/**' - - 'tests/**' - - 'composer.**' - - 'phpstan*' - - '.github/workflows/analyze.yml' - -jobs: - build: - name: PHP ${{ matrix.php-versions }} Static Analysis - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php-versions: ['7.3', '7.4', '8.0'] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - tools: composer, pecl, phpunit - extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Create composer cache directory - run: mkdir -p ${{ steps.composer-cache.outputs.dir }} - - - name: Cache composer dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Create PHPStan cache directory - run: mkdir -p build/phpstan - - - name: Cache PHPStan results - uses: actions/cache@v2 - with: - path: build/phpstan - key: ${{ runner.os }}-phpstan-${{ github.sha }} - restore-keys: ${{ runner.os }}-phpstan- - - - name: Install dependencies (limited) - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - - - name: Install dependencies (authenticated) - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - env: - COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} - - - name: Run static analysis - run: vendor/bin/phpstan analyze diff --git a/.github/workflows/deptrac.yml b/.github/workflows/deptrac.yml new file mode 100644 index 0000000..441c74d --- /dev/null +++ b/.github/workflows/deptrac.yml @@ -0,0 +1,73 @@ +name: Deptrac + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'depfile.yaml' + - '.github/workflows/deptrac.yml' + push: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'depfile.yaml' + - '.github/workflows/deptrac.yml' + +jobs: + build: + name: Dependency Tracing + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: phive + extensions: intl, json, mbstring, xml + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Create Deptrac cache directory + run: mkdir -p build/ + + - name: Cache Deptrac results + uses: actions/cache@v3 + with: + path: build + key: ${{ runner.os }}-deptrac-${{ github.sha }} + restore-keys: ${{ runner.os }}-deptrac- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Trace dependencies + run: | + sudo phive --no-progress install --global --trust-gpg-keys B8F640134AB1782E,A98E898BB53EB748 qossmic/deptrac + deptrac analyze --cache-file=build/deptrac.cache diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml new file mode 100644 index 0000000..0d39d58 --- /dev/null +++ b/.github/workflows/infection.yml @@ -0,0 +1,72 @@ +name: Infection + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'phpunit*' + - '.github/workflows/infection.yml' + push: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'phpunit*' + - '.github/workflows/infection.yml' + +jobs: + main: + name: Mutation Testing + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: infection, phpunit + extensions: intl, json, mbstring, gd, xml, sqlite3 + coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Configure matchers + uses: mheap/phpunit-matcher-action@v1 + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Test with PHPUnit + run: vendor/bin/phpunit --teamcity + + - name: Mutate with Infection + run: | + git fetch --depth=1 origin $GITHUB_BASE_REF + infection --threads=2 --skip-initial-tests --coverage=build/phpunit --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-filter=AM --logger-github --ignore-msi-with-no-mutations diff --git a/.github/workflows/inspect.yml b/.github/workflows/inspect.yml deleted file mode 100644 index 1a8dfbd..0000000 --- a/.github/workflows/inspect.yml +++ /dev/null @@ -1,83 +0,0 @@ -# When a PR is opened or a push is made, perform an -# architectural inspection on the code using Deptrac. -name: Deptrac - -on: - pull_request: - branches: - - 'develop' - paths: - - 'src/**' - - 'tests/**' - - 'composer.**' - - 'depfile.yaml' - - '.github/workflows/inspect.yml' - push: - branches: - - 'develop' - paths: - - 'src/**' - - 'tests/**' - - 'composer.**' - - 'depfile.yaml' - - '.github/workflows/inspect.yml' - -jobs: - build: - name: PHP ${{ matrix.php-versions }} Architectural Inspection - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php-versions: ['7.4', '8.0'] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - tools: composer, pecl, phive, phpunit - extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Create composer cache directory - run: mkdir -p ${{ steps.composer-cache.outputs.dir }} - - - name: Cache composer dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Create Deptrac cache directory - run: mkdir -p build/ - - - name: Cache Deptrac results - uses: actions/cache@v2 - with: - path: build - key: ${{ runner.os }}-deptrac-${{ github.sha }} - restore-keys: ${{ runner.os }}-deptrac- - - - name: Install dependencies (limited) - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - - - name: Install dependencies (authenticated) - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - env: - COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} - - - name: Run architectural inspection - run: | - sudo phive --no-progress install --global qossmic/deptrac --trust-gpg-keys B8F640134AB1782E - deptrac analyze --cache-file=build/deptrac.cache diff --git a/.github/workflows/phpcpd.yml b/.github/workflows/phpcpd.yml new file mode 100644 index 0000000..d6a877d --- /dev/null +++ b/.github/workflows/phpcpd.yml @@ -0,0 +1,36 @@ +name: PHPCPD + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - '.github/workflows/phpcpd.yml' + push: + branches: + - develop + paths: + - '**.php' + - '.github/workflows/phpcpd.yml' + +jobs: + build: + name: Code Copy-Paste Detection + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: phpcpd + extensions: dom, mbstring + coverage: none + + - name: Detect duplicate code + run: phpcpd src/ tests/ diff --git a/.github/workflows/phpcsfixer.yml b/.github/workflows/phpcsfixer.yml new file mode 100644 index 0000000..06d6453 --- /dev/null +++ b/.github/workflows/phpcsfixer.yml @@ -0,0 +1,60 @@ +name: PHPCSFixer + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - '.github/workflows/phpcsfixer.yml' + push: + branches: + - develop + paths: + - '**.php' + - '.github/workflows/phpcsfixer.yml' + +jobs: + build: + name: PHP ${{ matrix.php-versions }} Coding Standards + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json, tokenizer + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Check code for standards compliance + run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --using-cache=no --diff diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..6bdd00f --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,75 @@ +name: PHPStan + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'phpstan*' + - '.github/workflows/phpstan.yml' + push: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'phpstan*' + - '.github/workflows/phpstan.yml' + +jobs: + build: + name: PHP ${{ matrix.php-versions }} Static Analysis + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: phpstan, phpunit + extensions: intl, json, mbstring, xml + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Create PHPStan cache directory + run: mkdir -p build/phpstan + + - name: Cache PHPStan results + uses: actions/cache@v3 + with: + path: build/phpstan + key: ${{ runner.os }}-phpstan-${{ github.sha }} + restore-keys: ${{ runner.os }}-phpstan- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Run static analysis + run: vendor/bin/phpstan analyze diff --git a/.github/workflows/test.yml b/.github/workflows/phpunit.yml similarity index 53% rename from .github/workflows/test.yml rename to .github/workflows/phpunit.yml index 561601d..c875e33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/phpunit.yml @@ -4,32 +4,39 @@ on: pull_request: branches: - develop + paths: + - '**.php' + - 'composer.*' + - 'phpunit*' + - '.github/workflows/phpunit.yml' push: branches: - develop + paths: + - '**.php' + - 'composer.*' + - 'phpunit*' + - '.github/workflows/phpunit.yml' jobs: main: name: PHP ${{ matrix.php-versions }} Unit Tests - - strategy: - matrix: - php-versions: ['7.3', '7.4', '8.0'] - runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" + strategy: + matrix: + php-versions: ['7.4', '8.0', '8.1'] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Setup PHP, with composer and extensions + - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - tools: composer, pecl, phpunit - extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 + tools: composer, phive, phpunit + extensions: intl, json, mbstring, gd, xdebug, xml, sqlite3 coverage: xdebug env: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -39,21 +46,19 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- - - name: Install dependencies (limited) - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - - - name: Install dependencies (authenticated) - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader - env: - COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi - name: Test with PHPUnit run: vendor/bin/phpunit --verbose --coverage-text @@ -61,16 +66,12 @@ jobs: TERM: xterm-256color TACHYCARDIA_MONITOR_GA: enabled - - if: matrix.php-versions == '8.0' - name: Mutate with Infection - run: | - composer global require infection/infection - git fetch --depth=1 origin $GITHUB_BASE_REF - infection --threads=2 --skip-initial-tests --coverage=build/phpunit --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-filter=AM --logger-github --ignore-msi-with-no-mutations - - if: matrix.php-versions == '8.0' name: Run Coveralls - run: vendor/bin/php-coveralls --verbose --coverage_clover=build/phpunit/clover.xml --json_path build/phpunit/coveralls-upload.json + continue-on-error: true + run: | + sudo phive --no-progress install --global --trust-gpg-keys E82B2FB314E9906E php-coveralls + php-coveralls --verbose --coverage_clover=build/phpunit/clover.xml --json_path build/phpunit/coveralls-upload.json env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_PARALLEL: true @@ -83,6 +84,7 @@ jobs: steps: - name: Upload Coveralls results uses: coverallsapp/github-action@master + continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml new file mode 100644 index 0000000..a945743 --- /dev/null +++ b/.github/workflows/rector.yml @@ -0,0 +1,67 @@ +name: Rector + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'rector.php' + - '.github/workflows/rector.yml' + push: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - 'rector.php' + - '.github/workflows/rector.yml' + +jobs: + build: + name: PHP ${{ matrix.php-versions }} Rector Analysis + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: phpstan + extensions: intl, json, mbstring, xml + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Analyze for refactoring + run: | + composer global require --dev rector/rector:^0.12.16 + rector process --dry-run --no-progress-bar diff --git a/.github/workflows/unused.yml b/.github/workflows/unused.yml new file mode 100644 index 0000000..b02fab6 --- /dev/null +++ b/.github/workflows/unused.yml @@ -0,0 +1,59 @@ +name: Unused + +on: + pull_request: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - '.github/workflows/unused.yml' + push: + branches: + - develop + paths: + - '**.php' + - 'composer.*' + - '.github/workflows/unused.yml' + +jobs: + build: + name: Unused Package Detection + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: composer, composer-unused + extensions: intl, json, mbstring, xml + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader + else + composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Detect unused packages + run: composer-unused -vvv --output-format=github --ansi --no-interaction --no-progress diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ca260f1..1185b9c 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,33 +1,23 @@ files() - ->in(__DIR__) + ->in([ + __DIR__ . '/src/', + __DIR__ . '/tests/', + ]) ->exclude('build') ->append([__FILE__]); -// Remove overrides for incremental changes -$overrides = [ - 'array_indentation' => false, - 'braces' => false, - 'indentation_type' => false, -]; +$overrides = []; $options = [ 'finder' => $finder, 'cacheFile' => 'build/.php-cs-fixer.cache', ]; -/* Reenable after incremental changes are applied -return Factory::create(new Standard(), $overrides, $options)->forLibrary( - 'Library', - 'Tatter Software', - '', - 2021 -); -*/ -return Factory::create(new Standard(), $overrides, $options)->forProjects(); +return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..459a5fb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.** + +## Reporting a Vulnerability + +Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged. + +**Please report security flaws by emailing the development team directly: support@tattersoftware.com**. + +The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating +the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the +progress towards a fix and full announcement, and may ask for additional information or guidance. + +## Disclosure Policy + +When the security team receives a security bug report, they will assign it to a primary handler. +This person will coordinate the fix and release process, involving the following steps: + +- Confirm the problem and determine the affected versions. +- Audit code to find any potential similar problems. +- Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a Pull Request. diff --git a/composer-unused.php b/composer-unused.php new file mode 100644 index 0000000..f46e293 --- /dev/null +++ b/composer-unused.php @@ -0,0 +1,17 @@ +addNamedFilter(NamedFilter::fromString('symfony/config')) + // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')) + ->setAdditionalFilesFor('codeigniter4/framework', [ + ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), + ]); +}; diff --git a/composer.json b/composer.json index 1bf2b5b..7a614ac 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "codeigniter4/authentication-implementation": "1.0", "components/jquery": "^3.3", "league/commonmark": "^1.6 || ^2.0", @@ -28,9 +28,15 @@ "twbs/bootstrap": "^4.3" }, "require-dev": { - "codeigniter4/codeigniter4": "dev-develop", + "codeigniter4/framework": "^4.1", "myth/auth": "dev-develop", - "tatter/tools": "^1.10" + "tatter/tools": "^2.0" + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + } }, "autoload": { "psr-4": { @@ -46,10 +52,6 @@ } }, "repositories": [ - { - "type": "vcs", - "url": "https://github.com/codeigniter4/CodeIgniter4" - }, { "type": "vcs", "url": "https://github.com/lonnieezell/myth-auth" @@ -59,9 +61,20 @@ "prefer-stable": true, "scripts": { "analyze": "phpstan analyze", + "ci": [ + "Composer\\Config::disableProcessTimeout", + "@deduplicate", + "@analyze", + "@test", + "@inspect", + "rector process", + "@style" + ], + "deduplicate": "phpcpd app/ src/", "inspect": "deptrac analyze --cache-file=build/deptrac.cache", "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit", - "style": "php-cs-fixer fix --verbose --ansi", + "retool": "retool", + "style": "php-cs-fixer fix --verbose --ansi --using-cache=no", "test": "phpunit" } } diff --git a/depfile.yaml b/depfile.yaml index 79ec82a..e5ed3df 100644 --- a/depfile.yaml +++ b/depfile.yaml @@ -1,7 +1,7 @@ paths: - - ./src - - ./vendor/codeigniter4/codeigniter4/system - - ./vendor/tatter + - ./src/ + - ./vendor/codeigniter4/framework/system/ + - ./vendor/tatter/ exclude_files: - '#.*test.*#i' layers: @@ -109,6 +109,7 @@ ruleset: - Entity - Service - Vendor Config + - Vendor Entity - Vendor Model Service: - Config @@ -116,6 +117,7 @@ ruleset: # Ignore anything in the Vendor layers Vendor Model: + - Config - Service - Vendor Config - Vendor Controller @@ -130,6 +132,7 @@ ruleset: - Vendor Model - Vendor View Vendor Config: + - Config - Service - Vendor Config - Vendor Controller diff --git a/infection.json.dist b/infection.json.dist index b175102..7badcc6 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -1,7 +1,7 @@ { "source": { "directories": [ - "src" + "src/" ], "excludes": [ "Config", @@ -15,5 +15,5 @@ "mutators": { "@default": true }, - "bootstrap": "vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php" + "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ef9beb6..dd731a1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,25 +2,21 @@ parameters: tmpDir: build/phpstan level: 5 paths: - - src - - tests + - src/ + - tests/ bootstrapFiles: - - vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php - excludes_analyse: + - vendor/codeigniter4/framework/system/Test/bootstrap.php + excludePaths: - src/Config/Routes.php - src/Views/* ignoreErrors: - '#Call to an undefined static method Config\\Services::[A-Za-z]+\(\)#' - - '#Call to an undefined method CodeIgniter\\Database\\BaseBuilder::first\(\)#' - - '#Cannot access property [\$a-z_]+ on (array|object)#' - - '#Cannot call method [\$A-Za-z_]+\(\) on (array|object)#' universalObjectCratesClasses: - CodeIgniter\Entity - CodeIgniter\Entity\Entity - Faker\Generator scanDirectories: - - vendor/codeigniter4/codeigniter4/system/Helpers - - vendor/myth/auth/src/Helpers + - vendor/codeigniter4/framework/system/Helpers dynamicConstantNames: - APP_NAMESPACE - CI_DEBUG diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a90a1f2..ac88cfc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - ./src + ./src/ + ./src/Config ./src/Views - ./src/Config/Routes.php @@ -37,7 +37,7 @@ - + ./tests @@ -74,16 +74,16 @@ - + - + - + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..777f08f --- /dev/null +++ b/rector.php @@ -0,0 +1,119 @@ +sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_74, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitSetList::PHPUNIT_80]); + $rectorConfig->parallel(); + // The paths to refactor (can also be supplied with CLI arguments) + $rectorConfig->paths([ + __DIR__ . '/src/', + __DIR__ . '/tests/', + ]); + + // Include Composer's autoload - required for global execution, remove if running locally + $rectorConfig->autoloadPaths([ + __DIR__ . '/vendor/autoload.php', + ]); + + // Do you need to include constants, class aliases, or a custom autoloader? + $rectorConfig->bootstrapFiles([ + realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', + ]); + + if (is_file(__DIR__ . '/phpstan.neon.dist')) { + $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); + } + + // Set the target version for refactoring + $rectorConfig->phpVersion(PhpVersion::PHP_74); + + // Auto-import fully qualified class names + $rectorConfig->importNames(); + + // Are there files or rules you need to skip? + $rectorConfig->skip([ + __DIR__ . '/src/Views', + + JsonThrowOnErrorRector::class, + StringifyStrNeedlesRector::class, + + // Note: requires php 8 + RemoveUnusedPromotedPropertyRector::class, + + // Ignore tests that might make calls without a result + RemoveEmptyMethodCallRector::class => [ + __DIR__ . '/tests', + ], + + // Ignore files that should not be namespaced + NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [ + __DIR__ . '/src/Helpers', + ], + + // May load view files directly when detecting classes + StringClassNameToClassConstantRector::class, + + // May be uninitialized on purpose + AddDefaultValueForUndefinedVariableRector::class, + ]); + $rectorConfig->rule(SimplifyUselessVariableRector::class); + $rectorConfig->rule(RemoveAlwaysElseRector::class); + $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); + $rectorConfig->rule(ForToForeachRector::class); + $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); + $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); + $rectorConfig->rule(SimplifyStrposLowerRector::class); + $rectorConfig->rule(CombineIfRector::class); + $rectorConfig->rule(SimplifyIfReturnBoolRector::class); + $rectorConfig->rule(InlineIfToExplicitIfRector::class); + $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); + $rectorConfig->rule(ShortenElseIfRector::class); + $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); + $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); + $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); + $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); + $rectorConfig->rule(AddPregQuoteDelimiterRector::class); + $rectorConfig->rule(SimplifyRegexPatternRector::class); + $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); + $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); + $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); + $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); + $rectorConfig + ->ruleWithConfiguration(TypedPropertyRector::class, [ + TypedPropertyRector::INLINE_PUBLIC => true, + ]); +}; From f2cf0cea0db32b4847694b61ac33c05d3829760a Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 28 Apr 2022 14:02:19 +0000 Subject: [PATCH 2/5] Apply updated coding standard --- src/Config/Routes.php | 5 +- src/Controllers/Messages.php | 107 ++++--- .../2020-02-04-111617_create_chat_tables.php | 122 ++++---- src/Entities/Conversation.php | 196 ++++++------- src/Entities/Message.php | 135 +++++---- src/Entities/Participant.php | 276 +++++++++--------- src/Helpers/chat_helper.php | 104 ++++--- src/Models/ConversationModel.php | 45 ++- src/Models/MessageModel.php | 54 ++-- src/Models/ParticipantModel.php | 18 +- src/Views/message.php | 4 +- tests/_support/Database/Seeds/MythSeeder.php | 147 +++++----- tests/_support/ModuleTestCase.php | 55 ++-- tests/entities/ConversationTest.php | 102 +++---- tests/entities/MessageTest.php | 83 +++--- tests/entities/ParticipantTest.php | 142 ++++----- tests/models/MessageModelTest.php | 159 +++++----- 17 files changed, 873 insertions(+), 881 deletions(-) diff --git a/src/Config/Routes.php b/src/Config/Routes.php index bb923eb..ba5ddde 100644 --- a/src/Config/Routes.php +++ b/src/Config/Routes.php @@ -1,7 +1,6 @@ group('chatapi', ['namespace' => '\Tatter\Chat\Controllers'], function($routes) -{ - $routes->resource('messages', ['websafe' => 1]); +$routes->group('chatapi', ['namespace' => '\Tatter\Chat\Controllers'], static function ($routes) { + $routes->resource('messages', ['websafe' => 1]); }); diff --git a/src/Controllers/Messages.php b/src/Controllers/Messages.php index 7314ab8..e161a78 100644 --- a/src/Controllers/Messages.php +++ b/src/Controllers/Messages.php @@ -1,69 +1,68 @@ -request->getPost('conversation')) { + log_message('error', 'Conversation ID missing for Messages::create()'); + + return null; + } + + // Get the conversation + $conversation = model(ConversationModel::class)->find($conversationId); + if ($conversation === null) { + log_message('error', 'Unable to locate conversation # ' . $conversationId); + + return null; + } + + // Verify authentication + if (! function_exists('user_id')) { + throw new RuntimeException('Authentication system failure'); + } + + // Get the current user + if (! $userId = user_id()) { + log_message('error', 'Unable to determine the current user'); - /** - * Takes AJAX input and adds message content - * to an existent conversation. Creates the - * current user as a new Participant if - * necessary. - * - * @return ResponseInterface|null - */ - public function create(): ?ResponseInterface - { - if (! $conversationId = $this->request->getPost('conversation')) - { - log_message('error', 'Conversation ID missing for Messages::create()'); - return null; - } + return null; + } - // Get the conversation - $conversation = model(ConversationModel::class)->find($conversationId); - if ($conversation === null) - { - log_message('error', 'Unable to locate conversation # ' . $conversationId); - return null; - } + // Get or create the participant + if (! $participant = $conversation->addUser($userId)) { + log_message('error', 'Could not add participant to conversation # ' . $conversation->id); - // Verify authentication - if (! function_exists('user_id')) - { - throw new RuntimeException('Authentication system failure'); - } + return null; + } - // Get the current user - if (! $userId = user_id()) - { - log_message('error', 'Unable to determine the current user'); - return null; - } + // Say it + if (! $messageId = $participant->say($this->request->getPost('content'))) { + log_message('error', 'Failed to add content to conversation # ' . $conversation->id); - // Get or create the participant - if (! $participant = $conversation->addUser($userId)) - { - log_message('error', 'Could not add participant to conversation # ' . $conversation->id); - return null; - } - - // Say it - if (! $messageId = $participant->say($this->request->getPost('content'))) - { - log_message('error', 'Failed to add content to conversation # ' . $conversation->id); - return null; - } + return null; + } - // Respond with the pre-formatted message to display - return $this->respondCreated(view('Tatter\Chat\Views\message', [ - 'message' => $this->model->find($messageId) - ]), 'message created'); - } + // Respond with the pre-formatted message to display + return $this->respondCreated(view('Tatter\Chat\Views\message', [ + 'message' => $this->model->find($messageId), + ]), 'message created'); + } } diff --git a/src/Database/Migrations/2020-02-04-111617_create_chat_tables.php b/src/Database/Migrations/2020-02-04-111617_create_chat_tables.php index d4b7493..5ad4dda 100644 --- a/src/Database/Migrations/2020-02-04-111617_create_chat_tables.php +++ b/src/Database/Migrations/2020-02-04-111617_create_chat_tables.php @@ -1,72 +1,74 @@ - ['type' => 'varchar', 'constraint' => 255], - 'uid' => ['type' => 'varchar', 'constraint' => 255], - 'private' => ['type' => 'boolean', 'default' => 0], - 'direct' => ['type' => 'boolean', 'default' => 0], - 'created_at' => ['type' => 'datetime', 'null' => true], - 'updated_at' => ['type' => 'datetime', 'null' => true], - 'deleted_at' => ['type' => 'datetime', 'null' => true], - ]; - - $this->forge->addField('id'); - $this->forge->addField($fields); + public function up() + { + // Conversations + $fields = [ + 'title' => ['type' => 'varchar', 'constraint' => 255], + 'uid' => ['type' => 'varchar', 'constraint' => 255], + 'private' => ['type' => 'boolean', 'default' => 0], + 'direct' => ['type' => 'boolean', 'default' => 0], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]; + + $this->forge->addField('id'); + $this->forge->addField($fields); + + $this->forge->addUniqueKey('uid'); + $this->forge->addKey(['deleted_at', 'id']); + $this->forge->addKey('created_at'); + + $this->forge->createTable('chat_conversations'); + + // Participants + $fields = [ + 'conversation_id' => ['type' => 'int', 'unsigned' => true], + 'user_id' => ['type' => 'int', 'unsigned' => true], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]; + + $this->forge->addField('id'); + $this->forge->addField($fields); + + $this->forge->addKey(['conversation_id', 'user_id']); + $this->forge->addKey(['user_id', 'conversation_id']); + $this->forge->addKey('updated_at'); - $this->forge->addUniqueKey('uid'); - $this->forge->addKey(['deleted_at', 'id']); - $this->forge->addKey('created_at'); - - $this->forge->createTable('chat_conversations'); + $this->forge->createTable('chat_participants'); - /* Participants */ - $fields = [ - 'conversation_id' => ['type' => 'int', 'unsigned' => true], - 'user_id' => ['type' => 'int', 'unsigned' => true], - 'created_at' => ['type' => 'datetime', 'null' => true], - 'updated_at' => ['type' => 'datetime', 'null' => true], - 'deleted_at' => ['type' => 'datetime', 'null' => true], - ]; - - $this->forge->addField('id'); - $this->forge->addField($fields); + // Messages + $fields = [ + 'conversation_id' => ['type' => 'int', 'unsigned' => true], + 'participant_id' => ['type' => 'int', 'unsigned' => true], + 'content' => ['type' => 'text'], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]; - $this->forge->addKey(['conversation_id', 'user_id']); - $this->forge->addKey(['user_id', 'conversation_id']); - $this->forge->addKey('updated_at'); - - $this->forge->createTable('chat_participants'); + $this->forge->addField('id'); + $this->forge->addField($fields); - /* Messages */ - $fields = [ - 'conversation_id' => ['type' => 'int', 'unsigned' => true], - 'participant_id' => ['type' => 'int', 'unsigned' => true], - 'content' => ['type' => 'text'], - 'created_at' => ['type' => 'datetime', 'null' => true], - 'updated_at' => ['type' => 'datetime', 'null' => true], - 'deleted_at' => ['type' => 'datetime', 'null' => true], - ]; - - $this->forge->addField('id'); - $this->forge->addField($fields); + $this->forge->addKey(['conversation_id', 'created_at']); + $this->forge->addKey(['created_at', 'conversation_id']); - $this->forge->addKey(['conversation_id', 'created_at']); - $this->forge->addKey(['created_at', 'conversation_id']); - - $this->forge->createTable('chat_messages'); - } + $this->forge->createTable('chat_messages'); + } - public function down() - { - $this->forge->dropTable('chat_conversations'); - $this->forge->dropTable('chat_participants'); - $this->forge->dropTable('chat_messages'); - } + public function down() + { + $this->forge->dropTable('chat_conversations'); + $this->forge->dropTable('chat_participants'); + $this->forge->dropTable('chat_messages'); + } } diff --git a/src/Entities/Conversation.php b/src/Entities/Conversation.php index 02eb6b5..571c001 100644 --- a/src/Entities/Conversation.php +++ b/src/Entities/Conversation.php @@ -1,108 +1,104 @@ - 'bool', - 'direct' => 'bool', - ]; - - /** - * Gets the participants for this conversation - * - * @return array of Participants - */ - public function getParticipants(): array - { - return model(ParticipantModel::class) - ->where('conversation_id', $this->attributes['id']) - ->orderBy('created_at', 'asc') - ->findAll() ?? []; - } - - /** - * Gets the messages for this conversation. - * Preloads the Participant for each message. - * - * @return Message[] - */ - public function getMessages(): array - { - // Get the builder from the message model - $builder = model(MessageModel::class)->builder(); - - $rows = $builder - ->select('chat_messages.*, chat_participants.user_id') - ->join('chat_participants', 'chat_messages.participant_id = chat_participants.id', 'left') - ->where('chat_messages.conversation_id', $this->attributes['id']) - ->orderBy('chat_messages.created_at', 'asc') - ->get()->getResultArray(); - - if (empty($rows)) - { - return []; - } - - // Create the Message and Participant entities from each row - $messages = []; - foreach ($rows as $row) - { - $participant = new Participant([ - 'id' => $row['participant_id'], - 'conversation_id' => $row['conversation_id'], - 'user_id' => $row['user_id'], - ]); - - unset($row['user_id']); - - $message = new Message($row); - $message->setParticipant($participant); - - $messages[] = $message; - } - - return $messages; - } - - /** - * Adds a user to this conversation. - * - * @return Participant|null - */ - public function addUser(int $userId): ?Participant - { - // Build the row - $row = [ - 'conversation_id' => $this->attributes['id'], - 'user_id' => $userId, - ]; - - // Check for an existing participant - if ($participant = model(ParticipantModel::class)->where($row)->first()) - { - // Bump the last active date and return the entity - return $participant->active(); - } - - // Create the new participant - if ($id = model(ParticipantModel::class)->insert($row)) - { - return model(ParticipantModel::class)->find($id); - } - - // Something went wrong - // @codeCoverageIgnoreStart - $error = "Unable to add user {$userId} to conversation: " . $this->attributes['id']; - log_message('error', $error); - throw new RuntimeException($error); - // @codeCoverageIgnoreEnd - } + protected $table = 'chat_conversations'; + protected $casts = [ + 'private' => 'bool', + 'direct' => 'bool', + ]; + + /** + * Gets the participants for this conversation + * + * @return array of Participants + */ + public function getParticipants(): array + { + return model(ParticipantModel::class) + ->where('conversation_id', $this->attributes['id']) + ->orderBy('created_at', 'asc') + ->findAll() ?? []; + } + + /** + * Gets the messages for this conversation. + * Preloads the Participant for each message. + * + * @return Message[] + */ + public function getMessages(): array + { + // Get the builder from the message model + $builder = model(MessageModel::class)->builder(); + + $rows = $builder + ->select('chat_messages.*, chat_participants.user_id') + ->join('chat_participants', 'chat_messages.participant_id = chat_participants.id', 'left') + ->where('chat_messages.conversation_id', $this->attributes['id']) + ->orderBy('chat_messages.created_at', 'asc') + ->get()->getResultArray(); + + if (empty($rows)) { + return []; + } + + // Create the Message and Participant entities from each row + $messages = []; + + foreach ($rows as $row) { + $participant = new Participant([ + 'id' => $row['participant_id'], + 'conversation_id' => $row['conversation_id'], + 'user_id' => $row['user_id'], + ]); + + unset($row['user_id']); + + $message = new Message($row); + $message->setParticipant($participant); + + $messages[] = $message; + } + + return $messages; + } + + /** + * Adds a user to this conversation. + */ + public function addUser(int $userId): ?Participant + { + // Build the row + $row = [ + 'conversation_id' => $this->attributes['id'], + 'user_id' => $userId, + ]; + + // Check for an existing participant + if ($participant = model(ParticipantModel::class)->where($row)->first()) { + // Bump the last active date and return the entity + return $participant->active(); + } + + // Create the new participant + if ($id = model(ParticipantModel::class)->insert($row)) { + return model(ParticipantModel::class)->find($id); + } + + // Something went wrong + // @codeCoverageIgnoreStart + $error = "Unable to add user {$userId} to conversation: " . $this->attributes['id']; + log_message('error', $error); + + throw new RuntimeException($error); + // @codeCoverageIgnoreEnd + } } diff --git a/src/Entities/Message.php b/src/Entities/Message.php index 85771a8..a619243 100644 --- a/src/Entities/Message.php +++ b/src/Entities/Message.php @@ -1,85 +1,80 @@ - 'int', - 'participant_id' => 'int', - ]; + protected $table = 'chat_messages'; + protected $casts = [ + 'conversation_id' => 'int', + 'participant_id' => 'int', + ]; + + /** + * Initial default values + */ + protected $attributes = [ + 'content' => '', + ]; + + /** + * Stored copy of the sending Participant. + * + * @var Participant|null + */ + private $participant; - /** - * Initial default values - */ - protected $attributes = [ - 'content' => '', - ]; + /** + * Returns the message content with optional formatting. + * + * @param mixed $format + */ + public function getContent($format = 'html'): string + { + switch ($format) { + case 'html': + return nl2br(strip_tags($this->attributes['content'])); - /** - * Stored copy of the sending Participant. - * - * @var Participant|null - */ - private $participant; + case 'json': + return json_encode($this->attributes['content']); - /** - * Returns the message content with optional formatting. - * - * @return string - */ - public function getContent($format = 'html'): string - { - switch ($format) - { - case 'html': - return nl2br(strip_tags($this->attributes['content'])); + case 'markdown': + return (new GithubFlavoredMarkdownConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]))->convertToHtml($this->attributes['content']); - case 'json': - return json_encode($this->attributes['content']); + case 'raw': + default: + return $this->attributes['content']; + } + } - case 'markdown': - return (new GithubFlavoredMarkdownConverter([ - 'html_input' => 'strip', - 'allow_unsafe_links' => false, - ]))->convertToHtml($this->attributes['content']); + /** + * Injects the Participant. Used to eager load + * batches of Messages. + */ + public function setParticipant(?Participant $participant = null): void + { + $this->participant = $participant; + } - case 'raw': - default: - return $this->attributes['content']; - } - } - - /** - * Injects the Participant. Used to eager load - * batches of Messages. - * - * @param Participant|null $participant - * - * @return void - */ - public function setParticipant(Participant $participant = null): void - { - $this->participant = $participant; - } - - /** - * Loads and returns the participant who sent this message. - * Ideally this is already injected by the Conversation. - * - * @return Participant - */ - protected function getParticipant(): ?Participant - { - if (is_null($this->participant)) - { - $this->participant = model(ParticipantModel::class)->find($this->attributes['participant_id']); - } + /** + * Loads and returns the participant who sent this message. + * Ideally this is already injected by the Conversation. + * + * @return Participant + */ + protected function getParticipant(): ?Participant + { + if (null === $this->participant) { + $this->participant = model(ParticipantModel::class)->find($this->attributes['participant_id']); + } - return $this->participant; - } + return $this->participant; + } } diff --git a/src/Entities/Participant.php b/src/Entities/Participant.php index bfd60b4..148da67 100644 --- a/src/Entities/Participant.php +++ b/src/Entities/Participant.php @@ -1,155 +1,145 @@ - 'int', - 'user_id' => 'int', - ]; - - /** - * Cached copy of the underlying User - * - * @var UserEntity|null - */ - private $user; - - //-------------------------------------------------------------------- - // Getters - //-------------------------------------------------------------------- - - /** - * Returns a display name from the underlying user account - * - * @return string - */ - public function getUsername(): string - { - if ($username = $this->getUser()->getUsername()) - { - return $username; - } - - return isset($this->attributes['id']) ? 'Chatter' . $this->attributes['id'] : 'Chatter'; - } - - /** - * Returns a full name from the underlying user account - * - * @return string - */ - public function getName(): string - { - return $this->getUser()->getName() ?? $this->getUsername(); - } - - /** - * Returns initials from the underlying user account - * - * @return string - */ - public function getInitials(): string - { - $names = explode(' ', $this->getName()); - $string = ''; - foreach ($names as $name) - { - $string .= $name[0]; - } - - return strtoupper($string); - } - - //-------------------------------------------------------------------- - // Activities - //-------------------------------------------------------------------- - - /** - * Updates this participant's last activity date - * - * @return $this - */ - public function active(): self - { - $this->attributes['updated_at'] = date('Y-m-d H:i:s'); - - model(ParticipantModel::class)->update($this->attributes['id'], [ - 'updated_at' => $this->attributes['updated_at'], - ]); - - return $this; - } - - /** - * Creates a new message in the conversation and updates the activity timestamp - * - * @param string $content The content for the new message - * - * @return int|null ID of the new message - */ - public function say(string $content): ?int - { - $data = [ - 'conversation_id' => $this->attributes['conversation_id'], - 'participant_id' => $this->attributes['id'], - 'content' => $content, - ]; - - if ($id = model(MessageModel::class)->insert($data)) - { - $this->active(); - - $data['id'] = $id; - Events::trigger('chat', $data); - } - - return $id; - } - - //-------------------------------------------------------------------- - // Utilities - //-------------------------------------------------------------------- - - /** - * Loads and returns the user account for this - * participant using the UserProvider service - * - * @return UserEntity - */ - private function getUser(): UserEntity - { - if ($this->user) - { - return $this->user; - } - - // Load the UserFactory from the provider - $users = Services::users(); - - // If this is a Model then ignore soft delete status - if (method_exists($users, 'withDeleted')) - { - $users->withDeleted(); - } - - // Get the User - if (! $this->user = $users->findById($this->attributes['user_id'])) - { - $error = 'Unable to locate User ID: ' . $this->attributes['user_id']; - log_message('error', $error); - throw new RuntimeException($error); - } - - return $this->user; - } + protected $table = 'chat_participants'; + protected $casts = [ + 'conversation_id' => 'int', + 'user_id' => 'int', + ]; + + /** + * Cached copy of the underlying User + * + * @var UserEntity|null + */ + private $user; + + //-------------------------------------------------------------------- + // Getters + //-------------------------------------------------------------------- + + /** + * Returns a display name from the underlying user account + */ + public function getUsername(): string + { + if ($username = $this->getUser()->getUsername()) { + return $username; + } + + return isset($this->attributes['id']) ? 'Chatter' . $this->attributes['id'] : 'Chatter'; + } + + /** + * Returns a full name from the underlying user account + */ + public function getName(): string + { + return $this->getUser()->getName() ?? $this->getUsername(); + } + + /** + * Returns initials from the underlying user account + */ + public function getInitials(): string + { + $names = explode(' ', $this->getName()); + $string = ''; + + foreach ($names as $name) { + $string .= $name[0]; + } + + return strtoupper($string); + } + + //-------------------------------------------------------------------- + // Activities + //-------------------------------------------------------------------- + + /** + * Updates this participant's last activity date + * + * @return $this + */ + public function active(): self + { + $this->attributes['updated_at'] = date('Y-m-d H:i:s'); + + model(ParticipantModel::class)->update($this->attributes['id'], [ + 'updated_at' => $this->attributes['updated_at'], + ]); + + return $this; + } + + /** + * Creates a new message in the conversation and updates the activity timestamp + * + * @param string $content The content for the new message + * + * @return int|null ID of the new message + */ + public function say(string $content): ?int + { + $data = [ + 'conversation_id' => $this->attributes['conversation_id'], + 'participant_id' => $this->attributes['id'], + 'content' => $content, + ]; + + if ($id = model(MessageModel::class)->insert($data)) { + $this->active(); + + $data['id'] = $id; + Events::trigger('chat', $data); + } + + return $id; + } + + //-------------------------------------------------------------------- + // Utilities + //-------------------------------------------------------------------- + + /** + * Loads and returns the user account for this + * participant using the UserProvider service + */ + private function getUser(): UserEntity + { + if ($this->user) { + return $this->user; + } + + // Load the UserFactory from the provider + $users = Services::users(); + + // If this is a Model then ignore soft delete status + if (method_exists($users, 'withDeleted')) { + $users->withDeleted(); + } + + // Get the User + if (! $this->user = $users->findById($this->attributes['user_id'])) { + $error = 'Unable to locate User ID: ' . $this->attributes['user_id']; + log_message('error', $error); + + throw new RuntimeException($error); + } + + return $this->user; + } } diff --git a/src/Helpers/chat_helper.php b/src/Helpers/chat_helper.php index 9b53bbf..14e809c 100644 --- a/src/Helpers/chat_helper.php +++ b/src/Helpers/chat_helper.php @@ -2,59 +2,53 @@ use Tatter\Chat\Models\ConversationModel; -if (! function_exists('chat')) -{ - /** - * Loads or launches a Conversation with the current user. - * - * @param string $uid UID of the conversation, or empty to generate a throw-away - * @param string $title Title to apply to the display - * - * @return string View of the chat UI, or failure message - */ - function chat($uid = null, $title = null) - { - // Verify authentication - if (! function_exists('user_id')) - { - throw new RuntimeException('Authentication system failure'); - } - - // Get the current user - if (! $userId = user_id()) - { - return '

You must be logged in to chat!

'; - } - - // If no UID was passed then generate one - if ($uid === null) - { - $uid = bin2hex(random_bytes(16)); - } - - // Check for an existing conversation - $conversations = new ConversationModel(); - if (! $conversation = $conversations->where('uid', $uid)->first()) - { - // Create a new conversation - $data = [ - 'uid' => $uid, - 'title' => $title ?? 'Chat', - ]; - - $id = $conversations->insert($data); - $conversation = $conversations->find($id); - } - - // If a title was specified then use it over the database version - if ($title) - { - $conversation->title = $title; - } - - // Add/update the user - $participant = $conversation->addUser($userId); - - return view('Tatter\Chat\Views\conversation', ['conversation' => $conversation]); - } +if (! function_exists('chat')) { + /** + * Loads or launches a Conversation with the current user. + * + * @param string $uid UID of the conversation, or empty to generate a throw-away + * @param string $title Title to apply to the display + * + * @return string View of the chat UI, or failure message + */ + function chat($uid = null, $title = null) + { + // Verify authentication + if (! function_exists('user_id')) { + throw new RuntimeException('Authentication system failure'); + } + + // Get the current user + if (! $userId = user_id()) { + return '

You must be logged in to chat!

'; + } + + // If no UID was passed then generate one + if ($uid === null) { + $uid = bin2hex(random_bytes(16)); + } + + // Check for an existing conversation + $conversations = new ConversationModel(); + if (! $conversation = $conversations->where('uid', $uid)->first()) { + // Create a new conversation + $data = [ + 'uid' => $uid, + 'title' => $title ?? 'Chat', + ]; + + $id = $conversations->insert($data); + $conversation = $conversations->find($id); + } + + // If a title was specified then use it over the database version + if ($title) { + $conversation->title = $title; + } + + // Add/update the user + $participant = $conversation->addUser($userId); + + return view('Tatter\Chat\Views\conversation', ['conversation' => $conversation]); + } } diff --git a/src/Models/ConversationModel.php b/src/Models/ConversationModel.php index 991310b..28e140a 100644 --- a/src/Models/ConversationModel.php +++ b/src/Models/ConversationModel.php @@ -1,4 +1,6 @@ - 'required']; + protected $table = 'chat_conversations'; + protected $primaryKey = 'id'; + protected $returnType = Conversation::class; + protected $useTimestamps = true; + protected $useSoftDeletes = true; + protected $skipValidation = false; + protected $allowedFields = ['title', 'uid', 'private', 'direct']; + protected $validationRules = ['uid' => 'required']; - /** - * Faked data for Fabricator. - * - * @param Generator $faker - * - * @return Conversation - */ - public function fake(Generator &$faker): Conversation - { - return new Conversation([ - 'title' => $faker->company, - 'uid' => implode('_', $faker->words), - ]); - } + /** + * Faked data for Fabricator. + */ + public function fake(Generator &$faker): Conversation + { + return new Conversation([ + 'title' => $faker->company, + 'uid' => implode('_', $faker->words), + ]); + } } diff --git a/src/Models/MessageModel.php b/src/Models/MessageModel.php index cfb3055..51b1543 100644 --- a/src/Models/MessageModel.php +++ b/src/Models/MessageModel.php @@ -1,34 +1,38 @@ -builder() - ->select('chat_messages.*, chat_participants.updated_at') - ->join('chat_participants', - 'chat_messages.conversation_id = chat_participants.conversation_id AND user_id = ' . $userId) - ->where('chat_messages.created_at > chat_participants.updated_at') - ->get()->getCustomResultObject($this->returnType); + /** + * Returns all unread Messages for a user. + * + * @param int $userId ID of the user to match + * + * @return Message[] + */ + public function findUserUnread(int $userId): array + { + $result = $this->builder() + ->select('chat_messages.*, chat_participants.updated_at') + ->join( + 'chat_participants', + 'chat_messages.conversation_id = chat_participants.conversation_id AND user_id = ' . $userId + ) + ->where('chat_messages.created_at > chat_participants.updated_at') + ->get()->getCustomResultObject($this->returnType); - return $result ?? []; - } + return $result ?? []; + } } diff --git a/src/Models/ParticipantModel.php b/src/Models/ParticipantModel.php index ce66d89..1147a5e 100644 --- a/src/Models/ParticipantModel.php +++ b/src/Models/ParticipantModel.php @@ -1,15 +1,17 @@ -participant->user_id == user_id()): ?> + participant->user_id === user_id()): ?>
created_at->format('g:i A') ?>" >getContent('markdown') ?>
- + diff --git a/tests/_support/Database/Seeds/MythSeeder.php b/tests/_support/Database/Seeds/MythSeeder.php index 710f1e6..a023ae4 100644 --- a/tests/_support/Database/Seeds/MythSeeder.php +++ b/tests/_support/Database/Seeds/MythSeeder.php @@ -1,4 +1,6 @@ - 'yamira@noted.com', - 'username' => 'light', - 'password' => 'secretK33P3R', - ], - [ - 'email' => 'kazuto.kirigaya@castle.org', - 'username' => 'kirito', - 'password' => 'swordsX2', - ], - [ - 'email' => 'Mittelman@example.com', - 'username' => 'Saitama', - 'password' => '1punch', - ], + public function run() + { + // USERS + $users = [ + [ + 'email' => 'yamira@noted.com', + 'username' => 'light', + 'password' => 'secretK33P3R', + ], + [ + 'email' => 'kazuto.kirigaya@castle.org', + 'username' => 'kirito', + 'password' => 'swordsX2', + ], + [ + 'email' => 'Mittelman@example.com', + 'username' => 'Saitama', + 'password' => '1punch', + ], ]; - - $builder = $this->db->table('users'); - - foreach ($users as $user) - { - // Use the User entity to handle correct password hashing - $user = new User($user); - $builder->insert($user->toArray()); - } - - // GROUPS - $groups = [ - [ - 'name' => 'Administrators', - 'description' => 'Users with ultimate power', - ], - [ - 'name' => 'Blacklisted', - 'description' => 'Users sequestered for misconduct', - ], - [ - 'name' => 'Puny', - 'description' => 'Users who can do next to nothing', - ], + + $builder = $this->db->table('users'); + + foreach ($users as $user) { + // Use the User entity to handle correct password hashing + $user = new User($user); + $builder->insert($user->toArray()); + } + + // GROUPS + $groups = [ + [ + 'name' => 'Administrators', + 'description' => 'Users with ultimate power', + ], + [ + 'name' => 'Blacklisted', + 'description' => 'Users sequestered for misconduct', + ], + [ + 'name' => 'Puny', + 'description' => 'Users who can do next to nothing', + ], ]; - - $builder = $this->db->table('auth_groups'); - - foreach ($groups as $group) - { - $builder->insert($group); - } - - // GROUPS-USERS - $rows = [ - [ - 'group_id' => 1, - 'user_id' => 1, - ], - [ - 'group_id' => 2, - 'user_id' => 1, - ], - [ - 'group_id' => 3, - 'user_id' => 2, - ], + + $builder = $this->db->table('auth_groups'); + + foreach ($groups as $group) { + $builder->insert($group); + } + + // GROUPS-USERS + $rows = [ + [ + 'group_id' => 1, + 'user_id' => 1, + ], + [ + 'group_id' => 2, + 'user_id' => 1, + ], + [ + 'group_id' => 3, + 'user_id' => 2, + ], ]; - - $builder = $this->db->table('auth_groups_users'); - - foreach ($rows as $row) - { - $builder->insert($row); - } - } + + $builder = $this->db->table('auth_groups_users'); + + foreach ($rows as $row) { + $builder->insert($row); + } + } } diff --git a/tests/_support/ModuleTestCase.php b/tests/_support/ModuleTestCase.php index 154c902..8354465 100644 --- a/tests/_support/ModuleTestCase.php +++ b/tests/_support/ModuleTestCase.php @@ -1,34 +1,39 @@ -conversation = fake(ConversationModel::class); - } + $this->conversation = fake(ConversationModel::class); + } - public function testStartsWithoutParticipants() - { - $this->assertEquals([], $this->conversation->participants); - } + public function testStartsWithoutParticipants() + { + $this->assertSame([], $this->conversation->participants); + } - public function testStartsWithoutMessages() - { - $this->assertEquals([], $this->conversation->messages); - } + public function testStartsWithoutMessages() + { + $this->assertSame([], $this->conversation->messages); + } - public function testAddUserCreatesParticipant() - { - $user = model(UserModel::class)->first(); - $this->conversation->addUser($user->id); + public function testAddUserCreatesParticipant() + { + $user = model(UserModel::class)->first(); + $this->conversation->addUser($user->id); - $participants = $this->conversation->participants; + $participants = $this->conversation->participants; - $this->assertCount(1, $participants); - $this->assertEquals($user->username, $participants[0]->username); - } + $this->assertCount(1, $participants); + $this->assertSame($user->username, $participants[0]->username); + } - public function testSayAddsMessage() - { - $content = 'All your base'; - $user = model(UserModel::class)->first(); + public function testSayAddsMessage() + { + $content = 'All your base'; + $user = model(UserModel::class)->first(); - $participant = $this->conversation->addUser($user->id); - $participant->say($content); + $participant = $this->conversation->addUser($user->id); + $participant->say($content); - $messages = $this->conversation->messages; + $messages = $this->conversation->messages; - $this->assertEquals($content, $messages[0]->content); - } + $this->assertSame($content, $messages[0]->content); + } - public function testMessagesHaveParticipants() - { - $content = '...are belong to us'; - $user = model(UserModel::class)->first(); + public function testMessagesHaveParticipants() + { + $content = '...are belong to us'; + $user = model(UserModel::class)->first(); - $participant = $this->conversation->addUser($user->id); - $participant->say($content); + $participant = $this->conversation->addUser($user->id); + $participant->say($content); - $messages = $this->conversation->messages; + $messages = $this->conversation->messages; - $this->assertEquals($user->username, $messages[0]->participant->username); - } + $this->assertSame($user->username, $messages[0]->participant->username); + } } diff --git a/tests/entities/MessageTest.php b/tests/entities/MessageTest.php index 77a2ba2..22ade97 100644 --- a/tests/entities/MessageTest.php +++ b/tests/entities/MessageTest.php @@ -3,47 +3,50 @@ use Tatter\Chat\Entities\Message; use Tests\Support\ModuleTestCase; -class MessageTest extends ModuleTestCase +/** + * @internal + */ +final class MessageTest extends ModuleTestCase { - /** - * @dataProvider contentProvider - */ - public function testGetContent(string $format, string $content, string $expected) - { - $message = new Message(['content' => $content]); - $result = $message->getContent($format); + /** + * @dataProvider contentProvider + */ + public function testGetContent(string $format, string $content, string $expected) + { + $message = new Message(['content' => $content]); + $result = $message->getContent($format); - $this->assertSame($expected, $result); - } + $this->assertSame($expected, $result); + } - public function contentProvider() - { - return [ - 'html' => [ - 'html', - "hello\nworld", - "hello
\nworld", - ], - 'json' => [ - 'json', - 'hello world', - json_encode('hello world'), - ], - 'markdown' => [ - 'markdown', - '# hello world', - '

hello world

' . PHP_EOL, - ], - 'raw' => [ - 'raw', - 'hello world', - 'hello world', - ], - 'default' => [ - 'foo', - 'hello world', - 'hello world', - ], - ]; - } + public function contentProvider() + { + return [ + 'html' => [ + 'html', + "hello\nworld", + "hello
\nworld", + ], + 'json' => [ + 'json', + 'hello world', + json_encode('hello world'), + ], + 'markdown' => [ + 'markdown', + '# hello world', + '

hello world

' . PHP_EOL, + ], + 'raw' => [ + 'raw', + 'hello world', + 'hello world', + ], + 'default' => [ + 'foo', + 'hello world', + 'hello world', + ], + ]; + } } diff --git a/tests/entities/ParticipantTest.php b/tests/entities/ParticipantTest.php index 84203a1..fb48e56 100644 --- a/tests/entities/ParticipantTest.php +++ b/tests/entities/ParticipantTest.php @@ -2,7 +2,6 @@ use CodeIgniter\Events\Events; use Myth\Auth\Models\UserModel; -use Myth\Auth\Test\Fakers\UserFaker; use Tatter\Chat\Entities\Conversation; use Tatter\Chat\Entities\Participant; use Tatter\Chat\Models\ConversationModel; @@ -10,99 +9,102 @@ use Tatter\Chat\Models\ParticipantModel; use Tests\Support\ModuleTestCase; +/** + * @internal + */ final class ParticipantTest extends ModuleTestCase { - /** - * A generated Conversation - * - * @var Conversation - */ - private $conversation; + /** + * A generated Conversation + * + * @var Conversation + */ + private $conversation; - /** - * A generated Participant - * - * @var Participant - */ - private $participant; + /** + * A generated Participant + * + * @var Participant + */ + private $participant; - /** - * Create a mock Conversation with a Participant - */ - protected function setUp(): void - { - parent::setUp(); + /** + * Create a mock Conversation with a Participant + */ + protected function setUp(): void + { + parent::setUp(); - $this->conversation = fake(ConversationModel::class); + $this->conversation = fake(ConversationModel::class); - // Add a participant - $id = model(ParticipantModel::class)->insert([ - 'conversation_id' => $this->conversation->id, - 'user_id' => 1, - ]); + // Add a participant + $id = model(ParticipantModel::class)->insert([ + 'conversation_id' => $this->conversation->id, + 'user_id' => 1, + ]); - $this->participant = model(ParticipantModel::class)->find($id); - } + $this->participant = model(ParticipantModel::class)->find($id); + } - public function testGetUserReturnsDeleted() - { - $user = model(UserModel::class)->find(1); + public function testGetUserReturnsDeleted() + { + $user = model(UserModel::class)->find(1); - model(UserModel::class)->delete(1); + model(UserModel::class)->delete(1); - $this->assertEquals($user->username, $this->participant->username); - } + $this->assertSame($user->username, $this->participant->username); + } - public function testUsernameComesFromAccount() - { - $user = model(UserModel::class)->find(1); + public function testUsernameComesFromAccount() + { + $user = model(UserModel::class)->find(1); - $this->assertEquals($user->username, $this->participant->username); - } + $this->assertSame($user->username, $this->participant->username); + } - public function testGetNameFallsBackOnUsername() - { - $this->assertEquals('light', $this->participant->name); - } + public function testGetNameFallsBackOnUsername() + { + $this->assertSame('light', $this->participant->name); + } - public function testUsernameNoAccount() - { - model(UserModel::class)->skipValidation()->update(1, ['username' => null]); + public function testUsernameNoAccount() + { + model(UserModel::class)->skipValidation()->update(1, ['username' => null]); - $this->assertEquals('Chatter1', $this->participant->username); - } + $this->assertSame('Chatter1', $this->participant->username); + } - public function testUsernameNoAccountNoId() - { - model(UserModel::class)->skipValidation()->update(1, ['username' => null]); + public function testUsernameNoAccountNoId() + { + model(UserModel::class)->skipValidation()->update(1, ['username' => null]); - $this->participant->id = null; + $this->participant->id = null; - $this->assertEquals('Chatter', $this->participant->username); - } + $this->assertSame('Chatter', $this->participant->username); + } - public function testSayAddsMessage() - { - $content = 'All your base'; + public function testSayAddsMessage() + { + $content = 'All your base'; - $this->participant->say($content); + $this->participant->say($content); - $messages = model(MessageModel::class)->findAll(); + $messages = model(MessageModel::class)->findAll(); - $this->assertEquals($content, $messages[0]->content); - } + $this->assertSame($content, $messages[0]->content); + } - public function testSayTriggersEvent() - { - $test = null; + public function testSayTriggersEvent() + { + $test = null; - Events::on('chat', function ($data) use (&$test) { - $test = $data['id']; - }); + Events::on('chat', static function ($data) use (&$test) { + $test = $data['id']; + }); - $content = 'Are belong to us'; - $result = $this->participant->say($content); + $content = 'Are belong to us'; + $result = $this->participant->say($content); - $this->assertSame($result, $test); - } + $this->assertSame($result, $test); + } } diff --git a/tests/models/MessageModelTest.php b/tests/models/MessageModelTest.php index 8c0faba..6eac9e8 100644 --- a/tests/models/MessageModelTest.php +++ b/tests/models/MessageModelTest.php @@ -7,109 +7,112 @@ use Tatter\Chat\Models\MessageModel; use Tests\Support\ModuleTestCase; -class MessageModelTest extends ModuleTestCase +/** + * @internal + */ +final class MessageModelTest extends ModuleTestCase { - /** - * @var MessageModel - */ - private $model; + /** + * @var MessageModel + */ + private $model; - /** - * A generated Conversation - * - * @var Conversation - */ - private $conversation; + /** + * A generated Conversation + * + * @var Conversation + */ + private $conversation; - /** - * Set up the model and create a mock conversation - */ - public function setUp(): void - { - parent::setUp(); + /** + * Set up the model and create a mock conversation + */ + protected function setUp(): void + { + parent::setUp(); - $this->model = new MessageModel(); - $this->conversation = fake(ConversationModel::class); - } + $this->model = new MessageModel(); + $this->conversation = fake(ConversationModel::class); + } - public function testNoUnreadReturnsArray() - { - $result = $this->model->findUserUnread(1); + public function testNoUnreadReturnsArray() + { + $result = $this->model->findUserUnread(1); - $this->assertEquals([], $result); - } + $this->assertSame([], $result); + } - /** - * @timeLimit 1.5 - */ - public function testUnreadReturnsMessages() - { - $users = model(UserModel::class)->findAll(); + /** + * @timeLimit 1.5 + */ + public function testUnreadReturnsMessages() + { + $users = model(UserModel::class)->findAll(); - $participant1 = $this->conversation->addUser($users[0]->id); - $participant2 = $this->conversation->addUser($users[1]->id); + $participant1 = $this->conversation->addUser($users[0]->id); + $participant2 = $this->conversation->addUser($users[1]->id); - // Delay so the timestamps are different - sleep(1); + // Delay so the timestamps are different + sleep(1); - $participant1->say('All your base'); + $participant1->say('All your base'); - $result = $this->model->findUserUnread($participant2->user_id); + $result = $this->model->findUserUnread($participant2->user_id); - $this->assertInstanceOf(Message::class, $result[0]); - } + $this->assertInstanceOf(Message::class, $result[0]); + } - /** - * @timeLimit 1.5 - */ - public function testUnreadReturnsIgnoresUnjoined() - { - $users = model(UserModel::class)->findAll(); + /** + * @timeLimit 1.5 + */ + public function testUnreadReturnsIgnoresUnjoined() + { + $users = model(UserModel::class)->findAll(); - $participant1 = $this->conversation->addUser($users[0]->id); - $participant2 = $this->conversation->addUser($users[1]->id); + $participant1 = $this->conversation->addUser($users[0]->id); + $participant2 = $this->conversation->addUser($users[1]->id); - // Delay so the timestamps are different - sleep(1); + // Delay so the timestamps are different + sleep(1); - $participant1->say('...are belong to us'); + $participant1->say('...are belong to us'); - // Create another conversation - $conversation = fake(ConversationModel::class); + // Create another conversation + $conversation = fake(ConversationModel::class); - $participant1 = $conversation->addUser($users[0]->id); - $participant1->say('Somebody set us up the bomb!'); + $participant1 = $conversation->addUser($users[0]->id); + $participant1->say('Somebody set us up the bomb!'); - $result = $this->model->findUserUnread($participant2->user_id); + $result = $this->model->findUserUnread($participant2->user_id); - $this->assertCount(1, $result); - } + $this->assertCount(1, $result); + } - /** - * @timeLimit 2.5 - */ - public function testUnreadReturnsCorrectCount() - { - $users = (new UserModel())->findAll(); + /** + * @timeLimit 2.5 + */ + public function testUnreadReturnsCorrectCount() + { + $users = (new UserModel())->findAll(); - $participant1 = $this->conversation->addUser($users[0]->id); - $participant2 = $this->conversation->addUser($users[1]->id); + $participant1 = $this->conversation->addUser($users[0]->id); + $participant2 = $this->conversation->addUser($users[1]->id); - $participant1->say('All your base'); - $participant2->say('...are belong to us'); - sleep(1); - $participant1->say('Somebody set us up the bomb!'); + $participant1->say('All your base'); + $participant2->say('...are belong to us'); + sleep(1); + $participant1->say('Somebody set us up the bomb!'); - // Create another conversation - $conversation = fake(ConversationModel::class); + // Create another conversation + $conversation = fake(ConversationModel::class); - $participant1 = $conversation->addUser($users[0]->id); - $participant2 = $conversation->addUser($users[1]->id); - sleep(1); - $participant1->say('Somebody set us up the bomb!'); + $participant1 = $conversation->addUser($users[0]->id); + $participant2 = $conversation->addUser($users[1]->id); + sleep(1); + $participant1->say('Somebody set us up the bomb!'); - $result = $this->model->findUserUnread($participant2->user_id); + $result = $this->model->findUserUnread($participant2->user_id); - $this->assertCount(2, $result); - } + $this->assertCount(2, $result); + } } From 2d85b35b18fc5dfb4d4c8ce0aaa974725f02a108 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 28 Apr 2022 14:05:47 +0000 Subject: [PATCH 3/5] Apply Rector --- rector.php | 8 ++++---- src/Config/Routes.php | 2 ++ tests/_support/ModuleTestCase.php | 3 ++- tests/entities/ConversationTest.php | 4 +--- tests/entities/ParticipantTest.php | 8 ++------ tests/models/MessageModelTest.php | 9 ++------- 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/rector.php b/rector.php index 777f08f..44b9e38 100644 --- a/rector.php +++ b/rector.php @@ -112,8 +112,8 @@ $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); - $rectorConfig - ->ruleWithConfiguration(TypedPropertyRector::class, [ - TypedPropertyRector::INLINE_PUBLIC => true, - ]); + // $rectorConfig + // ->ruleWithConfiguration(TypedPropertyRector::class, [ + // TypedPropertyRector::INLINE_PUBLIC => true, + // ]); }; diff --git a/src/Config/Routes.php b/src/Config/Routes.php index ba5ddde..72182af 100644 --- a/src/Config/Routes.php +++ b/src/Config/Routes.php @@ -1,5 +1,7 @@ group('chatapi', ['namespace' => '\Tatter\Chat\Controllers'], static function ($routes) { $routes->resource('messages', ['websafe' => 1]); diff --git a/tests/_support/ModuleTestCase.php b/tests/_support/ModuleTestCase.php index 8354465..8151231 100644 --- a/tests/_support/ModuleTestCase.php +++ b/tests/_support/ModuleTestCase.php @@ -3,6 +3,7 @@ namespace Tests\Support; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; use Tests\Support\Database\Seeds\MythSeeder; /** @@ -10,7 +11,7 @@ */ final class ModuleTestCase extends CIUnitTestCase { - use \CodeIgniter\Test\DatabaseTestTrait; + use DatabaseTestTrait; /** * Should the db be refreshed before test? diff --git a/tests/entities/ConversationTest.php b/tests/entities/ConversationTest.php index 20a2bb8..42f273c 100644 --- a/tests/entities/ConversationTest.php +++ b/tests/entities/ConversationTest.php @@ -12,10 +12,8 @@ final class ConversationTest extends ModuleTestCase { /** * A generated Conversation - * - * @var Conversation */ - private $conversation; + private Conversation $conversation; /** * Create a mock conversation diff --git a/tests/entities/ParticipantTest.php b/tests/entities/ParticipantTest.php index fb48e56..c6b14db 100644 --- a/tests/entities/ParticipantTest.php +++ b/tests/entities/ParticipantTest.php @@ -16,17 +16,13 @@ final class ParticipantTest extends ModuleTestCase { /** * A generated Conversation - * - * @var Conversation */ - private $conversation; + private Conversation $conversation; /** * A generated Participant - * - * @var Participant */ - private $participant; + private Participant $participant; /** * Create a mock Conversation with a Participant diff --git a/tests/models/MessageModelTest.php b/tests/models/MessageModelTest.php index 6eac9e8..f799672 100644 --- a/tests/models/MessageModelTest.php +++ b/tests/models/MessageModelTest.php @@ -12,17 +12,12 @@ */ final class MessageModelTest extends ModuleTestCase { - /** - * @var MessageModel - */ - private $model; + private MessageModel $model; /** * A generated Conversation - * - * @var Conversation */ - private $conversation; + private Conversation $conversation; /** * Set up the model and create a mock conversation From be76de77fc2761a5721ac0b096d52e68cd12236f Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 28 Apr 2022 15:28:20 +0000 Subject: [PATCH 4/5] Fix static analysis --- composer.json | 2 +- phpstan.neon.dist | 3 +++ src/Entities/Conversation.php | 2 +- src/Entities/Message.php | 4 ++-- src/Entities/Participant.php | 2 +- tests/_support/ModuleTestCase.php | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 7a614ac..dfe904b 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "php": "^7.4 || ^8.0", "codeigniter4/authentication-implementation": "1.0", "components/jquery": "^3.3", - "league/commonmark": "^1.6 || ^2.0", + "league/commonmark": "^2.2", "tatter/assets": "^2.0", "tatter/users": "^1.0", "twbs/bootstrap": "^4.3" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index dd731a1..ccb744b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,7 +10,10 @@ parameters: - src/Config/Routes.php - src/Views/* ignoreErrors: + - '#Call to an undefined method CodeIgniter\\Database\\BaseBuilder::(first|findAll)\(\)#' - '#Call to an undefined static method Config\\Services::[A-Za-z]+\(\)#' + - '#Cannot access property [\$a-z_]+ on (array|object)#' + - '#Cannot call method [\$A-Za-z_]+\(\) on (array|object)#' universalObjectCratesClasses: - CodeIgniter\Entity - CodeIgniter\Entity\Entity diff --git a/src/Entities/Conversation.php b/src/Entities/Conversation.php index 571c001..0214c71 100644 --- a/src/Entities/Conversation.php +++ b/src/Entities/Conversation.php @@ -2,7 +2,7 @@ namespace Tatter\Chat\Entities; -use CodeIgniter\Entity; +use CodeIgniter\Entity\Entity; use RuntimeException; use Tatter\Chat\Models\MessageModel; use Tatter\Chat\Models\ParticipantModel; diff --git a/src/Entities/Message.php b/src/Entities/Message.php index a619243..7205fc2 100644 --- a/src/Entities/Message.php +++ b/src/Entities/Message.php @@ -2,7 +2,7 @@ namespace Tatter\Chat\Entities; -use CodeIgniter\Entity; +use CodeIgniter\Entity\Entity; use League\CommonMark\GithubFlavoredMarkdownConverter; use Tatter\Chat\Models\ParticipantModel; @@ -46,7 +46,7 @@ public function getContent($format = 'html'): string return (new GithubFlavoredMarkdownConverter([ 'html_input' => 'strip', 'allow_unsafe_links' => false, - ]))->convertToHtml($this->attributes['content']); + ]))->convert($this->attributes['content']); case 'raw': default: diff --git a/src/Entities/Participant.php b/src/Entities/Participant.php index 148da67..e5f0e84 100644 --- a/src/Entities/Participant.php +++ b/src/Entities/Participant.php @@ -2,7 +2,7 @@ namespace Tatter\Chat\Entities; -use CodeIgniter\Entity; +use CodeIgniter\Entity\Entity; use CodeIgniter\Events\Events; use Config\Services; use RuntimeException; diff --git a/tests/_support/ModuleTestCase.php b/tests/_support/ModuleTestCase.php index 8151231..2530f52 100644 --- a/tests/_support/ModuleTestCase.php +++ b/tests/_support/ModuleTestCase.php @@ -9,7 +9,7 @@ /** * @internal */ -final class ModuleTestCase extends CIUnitTestCase +abstract class ModuleTestCase extends CIUnitTestCase { use DatabaseTestTrait; From 03281e5980ebbf1f14eeb8002847c0950a5c09a8 Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 28 Apr 2022 15:33:27 +0000 Subject: [PATCH 5/5] Exempt assets packages --- composer-unused.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer-unused.php b/composer-unused.php index f46e293..4c8d929 100644 --- a/composer-unused.php +++ b/composer-unused.php @@ -9,8 +9,9 @@ return static function (Configuration $config): Configuration { return $config - // ->addNamedFilter(NamedFilter::fromString('symfony/config')) - // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')) + ->addNamedFilter(NamedFilter::fromString('components/jquery')) + ->addNamedFilter(NamedFilter::fromString('tatter/assets')) + ->addNamedFilter(NamedFilter::fromString('twbs/bootstrap')) ->setAdditionalFilesFor('codeigniter4/framework', [ ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), ]);