diff --git a/.github/workflows/ci-mssql.yml b/.github/workflows/ci-mssql.yml index ad39a85..f3c9fd4 100644 --- a/.github/workflows/ci-mssql.yml +++ b/.github/workflows/ci-mssql.yml @@ -24,7 +24,10 @@ jobs: extensions: pdo, pdo_sqlsrv mssql: 'server:2019-latest' - php: '8.1' - extensions: pdo, pdo_sqlsrv-5.10.0beta2 + extensions: pdo, pdo_sqlsrv-5.11.0 + mssql: 'server:2019-latest' + - php: '8.2' + extensions: pdo, pdo_sqlsrv-5.11.0 mssql: 'server:2019-latest' services: diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml index b1387fb..c69859b 100644 --- a/.github/workflows/ci-mysql.yml +++ b/.github/workflows/ci-mysql.yml @@ -22,6 +22,7 @@ jobs: php-version: - "8.0" - "8.1" + - "8.2" mysql-version: - "5.7" diff --git a/.github/workflows/ci-pgsql.yml b/.github/workflows/ci-pgsql.yml index 5447b33..38aa820 100644 --- a/.github/workflows/ci-pgsql.yml +++ b/.github/workflows/ci-pgsql.yml @@ -21,6 +21,7 @@ jobs: php-version: - "8.0" - "8.1" + - "8.2" pgsql-version: - "10" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af94c21..d0c966f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,7 @@ jobs: php-version: - "8.0" - "8.1" + - "8.2" steps: - name: Checkout uses: actions/checkout@v2 @@ -72,6 +73,7 @@ jobs: php-version: - "8.0" - "8.1" + - "8.2" steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5c63a24..dcd6a4b 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,47 +1,16 @@ -name: static analysis +on: + pull_request: + push: + branches: + - '*.*' -on: [push, pull_request] +name: static analysis jobs: - mutation: - name: PHP ${{ matrix.php }}-${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "8.0" - - steps: - - name: Checkout - uses: actions/checkout@v2.3.4 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php }}" - tools: composer:v2, cs2pr - coverage: none - - - name: Determine composer cache directory - run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - - - name: Cache dependencies installed with composer - uses: actions/cache@v2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - php${{ matrix.php }}-composer- - - name: Update composer - run: composer self-update - - - name: Install dependencies with composer - run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - - name: Static analysis - run: vendor/bin/psalm --shepherd --stats --output-format=checkstyle \ No newline at end of file + psalm: + uses: spiral/gh-actions/.github/workflows/psalm.yml@master + with: + os: >- + ['ubuntu-latest'] + php: >- + ['8.1'] diff --git a/composer.json b/composer.json index 0ffbc28..965dbc4 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "cycle/entity-behavior", + "description": "Provides a collection of attributes that add behaviors to Cycle ORM entities", "type": "library", "require": { "php": ">=8.0", @@ -11,10 +12,10 @@ }, "require-dev": { "cycle/annotated": "^3.0", - "ramsey/uuid": "^4.0", + "ramsey/uuid": "^4.5", "phpunit/phpunit": "^9.5", - "spiral/tokenizer": "^2.8", - "vimeo/psalm": "^4.13" + "spiral/tokenizer": "^2.8 || ^3.0", + "vimeo/psalm": "^5.11" }, "license": "MIT", "autoload": { diff --git a/psalm.xml b/psalm.xml index 0fed263..3aede9f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -4,6 +4,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + findUnusedBaselineEntry="true" + findUnusedCode="false" > diff --git a/src/OptimisticLock.php b/src/OptimisticLock.php index d48f408..583c3e0 100644 --- a/src/OptimisticLock.php +++ b/src/OptimisticLock.php @@ -4,12 +4,11 @@ namespace Cycle\ORM\Entity\Behavior; -use Cycle\Database\ColumnInterface; -use Cycle\Database\Schema\AbstractColumn; use Cycle\ORM\Entity\Behavior\Schema\BaseModifier; use Cycle\ORM\Entity\Behavior\Schema\RegistryModifier; use Cycle\ORM\Entity\Behavior\Exception\BehaviorCompilationException; use Cycle\ORM\Entity\Behavior\Listener\OptimisticLock as Listener; +use Cycle\Schema\Definition\Field; use Cycle\Schema\Registry; use Doctrine\Common\Annotations\Annotation\Enum; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -58,9 +57,9 @@ final class OptimisticLock extends BaseModifier */ public function __construct( private string $field = 'version', + ?string $column = null, /** @Enum({"microtime", "random-string", "increment", "datetime"}) */ #[ExpectedValues(valuesFromClass: Listener::class)] - ?string $column = null, private ?string $rule = null ) { $this->column = $column; @@ -106,12 +105,14 @@ public function render(Registry $registry): void * * @throws BehaviorCompilationException */ - private function computeRule(AbstractColumn $column): string + private function computeRule(Field $field): string { - return match ($column->getType()) { - ColumnInterface::INT => self::RULE_INCREMENT, - ColumnInterface::STRING => self::RULE_MICROTIME, - 'datetime' => self::RULE_DATETIME, + $type = $field->getType(); + + return match (true) { + RegistryModifier::isIntegerType($type) => self::RULE_INCREMENT, + RegistryModifier::isStringType($type) => self::RULE_MICROTIME, + RegistryModifier::isDatetimeType($type) => self::RULE_DATETIME, default => throw new BehaviorCompilationException('Failed to compute rule based on column type.') }; } @@ -123,7 +124,7 @@ private function addField(Registry $registry): void assert($this->column !== null); $this->rule ??= $fields->has($this->field) - ? $this->computeRule($registry->getTableSchema($registry->getEntity($this->role))->column($this->column)) + ? $this->computeRule($fields->get($this->field)) // rule not set, field not fount : Listener::DEFAULT_RULE; diff --git a/src/Schema/RegistryModifier.php b/src/Schema/RegistryModifier.php index b4392fd..bacf047 100644 --- a/src/Schema/RegistryModifier.php +++ b/src/Schema/RegistryModifier.php @@ -19,6 +19,18 @@ */ class RegistryModifier { + protected const DEFINITION = '/(?P[a-z]+)(?: *\((?P[^\)]+)\))?/i'; + protected const INTEGER_TYPES = [ + 'int', + 'smallint', + 'tinyint', + 'bigint', + 'integer', + 'tinyInteger', + 'smallInteger', + 'bigInteger', + ]; + protected const DATETIME_TYPES = ['datetime', 'datetime2']; protected const INT_COLUMN = AbstractColumn::INT; protected const STRING_COLUMN = AbstractColumn::STRING; protected const DATETIME_COLUMN = 'datetime'; @@ -38,7 +50,7 @@ public function __construct(Registry $registry, string $role) public function addDatetimeColumn(string $columnName, string $fieldName): AbstractColumn { if ($this->fields->has($fieldName)) { - if (!$this->isType(self::DATETIME_COLUMN, $fieldName, $columnName)) { + if (!static::isDatetimeType($this->fields->get($fieldName)->getType())) { throw new BehaviorCompilationException(sprintf('Field %s must be of type datetime.', $fieldName)); } $this->validateColumnName($fieldName, $columnName); @@ -57,7 +69,7 @@ public function addDatetimeColumn(string $columnName, string $fieldName): Abstra public function addIntegerColumn(string $columnName, string $fieldName): AbstractColumn { if ($this->fields->has($fieldName)) { - if (!$this->isType(self::INT_COLUMN, $fieldName, $columnName)) { + if (!static::isIntegerType($this->fields->get($fieldName)->getType())) { throw new BehaviorCompilationException(sprintf('Field %s must be of type integer.', $fieldName)); } $this->validateColumnName($fieldName, $columnName); @@ -73,7 +85,7 @@ public function addIntegerColumn(string $columnName, string $fieldName): Abstrac public function addStringColumn(string $columnName, string $fieldName): AbstractColumn { if ($this->fields->has($fieldName)) { - if (!$this->isType(self::STRING_COLUMN, $fieldName, $columnName)) { + if (!static::isStringType($this->fields->get($fieldName)->getType())) { throw new BehaviorCompilationException(sprintf('Field %s must be of type string.', $fieldName)); } $this->validateColumnName($fieldName, $columnName); @@ -92,7 +104,7 @@ public function addStringColumn(string $columnName, string $fieldName): Abstract public function addUuidColumn(string $columnName, string $fieldName): AbstractColumn { if ($this->fields->has($fieldName)) { - if (!$this->isType(self::UUID_COLUMN, $fieldName, $columnName)) { + if (!static::isUuidType($this->fields->get($fieldName)->getType())) { throw new BehaviorCompilationException(sprintf('Field %s must be of type uuid.', $fieldName)); } $this->validateColumnName($fieldName, $columnName); @@ -156,6 +168,9 @@ protected function validateColumnName(string $fieldName, string $columnName): vo } } + /** + * @deprecated since v1.2 + */ protected function isType(string $type, string $fieldName, string $columnName): bool { if ($type === self::DATETIME_COLUMN) { @@ -176,4 +191,32 @@ protected function isType(string $type, string $fieldName, string $columnName): return $this->table->column($columnName)->getType() === $type; } + + public static function isIntegerType(string $type): bool + { + \preg_match(self::DEFINITION, $type, $matches); + + return \in_array($matches['type'], self::INTEGER_TYPES, true); + } + + public static function isDatetimeType(string $type): bool + { + \preg_match(self::DEFINITION, $type, $matches); + + return \in_array($matches['type'], self::DATETIME_TYPES, true); + } + + public static function isStringType(string $type): bool + { + \preg_match(self::DEFINITION, $type, $matches); + + return $matches['type'] === 'string'; + } + + public static function isUuidType(string $type): bool + { + \preg_match(self::DEFINITION, $type, $matches); + + return $matches['type'] === 'uuid'; + } } diff --git a/tests/Behavior/Fixtures/OptimisticLock/WithAllParameters.php b/tests/Behavior/Fixtures/OptimisticLock/WithAllParameters.php new file mode 100644 index 0000000..f270abd --- /dev/null +++ b/tests/Behavior/Fixtures/OptimisticLock/WithAllParameters.php @@ -0,0 +1,24 @@ +assertSame(2, $fields->count()); } + public function testExistColumnAndAllOptimisticLockParameters(): void + { + $fields = $this->registry->getEntity(WithAllParameters::class)->getFields(); + + $this->assertTrue($fields->has('revision')); + $this->assertTrue($fields->hasColumn('revision_field')); + $this->assertSame('integer', $fields->get('revision')->getType()); + + // not added new columns + $this->assertSame(2, $fields->count()); + } + public function testAddDefaultColumAndRule(): void { $fields = $this->registry->getEntity(News::class)->getFields(); diff --git a/tests/Behavior/Functional/Driver/Common/Schema/RegistryModifierTest.php b/tests/Behavior/Functional/Driver/Common/Schema/RegistryModifierTest.php index 67ed62d..b4e4af9 100644 --- a/tests/Behavior/Functional/Driver/Common/Schema/RegistryModifierTest.php +++ b/tests/Behavior/Functional/Driver/Common/Schema/RegistryModifierTest.php @@ -18,6 +18,7 @@ abstract class RegistryModifierTest extends BaseTest private const ROLE_TEST = 'test'; protected RegistryModifier $modifier; + protected Registry $registry; public function setUp(): void { diff --git a/tests/Behavior/Unit/Schema/RegistryModifierTest.php b/tests/Behavior/Unit/Schema/RegistryModifierTest.php new file mode 100644 index 0000000..6e85224 --- /dev/null +++ b/tests/Behavior/Unit/Schema/RegistryModifierTest.php @@ -0,0 +1,115 @@ +assertTrue(RegistryModifier::isIntegerType($type)); + } + + /** + * @dataProvider datetimeDataProvider + * @dataProvider stringDataProvider + * @dataProvider invalidDataProvider + */ + public function testIsIntegerTypeFalse(mixed $type): void + { + $this->assertFalse(RegistryModifier::isIntegerType($type)); + } + + /** + * @dataProvider datetimeDataProvider + */ + public function testIsDatetimeTypeTrue(mixed $type): void + { + $this->assertTrue(RegistryModifier::isDatetimeType($type)); + } + + /** + * @dataProvider integerDataProvider + * @dataProvider stringDataProvider + * @dataProvider invalidDataProvider + */ + public function testIsDatetimeTypeFalse(mixed $type): void + { + $this->assertFalse(RegistryModifier::isDatetimeType($type)); + } + + /** + * @dataProvider stringDataProvider + */ + public function testIsStringTypeTrue(mixed $type): void + { + $this->assertTrue(RegistryModifier::isStringType($type)); + } + + /** + * @dataProvider integerDataProvider + * @dataProvider datetimeDataProvider + * @dataProvider invalidDataProvider + */ + public function testIsStringTypeFalse(mixed $type): void + { + $this->assertFalse(RegistryModifier::isStringType($type)); + } + + public function testIsUuidTypeTrue(): void + { + $this->assertTrue(RegistryModifier::isUuidType('uuid')); + } + + /** + * @dataProvider integerDataProvider + * @dataProvider datetimeDataProvider + * @dataProvider invalidDataProvider + * @dataProvider stringDataProvider + */ + public function testIsUuidTypeFalse(mixed $type): void + { + $this->assertFalse(RegistryModifier::isUuidType($type)); + } + + public static function integerDataProvider(): \Traversable + { + yield ['int']; + yield ['smallint']; + yield ['tinyint']; + yield ['bigint']; + yield ['integer']; + yield ['tinyInteger']; + yield ['smallInteger']; + yield ['bigInteger']; + yield ['integer(4)']; + } + + public static function datetimeDataProvider(): \Traversable + { + yield ['datetime']; + yield ['datetime2']; + yield ['datetime2(7)']; + } + + public static function stringDataProvider(): \Traversable + { + yield ['string']; + yield ['string(32)']; + } + + public static function invalidDataProvider(): \Traversable + { + yield ['text']; + yield ['json']; + yield ['foo']; + yield ['bar(32)']; + } +}