diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bb9de13 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: + - master + - 3.next + pull_request: + branches: + - '*' + +permissions: + contents: read + +jobs: + testsuite: + uses: cakephp/.github/.github/workflows/testsuite-with-db.yml@5.x + secrets: inherit + + cs-stan: + uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x + secrets: inherit diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 0000000..9aa6dfe --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/README.md b/README.md index b576a78..7fe6409 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,9 @@ CakePHP behavior plugin for easily generating some complicated queries like (bul [![License](https://poser.pugx.org/itosho/easy-query/license)](https://packagist.org/packages/itosho/easy-query) ## Requirements -- PHP 7.2+ -- CakePHP 4.0+ -- MySQL 5.6+ - -:warning: For CakePHP3.x, use 1.x branch. +- PHP 8.1+ +- CakePHP 5.0+ +- MySQL 8.0+ / MariaDB 10.4+ ## Installation ```bash diff --git a/composer.json b/composer.json index bce050e..bcfd135 100644 --- a/composer.json +++ b/composer.json @@ -26,12 +26,14 @@ "source": "https://github.com/itosho/easy-query" }, "require": { - "php": ">=7.2.0", - "cakephp/orm": "^4.0" + "php": ">=8.1", + "cakephp/orm": "^5.0.0" }, "require-dev": { - "cakephp/cakephp": "^4.0", - "phpunit/phpunit": "^8.5" + "cakephp/cakephp": "^5.0.0", + "phpunit/phpunit": "^10.1.0", + "cakephp/cakephp-codesniffer": "^5.0", + "vimeo/psalm": "^5.15" }, "autoload": { "psr-4": { @@ -42,5 +44,10 @@ "psr-4": { "Itosho\\EasyQuery\\Test\\": "tests" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..0737a91 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..f51e71c --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/phpstan.neon b/phpstan.neon index ca05326..f1af199 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,9 @@ +includes: + - phpstan-baseline.neon + parameters: - checkMissingIterableValueType: false - ignoreErrors: [] + level: 6 + checkGenericClassInNonGenericObjectType: false + checkMissingIterableValueType: false + paths: + - src/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 878585e..7acf8f8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,37 +1,23 @@ - + + + - - ./tests/TestCase + tests/TestCase/ - - - - - - - - - - - - - ./src/ - - - + + + + + + src/ + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..08f635d --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Model/Behavior/InsertBehavior.php b/src/Model/Behavior/InsertBehavior.php index 5d1c050..883e77f 100644 --- a/src/Model/Behavior/InsertBehavior.php +++ b/src/Model/Behavior/InsertBehavior.php @@ -5,11 +5,11 @@ use Cake\Database\Expression\QueryExpression; use Cake\Database\StatementInterface; -use Cake\I18n\FrozenTime; +use Cake\Datasource\EntityInterface; +use Cake\I18n\DateTime; use Cake\ORM\Behavior; -use Cake\ORM\Entity; -use Cake\ORM\Query; -use Cake\ORM\TableRegistry; +use Cake\ORM\Locator\LocatorAwareTrait; +use Cake\ORM\Query\SelectQuery; use LogicException; /** @@ -17,21 +17,23 @@ */ class InsertBehavior extends Behavior { + use LocatorAwareTrait; + /** * Default config * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'event' => ['beforeSave' => true], ]; /** * execute bulk insert query * - * @param Entity[] $entities insert entities - * @throws LogicException no save data - * @return StatementInterface query result + * @param array<\Cake\Datasource\EntityInterface> $entities insert entities + * @throws \LogicException no save data + * @return \Cake\Database\StatementInterface query result */ public function bulkInsert(array $entities): StatementInterface { @@ -50,7 +52,7 @@ public function bulkInsert(array $entities): StatementInterface $fields = array_keys($saveData[0]); $query = $this->_table - ->query() + ->insertQuery() ->insert($fields); $query->clause('values')->setValues($saveData); @@ -60,11 +62,11 @@ public function bulkInsert(array $entities): StatementInterface /** * execute insert select query for saving a record just once * - * @param Entity $entity insert entity + * @param \Cake\Datasource\EntityInterface $entity insert entity * @param array|null $conditions search conditions - * @return StatementInterface query result + * @return \Cake\Database\StatementInterface query result */ - public function insertOnce(Entity $entity, array $conditions = null): StatementInterface + public function insertOnce(EntityInterface $entity, ?array $conditions = null): StatementInterface { if ($this->_config['event']['beforeSave']) { $this->_table->dispatchEvent('Model.beforeSave', compact('entity')); @@ -72,11 +74,11 @@ public function insertOnce(Entity $entity, array $conditions = null): StatementI $entity->setVirtual([]); $insertData = $entity->toArray(); - if (isset($insertData['created']) && !is_null($insertData['created'])) { - $insertData['created'] = FrozenTime::now()->toDateTimeString(); + if (isset($insertData['created'])) { + $insertData['created'] = DateTime::now()->toDateTimeString(); } - if (isset($insertData['modified']) && !is_null($insertData['modified'])) { - $insertData['modified'] = FrozenTime::now()->toDateTimeString(); + if (isset($insertData['modified'])) { + $insertData['modified'] = DateTime::now()->toDateTimeString(); } $fields = array_keys($insertData); @@ -84,7 +86,7 @@ public function insertOnce(Entity $entity, array $conditions = null): StatementI if (is_null($existsConditions)) { $existsConditions = $this->getExistsConditions($insertData); } - $query = $this->_table->query()->insert($fields); + $query = $this->_table->insertQuery()->insert($fields); $subQuery = $this ->buildTmpTableSelectQuery($insertData) ->where(function (QueryExpression $exp) use ($existsConditions) { @@ -95,7 +97,7 @@ public function insertOnce(Entity $entity, array $conditions = null): StatementI return $exp->notExists($query); }) ->limit(1); - /* @phpstan-ignore-next-line */ + $query = $query->epilog($subQuery); return $query->execute(); @@ -105,10 +107,10 @@ public function insertOnce(Entity $entity, array $conditions = null): StatementI * build tmp table's select query for insert select query * * @param array $insertData insert data - * @throws LogicException select query is invalid - * @return Query tmp table's select query + * @return \Cake\ORM\Query\SelectQuery tmp table's select query + * @throws \LogicException select query is invalid */ - private function buildTmpTableSelectQuery($insertData): Query + private function buildTmpTableSelectQuery(array $insertData): SelectQuery { $driver = $this->_table ->getConnection() @@ -118,15 +120,15 @@ private function buildTmpTableSelectQuery($insertData): Query foreach ($insertData as $key => $value) { $col = $driver->quoteIdentifier($key); if (is_null($value)) { - $schema[] = "NULL AS {$col}"; + $schema[] = "NULL AS $col"; } else { $bindKey = ':' . strtolower($key); $binds[$bindKey] = $value; - $schema[] = "{$bindKey} AS {$col}"; + $schema[] = "$bindKey AS $col"; } } - $tmpTable = TableRegistry::getTableLocator()->get('tmp', [ + $tmpTable = $this->fetchTable('tmp', [ 'schema' => $this->_table->getSchema(), ]); $query = $tmpTable diff --git a/src/Model/Behavior/UpsertBehavior.php b/src/Model/Behavior/UpsertBehavior.php index 374f7e0..33d6f78 100644 --- a/src/Model/Behavior/UpsertBehavior.php +++ b/src/Model/Behavior/UpsertBehavior.php @@ -6,7 +6,6 @@ use Cake\Database\StatementInterface; use Cake\Datasource\EntityInterface; use Cake\ORM\Behavior; -use Cake\ORM\Entity; use LogicException; /** @@ -17,9 +16,9 @@ class UpsertBehavior extends Behavior /** * Default config * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'updateColumns' => null, 'uniqueColumns' => null, 'event' => ['beforeSave' => true], @@ -28,11 +27,11 @@ class UpsertBehavior extends Behavior /** * execute upsert query * - * @param Entity $entity upsert entity - * @return EntityInterface|array|null result entity - * @throws LogicException invalid config + * @param \Cake\Datasource\EntityInterface $entity upsert entity + * @return \Cake\Datasource\EntityInterface|array|null result entity + * @throws \LogicException invalid config */ - public function upsert(Entity $entity) + public function upsert(EntityInterface $entity): array|EntityInterface|null { if (!$this->isValidArrayConfig('updateColumns')) { throw new LogicException('config updateColumns is invalid.'); @@ -52,13 +51,13 @@ public function upsert(Entity $entity) $updateValues = []; foreach ($updateColumns as $column) { - $updateValues[] = "`{$column}`=VALUES(`{$column}`)"; + $updateValues[] = "`$column`=VALUES(`$column`)"; } $updateStatement = implode(', ', $updateValues); $expression = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; $this->_table - ->query() + ->insertQuery() ->insert($fields) ->values($upsertData) ->epilog($expression) @@ -80,9 +79,9 @@ public function upsert(Entity $entity) /** * execute bulk upsert query * - * @param Entity[] $entities upsert entities - * @return StatementInterface query result - * @throws LogicException invalid config or no save data + * @param array<\Cake\Datasource\EntityInterface> $entities upsert entities + * @return \Cake\Database\StatementInterface query result + * @throws \LogicException invalid config or no save data */ public function bulkUpsert(array $entities): StatementInterface { @@ -107,13 +106,12 @@ public function bulkUpsert(array $entities): StatementInterface $updateColumns = $this->_config['updateColumns']; $updateValues = []; foreach ($updateColumns as $column) { - $updateValues[] = "`{$column}`=VALUES(`{$column}`)"; + $updateValues[] = "`$column`=VALUES(`$column`)"; } $updateStatement = implode(', ', $updateValues); $expression = 'ON DUPLICATE KEY UPDATE ' . $updateStatement; - $query = $this->_table - ->query() + ->insertQuery() ->insert($fields) ->epilog($expression); $query->clause('values')->setValues($saveData); @@ -125,7 +123,6 @@ public function bulkUpsert(array $entities): StatementInterface * validate config value * * @param string $configName config key - * * @return bool valid or invalid */ private function isValidArrayConfig(string $configName): bool diff --git a/tests/Fixture/ArticlesFixture.php b/tests/Fixture/ArticlesFixture.php index 55a5835..ba6710b 100644 --- a/tests/Fixture/ArticlesFixture.php +++ b/tests/Fixture/ArticlesFixture.php @@ -7,18 +7,7 @@ class ArticlesFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'title' => ['type' => 'string', 'length' => 255, 'null' => false], - 'body' => 'text', - 'published' => ['type' => 'integer', 'default' => '0', 'null' => false], - 'created' => 'datetime', - 'modified' => 'datetime', - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id']], - ], - ]; - public $records = [ + public array $records = [ [ 'title' => 'First Article', 'body' => 'First Article Body', diff --git a/tests/Fixture/TagsFixture.php b/tests/Fixture/TagsFixture.php index 4d11962..a9d4250 100644 --- a/tests/Fixture/TagsFixture.php +++ b/tests/Fixture/TagsFixture.php @@ -7,18 +7,7 @@ class TagsFixture extends TestFixture { - public $fields = [ - 'id' => ['type' => 'integer'], - 'name' => ['type' => 'string', 'length' => 255, 'null' => false], - 'description' => ['type' => 'string', 'length' => 255, 'null' => false], - 'created' => 'datetime', - 'modified' => 'datetime', - '_constraints' => [ - 'primary' => ['type' => 'primary', 'columns' => ['id']], - 'unique' => ['type' => 'unique', 'columns' => ['name']], - ], - ]; - public $records = [ + public array $records = [ [ 'name' => 'tag1', 'description' => 'tag1 description', diff --git a/tests/TestCase/Model/Behavior/InsertBehaviorTest.php b/tests/TestCase/Model/Behavior/InsertBehaviorTest.php index 30ed2d9..e634eb1 100644 --- a/tests/TestCase/Model/Behavior/InsertBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/InsertBehaviorTest.php @@ -3,11 +3,10 @@ namespace Itosho\EasyQuery\Test\TestCase\Model\Behavior; -use Cake\Chronos\Chronos; -use Cake\I18n\FrozenTime; +use Cake\I18n\DateTime; use Cake\ORM\Table; -use Cake\ORM\TableRegistry; use Cake\TestSuite\TestCase; +use LogicException; /** * Itosho\EasyQuery\Model\Behavior\InsertBehavior Test Case @@ -19,31 +18,31 @@ class InsertBehaviorTest extends TestCase * * @var Table */ - public $Articles; + public Table $Articles; /** * Fixtures * * @var array */ - public $fixtures = ['plugin.Itosho/EasyQuery.Articles']; + public array $fixtures = ['plugin.Itosho/EasyQuery.Articles']; /** - * {@inheritDoc} + * @inheritDoc */ public function setUp(): void { parent::setUp(); - $this->Articles = TableRegistry::getTableLocator()->get('Itosho/EasyQuery.Articles'); + $this->Articles = $this->getTableLocator()->get('Itosho/EasyQuery.Articles'); $this->Articles->addBehavior('Itosho/EasyQuery.Insert'); } /** - * {@inheritDoc} + * @inheritDoc */ public function tearDown(): void { parent::tearDown(); - TableRegistry::getTableLocator()->clear(); + $this->getTableLocator()->clear(); unset($this->Articles); } @@ -55,7 +54,7 @@ public function tearDown(): void public function testBulkInsert() { $records = $this->getBaseInsertRecords(); - $now = Chronos::now(); + $now = DateTime::now(); foreach ($records as $key => $val) { $record[$key]['created'] = $now; $record[$key]['modified'] = $now; @@ -85,7 +84,7 @@ public function testBulkInsertAddTimestamp() $records[0]['modified'] = $customNow; $expectedRecords = $this->getBaseInsertRecords(); - $now = Chronos::now(); + $now = DateTime::now(); foreach ($expectedRecords as $key => $val) { $expectedRecords[$key]['created'] = $now; $expectedRecords[$key]['modified'] = $now; @@ -145,8 +144,8 @@ public function testBulkInsertNoBeforeSave() */ public function testBulkInsertNoSaveData() { - $this->expectExceptionMessage("entities has no save data."); - $this->expectException(\LogicException::class); + $this->expectExceptionMessage('entities has no save data.'); + $this->expectException(LogicException::class); $this->Articles->bulkInsert([]); } @@ -190,7 +189,7 @@ public function testInsertOnceAddTimestampBehavior() 'published' => 1, ]; $entity = $this->Articles->newEntity($newData); - $now = FrozenTime::now(); + $now = DateTime::now(); $this->Articles->insertOnce($entity); diff --git a/tests/TestCase/Model/Behavior/UpsertBehaviorTest.php b/tests/TestCase/Model/Behavior/UpsertBehaviorTest.php index 4a5cbb8..3a6ffdb 100644 --- a/tests/TestCase/Model/Behavior/UpsertBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UpsertBehaviorTest.php @@ -3,10 +3,10 @@ namespace Itosho\EasyQuery\Test\TestCase\Model\Behavior; -use Cake\Chronos\Chronos; +use Cake\I18n\DateTime; use Cake\ORM\Table; -use Cake\ORM\TableRegistry; use Cake\TestSuite\TestCase; +use LogicException; /** * Itosho\EasyQuery\Model\Behavior\UpsertBehavior Test Case @@ -18,21 +18,21 @@ class UpsertBehaviorTest extends TestCase * * @var Table */ - public $Tags; + public Table $Tags; /** * Fixtures * * @var array */ - public $fixtures = ['plugin.Itosho/EasyQuery.Tags']; + public array $fixtures = ['plugin.Itosho/EasyQuery.Tags']; /** - * {@inheritDoc} + * @inheritDoc */ public function setUp(): void { parent::setUp(); - $this->Tags = TableRegistry::getTableLocator()->get('Itosho/EasyQuery.Tags'); + $this->Tags = $this->getTableLocator()->get('Itosho/EasyQuery.Tags'); $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [ 'uniqueColumns' => ['name'], 'updateColumns' => ['description', 'modified'], @@ -40,12 +40,12 @@ public function setUp(): void } /** - * {@inheritDoc} + * @inheritDoc */ public function tearDown(): void { parent::tearDown(); - TableRegistry::getTableLocator()->clear(); + $this->getTableLocator()->clear(); unset($this->Tags); } @@ -56,7 +56,7 @@ public function tearDown(): void */ public function testUpsertByInsert() { - $now = Chronos::now(); + $now = DateTime::now(); $record = [ 'name' => 'tag4', 'description' => 'tag4 description', @@ -97,7 +97,7 @@ public function testUpsertByInsertAddTimestamp() 'name' => 'tag4', 'description' => 'tag4 description', ]; - $now = Chronos::now(); + $now = DateTime::now(); $expectedRecord = $record; $expectedRecord['created'] = $now; $expectedRecord['modified'] = $now; @@ -172,7 +172,7 @@ public function testUpsertByUpdateAddTimestamp() 'name' => 'tag1', 'description' => 'brand new tag1 description', ]; - $now = Chronos::now(); + $now = DateTime::now(); $currentCreated = '2017-09-01 00:00:00'; $expectedRecord = $record; $expectedRecord['created'] = $currentCreated; @@ -242,8 +242,8 @@ public function testUpsertNoBeforeSave() */ public function testUpsertInvalidUpdateColumnsConfig() { - $this->expectExceptionMessage("config updateColumns is invalid."); - $this->expectException(\LogicException::class); + $this->expectExceptionMessage('config updateColumns is invalid.'); + $this->expectException(LogicException::class); $this->Tags->removeBehavior('Upsert'); $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [ @@ -267,8 +267,8 @@ public function testUpsertInvalidUpdateColumnsConfig() */ public function testUpsertInvalidUniqueColumnsConfig() { - $this->expectExceptionMessage("config uniqueColumns is invalid."); - $this->expectException(\LogicException::class); + $this->expectExceptionMessage('config uniqueColumns is invalid.'); + $this->expectException(LogicException::class); $this->Tags->removeBehavior('Upsert'); $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [ @@ -298,7 +298,7 @@ public function testBulkUpsertByInsert() ]); $records = $this->getBaseInsertRecords(); - $now = Chronos::now(); + $now = DateTime::now(); foreach ($records as $key => $val) { $records[$key]['created'] = $now; $records[$key]['modified'] = $now; @@ -327,7 +327,7 @@ public function testBulkUpsertByInsertAddTimestamp() $this->Tags->addBehavior('Timestamp'); $records = $this->getBaseInsertRecords(); - $now = Chronos::now(); + $now = DateTime::now(); $expectedRecords = $records; foreach ($expectedRecords as $key => $val) { $expectedRecords[$key]['created'] = $now; @@ -356,7 +356,7 @@ public function testBulkUpsertByUpdate() ]); $records = $this->getBaseUpdateRecords(); - $now = Chronos::now(); + $now = DateTime::now(); foreach ($records as $key => $val) { $records[$key]['created'] = $now; $records[$key]['modified'] = $now; @@ -387,7 +387,7 @@ public function testBulkUpsertByUpdateAddTimestamp() $this->Tags->addBehavior('Timestamp'); $records = $this->getBaseUpdateRecords(); - $now = Chronos::now(); + $now = DateTime::now(); $currentCreated = '2017-09-01 00:00:00'; $expectedRecords = $records; foreach ($expectedRecords as $key => $val) { @@ -441,14 +441,14 @@ public function testBulkUpsertNoBeforeSave() */ public function testBulkUpsertInvalidUpdateColumnsConfig() { - $this->expectExceptionMessage("config updateColumns is invalid."); - $this->expectException(\LogicException::class); + $this->expectExceptionMessage('config updateColumns is invalid.'); + $this->expectException(LogicException::class); $this->Tags->removeBehavior('Upsert'); $this->Tags->addBehavior('Itosho/EasyQuery.Upsert'); $records = $this->getBaseInsertRecords(); - $now = Chronos::now(); + $now = DateTime::now(); foreach ($records as $key => $val) { $records[$key]['created'] = $now; $records[$key]['modified'] = $now; @@ -465,8 +465,8 @@ public function testBulkUpsertInvalidUpdateColumnsConfig() */ public function testBulkUpsertNoSaveData() { - $this->expectExceptionMessage("entities has no save data."); - $this->expectException(\LogicException::class); + $this->expectExceptionMessage('entities has no save data.'); + $this->expectException(LogicException::class); $this->Tags->removeBehavior('Upsert'); $this->Tags->addBehavior('Itosho/EasyQuery.Upsert', [ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e2b0179..66d427f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,18 +1,19 @@ Connection::class, - 'driver' => Mysql::class, - 'host' => getenv('db_host'), - 'username' => getenv('db_user'), - 'database' => getenv('db_name'), + 'url' => env('DB_URL'), ]; ConnectionManager::drop('test'); ConnectionManager::setConfig('test', $dbConfig); + +// Create test database schema +if (env('FIXTURE_SCHEMA_METADATA')) { + $loader = new SchemaLoader(); + $loader->loadInternalFile(env('FIXTURE_SCHEMA_METADATA')); +} diff --git a/tests/schema.php b/tests/schema.php new file mode 100644 index 0000000..c4d65d5 --- /dev/null +++ b/tests/schema.php @@ -0,0 +1,33 @@ + 'articles', + 'columns' => [ + 'id' => ['type' => 'integer'], + 'title' => ['type' => 'string', 'length' => 255, 'null' => false], + 'body' => 'text', + 'published' => ['type' => 'integer', 'default' => '0', 'null' => false], + 'created' => 'datetime', + 'modified' => 'datetime', + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ], + [ + 'table' => 'tags', + 'columns' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string', 'length' => 255, 'null' => false], + 'description' => ['type' => 'string', 'length' => 255, 'null' => false], + 'created' => 'datetime', + 'modified' => 'datetime', + ], + 'constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + 'unique' => ['type' => 'unique', 'columns' => ['name']], + ], + ], +];