diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..03760bc --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,51 @@ +on: + push: + pull_request: + +name: PHPStan lint checks + +jobs: + static-analysis: + name: "PHPStan" + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + matrix: + php-version: + - "8.3" + dependencies: [ highest ] + operating-system: [ ubuntu-latest] + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: json, bcmath, curl, intl, mbstring + tools: composer:v2 + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress --no-suggest" + + - name: "PHPStan" + run: "make lint-phpstan" + + - name: "is PHPStan check succeeded" + if: ${{ success() }} + run: | + echo '✅ PHPStan check pass, congratulations!' + + - name: "is PHPStan check failed" + if: ${{ failure() }} + run: | + echo '::error:: ❗️ PHPStan check failed (╯°益°)╯彡┻━┻' \ No newline at end of file diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..32152d5 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,50 @@ +name: "PHPUnit tests" + +on: + push: + pull_request: + +env: + COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" + +jobs: + tests: + name: "PHPUnit tests" + + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + matrix: + php-version: + - "8.3" + dependencies: [ highest ] + operating-system: [ ubuntu-latest] + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: json, bcmath, curl, intl, mbstring + + - name: "Install dependencies" + run: | + composer update ${{ env.COMPOSER_FLAGS }} + + - name: "run unit tests" + run: "make test-unit" + + - name: "is unit tests tests succeeded" + if: ${{ success() }} + run: | + echo '✅ unit tests pass, congratulations!' + + - name: "is unit tests tests failed" + if: ${{ failure() }} + run: | + echo '::error:: ❗️ unit tests tests failed (╯°益°)╯彡┻━┻' \ No newline at end of file diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml new file mode 100644 index 0000000..4a0bf8a --- /dev/null +++ b/.github/workflows/rector.yml @@ -0,0 +1,51 @@ +on: + push: + pull_request: + +name: Rector lint checks + +jobs: + static-analysis: + name: "Rector" + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + matrix: + php-version: + - "8.3" + dependencies: [ highest ] + operating-system: [ ubuntu-latest] + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: json, bcmath, curl, intl, mbstring + tools: composer:v2 + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress --no-suggest" + + - name: "Rector" + run: "make lint-rector" + + - name: "is Rector check succeeded" + if: ${{ success() }} + run: | + echo '✅ Rector check pass, congratulations!' + + - name: "is PHPStan check failed" + if: ${{ failure() }} + run: | + echo '::error:: ❗️ Rector check failed (╯°益°)╯彡┻━┻' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66c3dc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.idea* +/vendor +/.cache +composer.phar +composer.lock +.phpunit.result.cache +*.log +.env.local \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a1f253a --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# This file is part of the bitrix24-app-core package. +# +# © Maksim Mesilov +# +# For the full copyright and license information, please view the MIT-LICENSE.txt +# file that was distributed with this source code. + +default: + @echo "make needs target:" + @egrep -e '^\S+' ./Makefile | grep -v default | sed -r 's/://' | sed -r 's/^/ - /' + +# linters +lint-phpstan: + vendor/bin/phpstan --memory-limit=1G analyse +lint-rector: + vendor/bin/rector process --dry-run +lint-rector-fix: + vendor/bin/rector process + +# unit tests +test-unit: + vendor/bin/phpunit --testsuite unit_tests --display-warnings + +test-integration: + vendor/bin/phpunit --testsuite integration_tests --display-warnings \ No newline at end of file diff --git a/README.md b/README.md index cfc6000..cbda81e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ # bitrix24-app-core + Bitrix24 application core + +## Область применения + +Библиотека предназначена для быстрой разработки приложений для Битркис24. Предоставляет слой хранения данных в СУБД +[PostgreSQL](https://www.postgresql.org/), использует [Doctrine ORM](https://www.doctrine-project.org/). + +Реализует [контракты](https://github.com/mesilov/bitrix24-php-sdk/tree/master/src/Application/Contracts) из +bitrix24-php-sdk. + +## Поддерживаемые контракты + +### Bitrix24Accounts + +Отвечает за +хранение [аккаунтов Битрикс24](https://github.com/mesilov/bitrix24-php-sdk/tree/master/src/Application/Contracts/Bitrix24Accounts) +с токенами доступа к порталу. + +### ApplicationInstallations + +Отвечает за +хранение [фактов установок](https://github.com/mesilov/bitrix24-php-sdk/tree/master/src/Application/Contracts/ApplicationInstallations) +приложения на конкретный портал Битркис24 + +### ContactPersons + +Отвечает за +хранение [контактных лиц](https://github.com/mesilov/bitrix24-php-sdk/tree/master/src/Application/Contracts/ContactPersons), +которые произвели установку приложения + +### Bitrix24Partners + +Отвечает за +хранение [партнёра](https://github.com/mesilov/bitrix24-php-sdk/tree/master/src/Application/Contracts/Bitrix24Partners +) Битрикс24, который произвёл установку или обслуживает портал + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..593339e --- /dev/null +++ b/composer.json @@ -0,0 +1,68 @@ +{ + "name": "mesilov/bitrix24-app-core", + "description": "A powerful PHP library for rapid Bitrix24 application development", + "keywords": [ + "Bitrix24", + "PHP", + "REST", + "API", + "Bitrix24 Application", + "SaaS" + ], + "type": "library", + "homepage": "https://github.com/mesilov/bitrix24-app-core", + "license": "MIT", + "authors": [ + { + "name": "Maksim Mesilov", + "homepage": "https://github.com/mesilov/" + } + ], + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "require": { + "php": "8.3.*", + "ext-json": "*", + "ext-curl": "*", + "ext-intl": "*", + "psr/log": "^3", + "fig/http-message-util": "^1", + "giggsey/libphonenumber-for-php": "^8", + "darsyn/ip": "^5", + "nesbot/carbon": "^3", + "moneyphp/money": "^4", + "mesilov/bitrix24-php-sdk": "^2", + "doctrine/orm": "^2", + "doctrine/doctrine-bundle": "^2", + "doctrine/doctrine-migrations-bundle": "^3", + "symfony/event-dispatcher": "^7", + "symfony/uid": "^7", + "knplabs/knp-paginator-bundle": "^6" + }, + "require-dev": { + "monolog/monolog": "^3", + "fakerphp/faker": "^1", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^11", + "psalm/phar": "^5", + "rector/rector": "^1", + "roave/security-advisories": "dev-master", + "symfony/debug-bundle": "^7", + "symfony/stopwatch": "^7" + }, + "autoload": { + "psr-4": { + "Bitrix24\\SDK\\ApplicationCore\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Bitrix24\\SDK\\ApplicationCore\\Tests\\": "tests", + "Bitrix24\\SDK\\Tests\\":"vendor/mesilov/bitrix24-php-sdk/tests" + } + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..909e05e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,14 @@ +parameters: + level: 5 + paths: + - src/ + - tests/ + bootstrapFiles: + - tests/bootstrap.php + parallel: + jobSize: 20 + maximumNumberOfProcesses: 8 + minimumNumberOfJobsPerProcess: 2 + editorUrlTitle: '%%relFile%%:%%line%%' + editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' + treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0f572c3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + + + + ./tests/Unit + + + ./tests/Integration + + + + + ./src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..3ce8e42 --- /dev/null +++ b/rector.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use Rector\Config\RectorConfig; +use Rector\Naming\Rector\Class_\RenamePropertyToMatchTypeRector; +use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\Set\ValueObject\DowngradeLevelSetList; + +return RectorConfig::configure() + ->withPaths([ + __DIR__ . '/src/', + __DIR__ . '/tests/', + ]) + ->withCache(cacheDirectory: __DIR__ . '.cache/rector') + ->withSets( + [ + PHPUnitSetList::PHPUNIT_110 + ] + ) + ->withImportNames( + importNames: false, + importDocBlockNames: false, + importShortClasses: false, + removeUnusedImports: false, + ) + ->withPhpSets( + php83: true + ) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + privatization: true, + naming: true, + instanceOf: true, + earlyReturn: true, + strictBooleans: true + ) + ->withSkip([ + RenamePropertyToMatchTypeRector::class + ]); \ No newline at end of file diff --git a/src/Bitrix24Accounts/Entity/Bitrix24Account.php b/src/Bitrix24Accounts/Entity/Bitrix24Account.php new file mode 100644 index 0000000..a1a5dac --- /dev/null +++ b/src/Bitrix24Accounts/Entity/Bitrix24Account.php @@ -0,0 +1,298 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\SDK\ApplicationCore\Bitrix24Accounts\Entity; + +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountInterface; +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; +use Bitrix24\SDK\Core\Credentials\AuthToken; +use Bitrix24\SDK\Core\Credentials\Scope; +use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Core\Exceptions\UnknownScopeCodeException; +use Bitrix24\SDK\Core\Response\DTO\RenewedAuthToken; +use Carbon\CarbonImmutable; +use Symfony\Component\Uid\Uuid; + +class Bitrix24Account implements Bitrix24AccountInterface +{ + private string $accessToken; + + private string $refreshToken; + + private int $expires; + + private array $applicationScope; + + private ?string $applicationToken = null; + + private ?string $comment = null; + + public function __construct( + private readonly Uuid $id, + private readonly int $bitrix24UserId, + private readonly bool $isBitrix24UserAdmin, + private readonly string $memberId, + private string $domainUrl, + private Bitrix24AccountStatus $accountStatus, + AuthToken $authToken, + private readonly CarbonImmutable $createdAt, + private CarbonImmutable $updatedAt, + private int $applicationVersion, + Scope $applicationScope, + ) + { + $this->accessToken = $authToken->getAccessToken(); + $this->refreshToken = $authToken->getRefreshToken(); + $this->expires = $authToken->getExpires(); + $this->applicationScope = $applicationScope->getScopeCodes(); + } + + #[\Override] + public function getId(): Uuid + { + return $this->id; + } + + #[\Override] + public function getBitrix24UserId(): int + { + return $this->bitrix24UserId; + } + + #[\Override] + public function isBitrix24UserAdmin(): bool + { + return $this->isBitrix24UserAdmin; + } + + #[\Override] + public function getMemberId(): string + { + return $this->memberId; + } + + #[\Override] + public function getDomainUrl(): string + { + return $this->domainUrl; + } + + #[\Override] + public function getStatus(): Bitrix24AccountStatus + { + return $this->accountStatus; + } + + #[\Override] + public function getAuthToken(): AuthToken + { + return new AuthToken($this->accessToken, $this->refreshToken, $this->expires); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function renewAuthToken(RenewedAuthToken $renewedAuthToken): void + { + if ($this->memberId !== $renewedAuthToken->memberId) { + throw new InvalidArgumentException( + sprintf( + 'member id %s for bitrix24 account %s for domain %s mismatch with member id %s for renewed access token', + $this->memberId, + $this->id->toRfc4122(), + $this->domainUrl, + $renewedAuthToken->memberId, + ) + ); + } + + $this->accessToken = $renewedAuthToken->authToken->getAccessToken(); + $this->refreshToken = $renewedAuthToken->authToken->getRefreshToken(); + $this->expires = $renewedAuthToken->authToken->getExpires(); + $this->updatedAt = new CarbonImmutable(); + } + + #[\Override] + public function getApplicationVersion(): int + { + return $this->applicationVersion; + } + + /** + * @throws UnknownScopeCodeException + */ + #[\Override] + public function getApplicationScope(): Scope + { + return new Scope($this->applicationScope); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function changeDomainUrl(string $newDomainUrl): void + { + if ($newDomainUrl === '') { + throw new InvalidArgumentException('new domain url cannot be empty'); + } + + if (Bitrix24AccountStatus::blocked === $this->accountStatus || Bitrix24AccountStatus::deleted === $this->accountStatus) { + throw new InvalidArgumentException( + sprintf( + 'bitrix24 account %s for domain %s must be in active or new state, now account in %s state. domain url cannot be changed', + $this->id->toRfc4122(), + $this->domainUrl, + $this->accountStatus->name + ) + ); + } + + $this->domainUrl = $newDomainUrl; + $this->updatedAt = new CarbonImmutable(); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function applicationInstalled(string $applicationToken): void + { + if (Bitrix24AccountStatus::new !== $this->accountStatus) { + throw new InvalidArgumentException(sprintf( + 'for finish installation bitrix24 account must be in status «new», current status - «%s»', + $this->accountStatus->name)); + } + + if ($applicationToken === '') { + throw new InvalidArgumentException('application token cannot be empty'); + } + + $this->accountStatus = Bitrix24AccountStatus::active; + $this->applicationToken = $applicationToken; + $this->updatedAt = new CarbonImmutable(); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function applicationUninstalled(string $applicationToken): void + { + if ($applicationToken === '') { + throw new InvalidArgumentException('application token cannot be empty'); + } + + if (Bitrix24AccountStatus::active !== $this->accountStatus) { + throw new InvalidArgumentException(sprintf( + 'for uninstall account must be in status «active», current status - «%s»', + $this->accountStatus->name)); + } + + if ($this->applicationToken !== $applicationToken) { + throw new InvalidArgumentException( + sprintf( + 'application token «%s» mismatch with application token «%s» for bitrix24 account %s for domain %s', + $applicationToken, + $this->applicationToken, + $this->id->toRfc4122(), + $this->domainUrl + ) + ); + } + + $this->accountStatus = Bitrix24AccountStatus::deleted; + $this->updatedAt = new CarbonImmutable(); + } + + #[\Override] + public function isApplicationTokenValid(string $applicationToken): bool + { + return $this->applicationToken === $applicationToken; + } + + #[\Override] + public function getCreatedAt(): CarbonImmutable + { + return $this->createdAt; + } + + #[\Override] + public function getUpdatedAt(): CarbonImmutable + { + return $this->updatedAt; + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function updateApplicationVersion(int $version, ?Scope $newScope): void + { + if (Bitrix24AccountStatus::active !== $this->accountStatus) { + throw new InvalidArgumentException(sprintf('account must be in status «active», but now account in status «%s»', $this->accountStatus->name)); + } + + if ($this->applicationVersion >= $version) { + throw new InvalidArgumentException( + sprintf('you cannot downgrade application version or set some version, current version «%s», but you try to upgrade to «%s»', + $this->applicationVersion, + $version)); + } + + $this->applicationVersion = $version; + if ($newScope instanceof \Bitrix24\SDK\Core\Credentials\Scope) { + $this->applicationScope = $newScope->getScopeCodes(); + } + + $this->updatedAt = new CarbonImmutable(); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function markAsActive(?string $comment): void + { + if (Bitrix24AccountStatus::blocked !== $this->accountStatus) { + throw new InvalidArgumentException( + sprintf('you can activate account only in status blocked, now account in status %s', + $this->accountStatus->name)); + } + + $this->accountStatus = Bitrix24AccountStatus::active; + $this->comment = $comment; + $this->updatedAt = new CarbonImmutable(); + } + + /** + * @throws InvalidArgumentException + */ + #[\Override] + public function markAsBlocked(?string $comment): void + { + if (Bitrix24AccountStatus::deleted === $this->accountStatus) { + throw new InvalidArgumentException('you cannot block account in status «deleted»'); + } + + $this->accountStatus = Bitrix24AccountStatus::blocked; + $this->comment = $comment; + $this->updatedAt = new CarbonImmutable(); + } + + #[\Override] + public function getComment(): ?string + { + return $this->comment; + } +} \ No newline at end of file diff --git a/tests/Unit/Bitrix24Accounts/Entity/Bitrix24AccountTest.php b/tests/Unit/Bitrix24Accounts/Entity/Bitrix24AccountTest.php new file mode 100644 index 0000000..f4986a9 --- /dev/null +++ b/tests/Unit/Bitrix24Accounts/Entity/Bitrix24AccountTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\SDK\ApplicationCore\Tests\Unit\Bitrix24Accounts\Entity; + +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountInterface; +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; +use Bitrix24\SDK\ApplicationCore\Bitrix24Accounts\Entity\Bitrix24Account; +use Bitrix24\SDK\Core\Credentials\AuthToken; +use Bitrix24\SDK\Core\Credentials\Scope; +use Bitrix24\SDK\Tests\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountInterfaceTest; +use Carbon\CarbonImmutable; +use Override; +use PHPUnit\Framework\Attributes\CoversClass; +use Symfony\Component\Uid\Uuid; + +#[CoversClass(Bitrix24Account::class)] +class Bitrix24AccountTest extends Bitrix24AccountInterfaceTest +{ + #[Override] + protected function createBitrix24AccountImplementation( + Uuid $uuid, + int $bitrix24UserId, + bool $isBitrix24UserAdmin, + string $memberId, + string $domainUrl, + Bitrix24AccountStatus $bitrix24AccountStatus, + AuthToken $authToken, + CarbonImmutable $createdAt, + CarbonImmutable $updatedAt, + int $applicationVersion, + Scope $applicationScope + ): Bitrix24AccountInterface + { + return new Bitrix24Account( + $uuid, + $bitrix24UserId, + $isBitrix24UserAdmin, + $memberId, + $domainUrl, + $bitrix24AccountStatus, + $authToken, + $createdAt, + $updatedAt, + $applicationVersion, + $applicationScope + ); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..8131bde --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +require_once dirname(__DIR__) . '/vendor/autoload.php'; \ No newline at end of file