diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d09a5f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.github export-ignore
+/.scrutinizer.yml export-ignore
+/doc export-ignore
+/phpunit.xml export-ignore
+/tests export-ignore
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..206c8af
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,51 @@
+name: build
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate --strict
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Create runtime cache folder
+ run: mkdir -v -p -m 777 tests/runtime/cache
+
+ - name: Create sqlite folder
+ run: mkdir -v -p -m 777 tests/runtime/sqlite && touch tests/runtime/sqlite/database.db
+
+ - name: Execute Tests
+ run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover
+ env:
+ XDEBUG_MODE: coverage
+
+ - name: Upload coverage reports to Codecov
+ continue-on-error: true
+ uses: codecov/codecov-action@v3
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.clover
diff --git a/.gitignore b/.gitignore
index a4cb13c..caa2c69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
.idea
.phpunit.result.cache
+.phpunit.cache
+coverage.clover
+coverage.txt
vendor/
composer.lock
tests/runtime/
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..b9d8d04
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,16 @@
+checks:
+ php: true
+
+filter:
+ paths:
+ - "src/*"
+
+tools:
+ external_code_coverage:
+ timeout: 900 # Timeout in seconds.
+ runs: 2 # How many code coverage submissions Scrutinizer will wait
+
+build:
+ image: default-bionic
+ environment:
+ php: 8.1.2
\ No newline at end of file
diff --git a/README.md b/README.md
index c4f3f59..256a728 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,37 @@
# What is Composite DB
+[](https://packagist.org/packages/compositephp/db)
+[](https://github.com/compositephp/db/actions)
+[](https://codecov.io/gh/compositephp/db/)
-Composite DB is lightweight and fast PHP ORM, DataMapper and Table Gateway which allows you to represent your SQL tables
+Composite DB is lightweight and fast PHP DataMapper and Table Gateway which allows you to represent your SQL tables
scheme in OOP style using full power of PHP 8.1+ class syntax.
It also gives you CRUD, query builder and automatic caching out of the box, so you can start
to work with your database from php code in a minutes!
Overview:
-* [Mission](#mission)
+* [Features](#features)
* [Requirements](#requirements)
* [Installation](#installation)
* [Quick example](#quick-example)
* [Documentation](doc/README.md)
-## Mission
-You probably may ask, why do you need another ORM if there are already popular Doctrine, CycleORM, etc.?
-
-Composite DB solves multiple problems:
+## Features
* **Lightweight** - easier entity schema, no getters and setters, you don't need attributes for each column definition,
just use native php class syntax.
-* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode.
-* **Easy caching** - gives you CRUD operations caching out of the box and in general its much easier to work with cached "selects".
+* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode (see [benchmark](https://github.com/compositephp/php-orm-benchmark)).
+* **Easy caching** - gives you CRUD operations caching out of the box and in general it's much easier to work with cached "selects".
* **Strict types** - Composite DB forces you to be more strict typed and makes your IDE happy.
* **Hydration** - you can serialize your Entities to plain array or json and deserialize them back.
-* **Flexibility** - gives you more freedom to extend Repositories, for example its easier to build sharding tables.
+* **Flexibility** - gives you more freedom to extend Repositories, for example it's easier to build sharding tables.
* **Code generation** - you can generate Entity and Repository classes from your SQL tables.
-* **Division of responsibility** - there is no "god" entity manager, every Entity has its own Repository class and its the only entry point to make queries to your table.
+* **Division of responsibility** - every Entity has its own Repository class, and it's the only entry point to make queries to your table.
It also has many popular features such as:
* **Query Builder** - build your queries with constructor, based on [doctrine/dbal](https://github.com/doctrine/dbal)
-* **Migrations** - based on [doctrine/migrations](https://github.com/doctrine/migrations)
-
-But there is 1 sacrifice for all these features - there is no support for relations in Composite DB. Its too much
-uncontrollable magic and hidden bottlenecks with "JOINs" and its not possible to implement automatic caching with
-relations. We recommend to have full control and make several cached select queries instead of "JOINs".
-
-### When you shouldn't use Composite DB
-
-1. If you have intricate structure with many foreign keys in your database
-2. You 100% sure in your indexes and fully trust "JOINs" performance
-3. You dont want to do extra cached select queries and want some magic
+* **Migrations** - synchronise your php entities with database tables
## Requirements
@@ -177,7 +167,7 @@ $user = User::fromArray([
]);
```
-And thats it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically.
+And that's it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically.
More about Entity and supported auto casting types you can find [here](doc/entity.md).
## License:
diff --git a/composer.json b/composer.json
index bc13c29..1adebb3 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "compositephp/db",
- "description": "PHP 8.1+ ORM and Table Gateway",
+ "description": "PHP 8.1+ DataMapper and Table Gateway",
"type": "library",
"license": "MIT",
"minimum-stability": "dev",
@@ -13,18 +13,16 @@
],
"require": {
"php": "^8.1",
+ "ext-pdo": "*",
"psr/simple-cache": "1 - 3",
- "compositephp/entity": "^0.1.2",
- "doctrine/dbal": "^3.5",
- "doctrine/inflector": "^2.0",
- "iamcal/sql-parser": "^0.4.0",
- "nette/php-generator": "^4.0",
- "symfony/console": "2 - 6"
+ "compositephp/entity": "^v0.1.11",
+ "doctrine/dbal": "^4.2"
},
"require-dev": {
"kodus/file-cache": "^2.0",
- "phpunit/phpunit": "^9.5",
- "phpstan/phpstan": "^1.9"
+ "phpunit/phpunit": "^10.1",
+ "phpstan/phpstan": "^1.9",
+ "phpunit/php-code-coverage": "^10.1"
},
"autoload": {
"psr-4": {
diff --git a/doc/cache.md b/doc/cache.md
index e3ca6ab..a5d6340 100644
--- a/doc/cache.md
+++ b/doc/cache.md
@@ -6,7 +6,7 @@ To start using auto-cache feature you need:
to `Composite\DB\AbstractCachedTable`
3. Implement method `getFlushCacheKeys()`
4. Change all internal select methods to their cached versions (example: `findByPkInternal()`
-to `findByPkCachedInternal()` etc.)
+to `_findByPkCached()` etc.)
You can also generate cached version of your table with console command:
@@ -46,7 +46,7 @@ class PostsTable extends AbstractCachedTable
public function findByPk(int $id): ?Post
{
- return $this->createEntity($this->findByPkInternalCached($id));
+ return $this->_findByPkCached($id);
}
/**
@@ -54,15 +54,12 @@ class PostsTable extends AbstractCachedTable
*/
public function findAllFeatured(): array
{
- return $this->createEntities($this->findAllInternal(
- 'is_featured = :is_featured',
- ['is_featured' => true],
- ));
+ return $this->_findAll(['is_featured' => true]);
}
public function countAllFeatured(): int
{
- return $this->countAllCachedInternal(
+ return $this->_countAllCached(
'is_featured = :is_featured',
['is_featured' => true],
);
diff --git a/doc/code-generators.md b/doc/code-generators.md
index 276f294..ce53614 100644
--- a/doc/code-generators.md
+++ b/doc/code-generators.md
@@ -1,27 +1,90 @@
# Code generators
-Before start, you need to [configure](configuration.md#configure-console-commands) code generators.
+Code generation is on of key features of the Composite Sync package.
+This enables you to generate Entity classes directly from SQL tables, thereby enabling a literal reflection of the SQL table schema into native PHP classes.
-## Entity class generator
-Arguments:
-1. `db` - DatabaseManager database name
-2. `table` - SQL table name
-3. `entity` - Full classname of new entity
-4. `--force` - option if existing file should be overwritten
+## Supported Databases
+- MySQL
+- Postgres
+- SQLite
+
+## Getting Started
+
+To begin using Composite Sync in your project, follow these steps:
+
+### 1. Install package via composer:
+ ```shell
+ $ composer require compositephp/sync
+ ```
+### 2. Configure connections
+You need to configure ConnectionManager, see instructions [here](configuration.md)
+
+### 3. Configure commands
+
+Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application:
+- Composite\Sync\Commands\MigrateCommand
+- Composite\Sync\Commands\MigrateNewCommand
+- Composite\Sync\Commands\MigrateDownCommand
+
+Here is an example of a minimalist, functional PHP file if you don't have configured symfony/console:
+
+```php
+addCommands([
+ new Commands\GenerateEntityCommand(),
+ new Commands\GenerateTableCommand(),
+]);
+$app->run();
+```
+## Available commands
+
+* ### composite:generate-entity
+
+The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class.
+This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format.
-Example:
```shell
-$ php console.php composite-db:generate-entity dbName Users 'App\User' --force
+php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName'
```
-## Table class generator
-Arguments:
-1. `entity` - Entity full class name
-2. `table` - Table full class name
-3. `--cached` - Option if cached version of table class should be generated
-4. `--force` - Option if existing file should be overwritten
+| Argument | Required | Description |
+|------------|----------|------------------------------------------------------|
+| connection | Yes | Name of connection from connection config file |
+| table | Yes | Name of SQL table |
+| entity | Yes | Full classname of the class that needs to be created |
+
+Options:
+
+| Option | Description |
+|---------|-------------------------|
+| --force | Overwrite existing file |
+
+* ### composite:generate-table
+
+The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class.
+This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat.
-Example:
```shell
-$ php console.php composite-db:generate-table 'App\User' 'App\UsersTable'
-```
\ No newline at end of file
+php cli.php composite:generate-table connection_name TableName 'App\Models\EntityName'
+```
+
+| Argument | Required | Description |
+|-----------|----------|-----------------------------------------------|
+| entity | Yes | Full Entity classname |
+| table | No | Full Table classname that needs to be created |
+
+Options:
+
+| Option | Description |
+|----------|--------------------------------------------|
+| --cached | Generate cached version of PHP Table class |
+| --force | Overwrite existing file |
\ No newline at end of file
diff --git a/doc/example.md b/doc/example.md
index 096ea6f..4709c67 100644
--- a/doc/example.md
+++ b/doc/example.md
@@ -43,7 +43,7 @@ class UsersTable extends \Composite\DB\AbstractTable
public function findByPk(int $id): ?User
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
/**
@@ -51,17 +51,13 @@ class UsersTable extends \Composite\DB\AbstractTable
*/
public function findAllActive(): array
{
- return $this->createEntities($this->findAllInternal(
- 'status = :status',
- ['status' => Status::ACTIVE->name],
- ));
+ return $this->_findAll(['status' => Status::ACTIVE]);
}
public function countAllActive(): int
{
- return $this->countAllInternal(
- 'status = :status',
- ['status' => Status::ACTIVE->name],
+ return $this->_countAll(
+ ['status' => Status::ACTIVE],
);
}
diff --git a/doc/migrations.md b/doc/migrations.md
index da0fe68..f772d25 100644
--- a/doc/migrations.md
+++ b/doc/migrations.md
@@ -2,96 +2,157 @@
> **_NOTE:_** This is experimental feature
-Migrations used a bridge to [doctrine/migrations](https://github.com/doctrine/migrations) package.
-If you are not familiar with it, please read documentation before using composite bridge.
-
-1. Install package:
- ```shell
- $ composer require compositephp/doctrine-migrations
- ```
-
-2. Configure bridge:
- ```php
- $bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge(
- entityDirs: [
- '/path/to/your/src', //path to your source code, where bridge will search for entities
- ],
- connectionName: 'sqlite', //only entities with this connection name will be affected
- connection: $connection, //Doctrine\DBAL\Connection instance
- );
- ```
-
-3. Inject bridge into `\Doctrine\Migrations\DependencyFactory` as `\Doctrine\Migrations\Provider\SchemaProvider`
-instance.
- ```php
- $dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge);
- ```
-
-Full example:
+Migrations enable you to maintain your database schema within your PHP entity classes.
+Any modification made in your class triggers the generation of migration files.
+These files execute SQL queries which synchronize the schema from the PHP class to the corresponding SQL table.
+This mechanism ensures consistent alignment between your codebase and the database structure.
+
+## Supported Databases
+- MySQL
+- Postgres (Coming soon)
+- SQLite (Coming soon)
+
+## Getting Started
+
+To begin using migrations you need to add Composite Sync package into your project and configure it:
+
+### 1. Install package via composer:
+ ```shell
+ $ composer require compositephp/sync
+ ```
+### 2. Configure connections
+You need to configure ConnectionManager, see instructions [here](configuration.md)
+
+### 3. Configure commands
+
+Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application:
+- Composite\Sync\Commands\MigrateCommand
+- Composite\Sync\Commands\MigrateNewCommand
+- Composite\Sync\Commands\MigrateDownCommand
+- Composite\Sync\Commands\GenerateEntityCommand
+- Composite\Sync\Commands\GenerateTableCommand
+
+Here is an example of a minimalist, functional PHP file:
+
```php
'pdo_mysql',
- 'dbname' => 'test',
- 'user' => 'test',
- 'password' => 'test',
- 'host' => '127.0.0.1',
+//may be changed with .env file
+putenv('CONNECTIONS_CONFIG_FILE=/path/to/your/connections/config.php');
+putenv('ENTITIES_DIR=/path/to/your/source/dir'); // e.g. "./src"
+putenv('MIGRATIONS_DIR=/path/to/your/migrations/dir'); // e.g. "./src/Migrations"
+putenv('MIGRATIONS_NAMESPACE=Migrations\Namespace'); // e.g. "App\Migrations"
+
+$app = new Application();
+$app->addCommands([
+ new Commands\MigrateCommand(),
+ new Commands\MigrateNewCommand(),
+ new Commands\MigrateDownCommand(),
+ new Commands\GenerateEntityCommand(),
+ new Commands\GenerateTableCommand(),
]);
+$app->run();
+```
+## Available commands
+
+* ### composite:migrate
+
+This command performs two primary functions depending on its usage context. Initially, when called for the first time,
+it scans all entities located in the `ENTITIES_DIR` directory and generates migration files corresponding to these entities.
+This initial step prepares the necessary migration scripts based on the current entity definitions. Upon its second
+invocation, the command shifts its role to apply these generated migration scripts to the database. This two-step process
+ensures that the database schema is synchronized with the entity definitions, first by preparing the migration scripts
+and then by executing them to update the database.
+
+```shell
+php cli.php composite:migrate
+```
+
+| Option | Short | Description |
+|--------------|-------|-----------------------------------------------------------|
+| --connection | -c | Check migrations for all entities with desired connection |
+| --entity | -e | Check migrations only for entity class |
+| --run | -r | Run migrations without asking for confirmation |
+| --dry | -d | Dry run mode, no real SQL queries will be executed |
+
+* ### composite:migrate-new
+
+This command generates a new, empty migration file. The file is provided as a template for the user to fill with the
+necessary database schema changes or updates. This command is typically used for initiating a new database migration,
+where the user can define the specific changes to be applied to the database schema. The generated file needs to be
+manually edited to include the desired migration logic before it can be executed with the migration commands.
+
+```shell
+php cli.php composite:migrate-new
+```
+
+| Argument | Required | Description |
+|-------------|----------|------------------------------------------|
+| connection | No | Name of connection from your config file |
+| description | No | Short description of desired changes |
+
+* ### composite:migrate-down
+
+This command rolls back the most recently applied migration. It is useful for undoing the last schema change made to
+the database. This can be particularly helpful during development or testing phases, where you might need to revert
+recent changes quickly.
+
+```shell
+php cli.php composite:migrate-down
+```
+
+| Argument | Required | Description |
+|------------|----------|---------------------------------------------------------------------------|
+| connection | No | Name of connection from your config file |
+| limit | No | Number of migrations should be rolled back from current state, default: 1 |
+
+
+| Option | Short | Description |
+|--------|-------|-----------------------------------------------------|
+| --dry | -d | Dry run mode, no real SQL queries will be executed |
+
+* ### composite:generate-entity
+
+The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class.
+This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format.
+
+```shell
+php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName'
+```
+
+| Argument | Required | Description |
+|------------|----------|------------------------------------------------------|
+| connection | Yes | Name of connection from connection config file |
+| table | Yes | Name of SQL table |
+| entity | Yes | Full classname of the class that needs to be created |
+
+Options:
+
+| Option | Short | Description |
+|---------|-------|-------------------------|
+| --force | -f | Overwrite existing file |
+
+* ### composite:generate-table
+
+The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class.
+This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat.
+
+```shell
+php cli.php composite:generate-table 'App\Models\EntityName'
+```
+
+| Argument | Required | Description |
+|-----------|----------|-----------------------------------------------|
+| entity | Yes | Full Entity classname |
+| table | No | Full Table classname that needs to be created |
+
+Options:
-$configuration = new Configuration();
-
-$configuration->addMigrationsDirectory('Composite\DoctrineMigrations\Tests\runtime\migrations', __DIR__ . '/tests/runtime/migrations');
-$configuration->setAllOrNothing(true);
-$configuration->setCheckDatabasePlatform(false);
-
-$storageConfiguration = new TableMetadataStorageConfiguration();
-$storageConfiguration->setTableName('doctrine_migration_versions');
-
-$configuration->setMetadataStorageConfiguration($storageConfiguration);
-
-$dependencyFactory = DependencyFactory::fromConnection(
- new ExistingConfiguration($configuration),
- new ExistingConnection($connection)
-);
-
-$bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge(
- entityDirs: [
- __DIR__ . '/src',
- ],
- connectionName: 'mysql',
- connection: $connection,
-);
-$dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge);
-
-$cli = new Application('Migrations');
-$cli->setCatchExceptions(true);
-
-$cli->addCommands(array(
- new Command\DumpSchemaCommand($dependencyFactory),
- new Command\ExecuteCommand($dependencyFactory),
- new Command\GenerateCommand($dependencyFactory),
- new Command\LatestCommand($dependencyFactory),
- new Command\ListCommand($dependencyFactory),
- new Command\MigrateCommand($dependencyFactory),
- new Command\DiffCommand($dependencyFactory),
- new Command\RollupCommand($dependencyFactory),
- new Command\StatusCommand($dependencyFactory),
- new Command\SyncMetadataCommand($dependencyFactory),
- new Command\VersionCommand($dependencyFactory),
-));
-
-$cli->run();
-```
\ No newline at end of file
+| Option | Short | Description |
+|----------|-------|--------------------------------------------|
+| --cached | -c | Generate cached version of PHP Table class |
+| --force | -f | Overwrite existing file |
\ No newline at end of file
diff --git a/doc/sync_illustration.png b/doc/sync_illustration.png
new file mode 100644
index 0000000..94c451a
Binary files /dev/null and b/doc/sync_illustration.png differ
diff --git a/doc/table.md b/doc/table.md
index 326f095..852c55b 100644
--- a/doc/table.md
+++ b/doc/table.md
@@ -38,7 +38,7 @@ class UsersTable extends AbstractTable
public function findOne(int $id): ?User
{
- return $this->createEntity($this->findOneInternal($id));
+ return $this->_findByPk($id);
}
/**
@@ -46,12 +46,12 @@ class UsersTable extends AbstractTable
*/
public function findAll(): array
{
- return $this->createEntities($this->findAllInternal());
+ return $this->_findAll();
}
public function countAll(): int
{
- return $this->countAllInternal();
+ return $this->_countAll();
}
}
```
@@ -67,15 +67,30 @@ Example with internal helper:
*/
public function findAllActiveAdults(): array
{
- $rows = $this->findAllInternal(
- 'age > :age AND status = :status',
- ['age' => 18, 'status' => Status::ACTIVE->name],
+ return $this->_findAll(
+ new Where(
+ 'age > :age AND status = :status',
+ ['age' => 18, 'status' => Status::ACTIVE->name],
+ )
);
- return $this->createEntities($rows);
}
```
-Example with pure query builder
+Or it might be simplified to:
+```php
+/**
+ * @return User[]
+ */
+public function findAllActiveAdults(): array
+{
+ return $this->_findAll([
+ 'age' => ['>', 18],
+ 'status' => Status:ACTIVE,
+ ]);
+}
+```
+
+Or you can use standard Doctrine QueryBuilder
```php
/**
* @return User[]
@@ -93,27 +108,36 @@ public function findCustom(): array
```
## Transactions
+In order to encapsulate your operations within a single transaction, you have two strategies at your disposal:
+1. Use the internal table class method transaction() if your operations are confined to a single table.
+2. Use the Composite\DB\CombinedTransaction class if your operations involve multiple tables within a single transaction.
-To wrap you operations in 1 transactions there are 2 ways:
-1. Use internal table class method `transaction()` if you are working only with 1 table.
-2. Use class `Composite\DB\CombinedTransaction` if you need to work with several tables in 1 transaction.
+Below is a sample code snippet illustrating how you can use the CombinedTransaction class:
```php
+ // Create instances of the tables you want to work with
$usersTable = new UsersTable();
$photosTable = new PhotosTable();
-
+
+ // Instantiate the CombinedTransaction class
$transaction = new CombinedTransaction();
+ // Create a new user and add it to the users table within the transaction
$user = new User(...);
$transaction->save($usersTable, $user);
+ // Create a new photo associated with the user and add it to the photos table within the transaction
$photo = new Photo(
user_id: $user->id,
...
);
$transaction->save($photosTable, $photo);
+
+ // Commit the transaction to finalize the changes
$transaction->commit();
```
+
+Remember, using a transaction ensures that your operations are atomic. This means that either all changes are committed to the database, or if an error occurs, no changes are made.
## Locks
If you worry about concurrency updates during your transaction and want to be sure that only 1 process changing your
diff --git a/phpunit.xml b/phpunit.xml
index 23ced23..1389feb 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,6 +1,27 @@
-
-
\ No newline at end of file
+
+
+
+
+ tests
+
+
+
+
+
+
+
+
+
+ src
+
+
+
diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php
index 9d3064e..24ff5f9 100644
--- a/src/AbstractCachedTable.php
+++ b/src/AbstractCachedTable.php
@@ -2,12 +2,14 @@
namespace Composite\DB;
-use Composite\DB\Exceptions\DbException;
use Composite\Entity\AbstractEntity;
use Psr\SimpleCache\CacheInterface;
+use Ramsey\Uuid\UuidInterface;
abstract class AbstractCachedTable extends AbstractTable
{
+ use Helpers\SelectRawTrait;
+
protected const CACHE_VERSION = 1;
public function __construct(
@@ -24,72 +26,58 @@ abstract protected function getFlushCacheKeys(AbstractEntity $entity): array;
/**
* @throws \Throwable
*/
- public function save(AbstractEntity &$entity): void
+ public function save(AbstractEntity $entity): void
{
- $this->getConnection()->transactional(function () use (&$entity) {
- $cacheKeys = $this->collectCacheKeysByEntity($entity);
- parent::save($entity);
- if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
- throw new DbException('Failed to flush cache keys');
- }
- });
+ $cacheKeys = $this->collectCacheKeysByEntity($entity);
+ parent::save($entity);
+ if ($cacheKeys) {
+ $this->cache->deleteMultiple($cacheKeys);
+ }
}
/**
* @param AbstractEntity[] $entities
- * @return AbstractEntity[]
* @throws \Throwable
*/
- public function saveMany(array $entities): array
+ public function saveMany(array $entities): void
{
- return $this->getConnection()->transactional(function() use ($entities) {
- $cacheKeys = [];
- foreach ($entities as $entity) {
- $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
- }
- foreach ($entities as $entity) {
- parent::save($entity);
- }
- if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
- throw new DbException('Failed to flush cache keys');
- }
- return $entities;
- });
+ $cacheKeys = [];
+ foreach ($entities as $entity) {
+ $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
+ }
+ parent::saveMany($entities);
+ if ($cacheKeys) {
+ $this->cache->deleteMultiple(array_unique($cacheKeys));
+ }
}
/**
* @throws \Throwable
*/
- public function delete(AbstractEntity &$entity): void
+ public function delete(AbstractEntity $entity): void
{
- $this->getConnection()->transactional(function () use (&$entity) {
- $cacheKeys = $this->collectCacheKeysByEntity($entity);
- parent::delete($entity);
- if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
- throw new DbException('Failed to flush cache keys');
- }
- });
+ $cacheKeys = $this->collectCacheKeysByEntity($entity);
+ parent::delete($entity);
+ if ($cacheKeys) {
+ $this->cache->deleteMultiple($cacheKeys);
+ }
}
/**
* @param AbstractEntity[] $entities
* @throws \Throwable
*/
- public function deleteMany(array $entities): bool
+ public function deleteMany(array $entities): void
{
- return $this->getConnection()->transactional(function() use ($entities) {
- $cacheKeys = [];
- foreach ($entities as $entity) {
- $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
- }
- foreach ($entities as $entity) {
- parent::delete($entity);
- }
- if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) {
- throw new DbException('Failed to flush cache keys');
- }
- return true;
- });
+ $cacheKeys = [];
+ foreach ($entities as $entity) {
+ $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity));
+ parent::delete($entity);
+ }
+ parent::deleteMany($entities);
+ if ($cacheKeys) {
+ $this->cache->deleteMultiple($cacheKeys);
+ }
}
/**
@@ -102,62 +90,64 @@ private function collectCacheKeysByEntity(AbstractEntity $entity): array
if (!$entity->isNew() || !$this->getConfig()->autoIncrementKey) {
$keys[] = $this->getOneCacheKey($entity);
}
- return $keys;
+ return array_unique($keys);
}
/**
- * @return array|null
+ * @return AbstractEntity|null
*/
- protected function findByPkCachedInternal(mixed $pk, null|int|\DateInterval $ttl = null): ?array
+ protected function _findByPkCached(mixed $pk, null|int|\DateInterval $ttl = null): mixed
{
- return $this->findOneCachedInternal($this->getPkCondition($pk), $ttl);
+ return $this->_findOneCached($this->getPkCondition($pk), $ttl);
}
/**
- * @param array $condition
+ * @param array $where
* @param int|\DateInterval|null $ttl
- * @return array|null
+ * @return AbstractEntity|null
*/
- protected function findOneCachedInternal(array $condition, null|int|\DateInterval $ttl = null): ?array
+ protected function _findOneCached(array $where, null|int|\DateInterval $ttl = null): mixed
{
- return $this->getCached(
- $this->getOneCacheKey($condition),
- fn() => $this->findOneInternal($condition),
+ $row = $this->getCached(
+ $this->getOneCacheKey($where),
+ fn() => $this->_findOneRaw($where),
$ttl,
- ) ?: null;
+ );
+ return $this->createEntity($row);
}
/**
+ * @param array|Where $where
* @param array|string $orderBy
- * @return array[]
+ * @return array|array
*/
- protected function findAllCachedInternal(
- string $whereString = '',
- array $whereParams = [],
+ protected function _findAllCached(
+ array|Where $where = [],
array|string $orderBy = [],
?int $limit = null,
null|int|\DateInterval $ttl = null,
+ ?string $keyColumnName = null,
): array
{
- return $this->getCached(
- $this->getListCacheKey($whereString, $whereParams, $orderBy, $limit),
- fn() => $this->findAllInternal(whereString: $whereString, whereParams: $whereParams, orderBy: $orderBy, limit: $limit),
+ $rows = $this->getCached(
+ $this->getListCacheKey($where, $orderBy, $limit),
+ fn() => $this->_findAllRaw(where: $where, orderBy: $orderBy, limit: $limit),
$ttl,
);
+ return $this->createEntities($rows, $keyColumnName);
}
/**
- * @param array $whereParams
+ * @param array|Where $where
*/
- protected function countAllCachedInternal(
- string $whereString = '',
- array $whereParams = [],
+ protected function _countByAllCached(
+ array|Where $where = [],
null|int|\DateInterval $ttl = null,
): int
{
return (int)$this->getCached(
- $this->getCountCacheKey($whereString, $whereParams),
- fn() => $this->countAllInternal(whereString: $whereString, whereParams: $whereParams),
+ $this->getCountCacheKey($where),
+ fn() => $this->_countAll(where: $where),
$ttl,
);
}
@@ -175,7 +165,17 @@ protected function getCached(string $cacheKey, callable $dataCallback, null|int|
return $data;
}
- protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $ttl = null): array
+ /**
+ * @param mixed[] $ids
+ * @param int|\DateInterval|null $ttl
+ * @return array|array
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ */
+ protected function _findMultiCached(
+ array $ids,
+ null|int|\DateInterval $ttl = null,
+ ?string $keyColumnName = null,
+ ): array
{
$result = $cacheKeys = $foundIds = [];
foreach ($ids as $id) {
@@ -184,22 +184,27 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t
}
$cache = $this->cache->getMultiple(array_keys($cacheKeys));
foreach ($cache as $cacheKey => $cachedRow) {
- $result[] = $cachedRow;
- if (empty($cacheKeys[$cacheKey])) {
+ if ($cachedRow === null) {
continue;
}
- $foundIds[] = $cacheKeys[$cacheKey];
+ if (isset($cacheKeys[$cacheKey])) {
+ $result[] = $cachedRow;
+ $foundIds[] = $cacheKeys[$cacheKey];
+ }
}
$ids = array_diff($ids, $foundIds);
foreach ($ids as $id) {
- if ($row = $this->findOneCachedInternal($id, $ttl)) {
+ if ($row = $this->_findByPkCached($id, $ttl)) {
$result[] = $row;
}
}
- return $result;
+ return $this->createEntities($result, $keyColumnName);
}
- protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string
+ /**
+ * @param string|int|array|AbstractEntity $keyOrEntity
+ */
+ protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface $keyOrEntity): string
{
if (!is_array($keyOrEntity)) {
$condition = $this->getPkCondition($keyOrEntity);
@@ -209,31 +214,36 @@ protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity):
return $this->buildCacheKey('o', $condition ?: 'one');
}
+ /**
+ * @param array|Where $where
+ * @param array|string $orderBy
+ */
protected function getListCacheKey(
- string $whereString = '',
- array $whereParams = [],
+ array|Where $where = [],
array|string $orderBy = [],
?int $limit = null
): string
{
- $wherePart = $this->prepareWhereKey($whereString, $whereParams);
+ $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where);
return $this->buildCacheKey(
'l',
- $wherePart ?? 'all',
+ $wherePart ?: 'all',
$orderBy ? ['ob' => $orderBy] : null,
$limit ? ['limit' => $limit] : null,
);
}
+ /**
+ * @param array|Where $where
+ */
protected function getCountCacheKey(
- string $whereString = '',
- array $whereParams = [],
+ array|Where $where = [],
): string
{
- $wherePart = $this->prepareWhereKey($whereString, $whereParams);
+ $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where);
return $this->buildCacheKey(
'c',
- $wherePart ?? 'all',
+ $wherePart ?: 'all',
);
}
@@ -244,7 +254,7 @@ protected function buildCacheKey(mixed ...$parts): string
$formattedParts = [];
foreach ($parts as $part) {
if (is_array($part)) {
- $string = json_encode($part);
+ $string = json_encode($part, JSON_THROW_ON_ERROR);
} else {
$string = strval($part);
}
@@ -268,24 +278,18 @@ protected function buildCacheKey(mixed ...$parts): string
private function formatStringForCacheKey(string $string): string
{
- $string = mb_strtolower($string);
+ $string = strtolower($string);
$string = str_replace(['!=', '<>', '>', '<', '='], ['_not_', '_not_', '_gt_', '_lt_', '_eq_'], $string);
- $string = preg_replace('/\W/', '_', $string);
- return trim(preg_replace('/_+/', '_', $string), '_');
+ $string = (string)preg_replace('/\W/', '_', $string);
+ return trim((string)preg_replace('/_+/', '_', $string), '_');
}
- private function prepareWhereKey(string $whereString, array $whereParams): ?string
+ private function prepareWhereKey(Where $where): string
{
- if (!$whereString) {
- return null;
- }
- if (!$whereParams) {
- return $whereString;
- }
return str_replace(
- array_map(fn (string $key): string => ':' . $key, array_keys($whereParams)),
- array_values($whereParams),
- $whereString,
+ array_map(fn (string $key): string => ':' . $key, array_keys($where->params)),
+ array_values($where->params),
+ $where->condition,
);
}
}
diff --git a/src/AbstractTable.php b/src/AbstractTable.php
index 4d465f1..b2536a7 100644
--- a/src/AbstractTable.php
+++ b/src/AbstractTable.php
@@ -2,16 +2,23 @@
namespace Composite\DB;
-use Composite\Entity\AbstractEntity;
use Composite\DB\Exceptions\DbException;
-use Composite\Entity\Exceptions\EntityException;
+use Composite\DB\MultiQuery\MultiInsert;
+use Composite\DB\MultiQuery\MultiSelect;
+use Composite\Entity\AbstractEntity;
+use Composite\Entity\Columns;
+use Composite\Entity\Helpers\DateTimeHelper;
use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Query\QueryBuilder;
+use Doctrine\DBAL\ParameterType;
+use Ramsey\Uuid\UuidInterface;
abstract class AbstractTable
{
+ use Helpers\SelectRawTrait;
+ use Helpers\DatabaseSpecificTrait;
+
protected readonly TableConfig $config;
- private ?QueryBuilder $selectQuery = null;
+
abstract protected function getConfig(): TableConfig;
@@ -40,167 +47,257 @@ public function getConnectionName(): string
* @return void
* @throws \Throwable
*/
- public function save(AbstractEntity &$entity): void
+ public function save(AbstractEntity $entity): void
{
$this->config->checkEntity($entity);
if ($entity->isNew()) {
$connection = $this->getConnection();
+ $this->checkUpdatedAt($entity);
+
$insertData = $entity->toArray();
- $this->getConnection()->insert($this->getTableName(), $insertData);
+ $preparedInsertData = $this->prepareDataForSql($insertData);
+ $this->getConnection()->insert(
+ table: $this->getTableName(),
+ data: $preparedInsertData,
+ types: $this->getDoctrineTypes($insertData),
+ );
- if ($this->config->autoIncrementKey) {
- $insertData[$this->config->autoIncrementKey] = intval($connection->lastInsertId());
- $entity = $entity::fromArray($insertData);
- } else {
- $entity->resetChangedColumns();
+ if ($this->config->autoIncrementKey && ($lastInsertedId = $connection->lastInsertId())) {
+ $insertData[$this->config->autoIncrementKey] = intval($lastInsertedId);
+ $entity::schema()
+ ->getColumn($this->config->autoIncrementKey)
+ ->setValue($entity, $insertData[$this->config->autoIncrementKey]);
}
+ $entity->resetChangedColumns($insertData);
} else {
if (!$changedColumns = $entity->getChangedColumns()) {
return;
}
- $connection = $this->getConnection();
- $where = $this->getPkCondition($entity);
- $this->enrichCondition($where);
+ if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) {
+ $entity->updated_at = new \DateTimeImmutable();
+ $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at);
+ }
+ $whereParams = $this->getPkCondition($entity);
+ if ($this->config->hasOptimisticLock()
+ && method_exists($entity, 'getVersion')
+ && method_exists($entity, 'incrementVersion')) {
+ $whereParams['lock_version'] = $entity->getVersion();
+ $entity->incrementVersion();
+ $changedColumns['lock_version'] = $entity->getVersion();
+ }
+ $updateString = implode(', ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($changedColumns)));
+ $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
+ $preparedParams = array_merge(
+ array_values($this->prepareDataForSql($changedColumns)),
+ array_values($this->prepareDataForSql($whereParams)),
+ );
+ $types = array_merge(
+ $this->getDoctrineTypes($changedColumns),
+ $this->getDoctrineTypes($whereParams),
+ );
- if ($this->config->isOptimisticLock && isset($entity->version)) {
- $currentVersion = $entity->version;
- try {
- $connection->beginTransaction();
- $connection->update(
- $this->getTableName(),
- $changedColumns,
- $where
- );
- $versionUpdated = $connection->update(
- $this->getTableName(),
- ['version' => $currentVersion + 1],
- $where + ['version' => $currentVersion]
- );
- if (!$versionUpdated) {
- throw new DbException('Failed to update entity version, concurrency modification, rolling back.');
- }
- $connection->commit();
- } catch (\Throwable $e) {
- $connection->rollBack();
- throw $e;
- }
+ $entityUpdated = (bool)$this->getConnection()->executeStatement(
+ sql: "UPDATE " . $this->escapeIdentifier($this->getTableName()) . " SET $updateString WHERE $whereString;",
+ params: $preparedParams,
+ types: $types,
+ );
+ if ($this->config->hasOptimisticLock() && !$entityUpdated) {
+ throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.');
+ }
+ $entity->resetChangedColumns($changedColumns);
+ }
+ }
+
+ private function getDoctrineTypes(array $data): array
+ {
+ $result = [];
+ foreach ($data as $value) {
+ if (is_bool($value)) {
+ $result[] = ParameterType::BOOLEAN;
+ } elseif (is_int($value)) {
+ $result[] = ParameterType::INTEGER;
+ } elseif (is_null($value)) {
+ $result[] = ParameterType::NULL;
} else {
- $connection->update(
- $this->getTableName(),
- $changedColumns,
- $where
- );
+ $result[] = ParameterType::STRING;
}
- $entity->resetChangedColumns();
}
+ return $result;
}
/**
* @param AbstractEntity[] $entities
- * @return AbstractEntity[] $entities
* @throws \Throwable
*/
- public function saveMany(array $entities): array
+ public function saveMany(array $entities): void
{
- return $this->getConnection()->transactional(function() use ($entities) {
+ $rowsToInsert = [];
+ foreach ($entities as $i => $entity) {
+ if ($entity->isNew()) {
+ $this->config->checkEntity($entity);
+ $this->checkUpdatedAt($entity);
+ $rowsToInsert[] = $this->prepareDataForSql($entity->toArray());
+ unset($entities[$i]);
+ }
+ }
+ $connection = $this->getConnection();
+ $connection->beginTransaction();
+ try {
foreach ($entities as $entity) {
$this->save($entity);
}
- return $entities;
- });
+ if ($rowsToInsert) {
+ $chunks = array_chunk($rowsToInsert, 1000);
+ $connection = $this->getConnection();
+ foreach ($chunks as $chunk) {
+ $multiInsert = new MultiInsert(
+ connection: $connection,
+ tableName: $this->getTableName(),
+ rows: $chunk,
+ );
+ if ($multiInsert->getSql()) {
+ $connection->executeStatement(
+ sql: $multiInsert->getSql(),
+ params: $multiInsert->getParameters(),
+ types: $this->getDoctrineTypes(array_keys($chunk[0])),
+ );
+ }
+ }
+ }
+ $connection->commit();
+ } catch (\Throwable $e) {
+ $connection->rollBack();
+ throw $e;
+ }
}
/**
- * @throws EntityException
+ * @param AbstractEntity $entity
+ * @throws \Throwable
*/
- public function delete(AbstractEntity &$entity): void
+ public function delete(AbstractEntity $entity): void
{
$this->config->checkEntity($entity);
- if ($this->config->isSoftDelete) {
+ if ($this->config->hasSoftDelete()) {
if (method_exists($entity, 'delete')) {
$entity->delete();
$this->save($entity);
}
} else {
- $where = $this->getPkCondition($entity);
- $this->enrichCondition($where);
- $this->getConnection()->delete($this->getTableName(), $where);
+ $whereParams = $this->getPkCondition($entity);
+ $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
+ $this->getConnection()->executeQuery(
+ sql: "DELETE FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
+ params: array_values($whereParams),
+ );
}
}
/**
* @param AbstractEntity[] $entities
+ * @throws \Throwable
*/
- public function deleteMany(array $entities): bool
+ public function deleteMany(array $entities): void
{
- return $this->getConnection()->transactional(function() use ($entities) {
+ $connection = $this->getConnection();
+ $connection->beginTransaction();
+ try {
foreach ($entities as $entity) {
$this->delete($entity);
}
- return true;
- });
+ $connection->commit();
+ } catch (\Throwable $e) {
+ $connection->rollBack();
+ throw $e;
+ }
}
- protected function countAllInternal(string $whereString = '', array $whereParams = []): int
+ /**
+ * @param array|Where $where
+ * @throws \Doctrine\DBAL\Exception
+ */
+ protected function _countAll(array|Where $where = []): int
{
$query = $this->select('COUNT(*)');
- if ($whereString) {
- $query->where($whereString);
- foreach ($whereParams as $param => $value) {
+ if (is_array($where)) {
+ $this->buildWhere($query, $where);
+ } else {
+ $query->where($where->condition);
+ foreach ($where->params as $param => $value) {
$query->setParameter($param, $value);
}
}
- $this->enrichCondition($query);
return intval($query->executeQuery()->fetchOne());
}
- protected function findByPkInternal(mixed $pk): ?array
+ /**
+ * @throws \Doctrine\DBAL\Exception
+ * @return AbstractEntity|null
+ */
+ protected function _findByPk(mixed $pk): mixed
+ {
+ $whereParams = $this->getPkCondition($pk);
+ $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams)));
+ $row = $this->getConnection()
+ ->executeQuery(
+ sql: "SELECT * FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;",
+ params: array_values($whereParams),
+ )
+ ->fetchAssociative();
+ return $this->createEntity($row);
+ }
+
+ /**
+ * @param array|Where $where
+ * @param array|string $orderBy
+ * @return AbstractEntity|null
+ * @throws \Doctrine\DBAL\Exception
+ */
+ protected function _findOne(array|Where $where, array|string $orderBy = []): mixed
{
- $where = $this->getPkCondition($pk);
- return $this->findOneInternal($where);
+ return $this->createEntity($this->_findOneRaw($where, $orderBy));
}
- protected function findOneInternal(array $where): ?array
+ /**
+ * @param array> $pkList
+ * @return array| array
+ * @throws DbException
+ * @throws \Doctrine\DBAL\Exception
+ */
+ protected function _findMulti(array $pkList, ?string $keyColumnName = null): array
{
- $query = $this->select();
- $this->enrichCondition($where);
- $this->buildWhere($query, $where);
- return $query->fetchAssociative() ?: null;
+ if (!$pkList) {
+ return [];
+ }
+ $multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList);
+ return $this->createEntities(
+ $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative(),
+ $keyColumnName,
+ );
}
- protected function findAllInternal(
- string $whereString = '',
- array $whereParams = [],
+ /**
+ * @param array|Where $where
+ * @param array|string $orderBy
+ * @return array| array
+ */
+ protected function _findAll(
+ array|Where $where = [],
array|string $orderBy = [],
?int $limit = null,
?int $offset = null,
+ ?string $keyColumnName = null,
): array
{
- $query = $this->select();
- if ($whereString) {
- $query->where($whereString);
- foreach ($whereParams as $param => $value) {
- $query->setParameter($param, $value);
- }
- }
- $this->enrichCondition($query);
-
- if ($orderBy) {
- if (is_array($orderBy)) {
- foreach ($orderBy as $column => $direction) {
- $query->addOrderBy($column, $direction);
- }
- } else {
- $query->orderBy($orderBy);
- }
- }
- if ($limit > 0) {
- $query->setMaxResults($limit);
- }
- if ($offset > 0) {
- $query->setFirstResult($offset);
- }
- return $query->executeQuery()->fetchAllAssociative();
+ return $this->createEntities(
+ data: $this->_findAllRaw(
+ where: $where,
+ orderBy: $orderBy,
+ limit: $limit,
+ offset: $offset,
+ ),
+ keyColumnName: $keyColumnName,
+ );
}
final protected function createEntity(mixed $data): mixed
@@ -209,7 +306,7 @@ final protected function createEntity(mixed $data): mixed
return null;
}
try {
- /** @psalm-var class-string $entityClass */
+ /** @var class-string $entityClass */
$entityClass = $this->config->entityClass;
return $entityClass::fromArray($data);
} catch (\Throwable) {
@@ -217,7 +314,10 @@ final protected function createEntity(mixed $data): mixed
}
}
- final protected function createEntities(mixed $data): array
+ /**
+ * @return AbstractEntity[]
+ */
+ final protected function createEntities(mixed $data, ?string $keyColumnName = null): array
{
if (!is_array($data)) {
return [];
@@ -227,10 +327,19 @@ final protected function createEntities(mixed $data): array
$entityClass = $this->config->entityClass;
$result = [];
foreach ($data as $datum) {
- if (!is_array($datum)) {
- continue;
+ if (is_array($datum)) {
+ if ($keyColumnName && isset($datum[$keyColumnName])) {
+ $result[$datum[$keyColumnName]] = $entityClass::fromArray($datum);
+ } else {
+ $result[] = $entityClass::fromArray($datum);
+ }
+ } elseif ($datum instanceof $this->config->entityClass) {
+ if ($keyColumnName && property_exists($datum, $keyColumnName)) {
+ $result[$datum->{$keyColumnName}] = $datum;
+ } else {
+ $result[] = $datum;
+ }
}
- $result[] = $entityClass::fromArray($datum);
}
} catch (\Throwable) {
return [];
@@ -238,11 +347,25 @@ final protected function createEntities(mixed $data): array
return $result;
}
- protected function getPkCondition(int|string|array|AbstractEntity $data): array
+ /**
+ * @param int|string|array|AbstractEntity $data
+ * @return array
+ */
+ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array
{
+ if (empty($this->config->primaryKeys)) {
+ throw new \Exception("Primary keys are not defined in `" . $this::class . "` table config");
+ }
$condition = [];
if ($data instanceof AbstractEntity) {
- $data = $data->toArray();
+ if ($data->isNew()) {
+ $data = $data->toArray();
+ } else {
+ foreach ($this->config->primaryKeys as $key) {
+ $condition[$key] = $data->getOldValue($key);
+ }
+ return $condition;
+ }
}
if (is_array($data)) {
foreach ($this->config->primaryKeys as $key) {
@@ -256,36 +379,10 @@ protected function getPkCondition(int|string|array|AbstractEntity $data): array
return $condition;
}
- protected function enrichCondition(array|QueryBuilder &$query): void
- {
- if ($this->config->isSoftDelete) {
- if ($query instanceof QueryBuilder) {
- $query->andWhere('deleted_at IS NULL');
- } else {
- if (!isset($query['deleted_at'])) {
- $query['deleted_at'] = null;
- }
- }
- }
- }
-
- protected function select(string $select = '*'): QueryBuilder
- {
- if ($this->selectQuery === null) {
- $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName());
- }
- return (clone $this->selectQuery)->select($select);
- }
-
- private function buildWhere(QueryBuilder $query, array $where): void
+ private function checkUpdatedAt(AbstractEntity $entity): void
{
- foreach ($where as $column => $value) {
- if ($value === null) {
- $query->andWhere("$column IS NULL");
- } else {
- $query->andWhere("$column = :" . $column);
- $query->setParameter($column, $value);
- }
+ if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) {
+ $entity->updated_at = new \DateTimeImmutable();
}
}
}
diff --git a/src/Attributes/Column.php b/src/Attributes/Column.php
deleted file mode 100644
index 11ac3ca..0000000
--- a/src/Attributes/Column.php
+++ /dev/null
@@ -1,15 +0,0 @@
-getConnectionName();
- if (empty($this->transactions[$connectionName])) {
- $connection = ConnectionManager::getConnection($connectionName);
- $connection->beginTransaction();
- $this->transactions[$connectionName] = $connection;
- }
- $table->save($entity);
- } catch (\Throwable $e) {
- $this->rollback();
- throw new Exceptions\DbException($e->getMessage(), 500, $e);
+ if (!$entity->isNew() && !$entity->getChangedColumns()) {
+ return;
}
+ $this->doInTransaction($table, fn() => $table->save($entity));
+ }
+
+ /**
+ * @param AbstractTable $table
+ * @param AbstractEntity[] $entities
+ * @throws DbException
+ */
+ public function saveMany(AbstractTable $table, array $entities): void
+ {
+ if (!$entities) {
+ return;
+ }
+ $this->doInTransaction($table, fn () => $table->saveMany($entities));
+ }
+
+ /**
+ * @param AbstractTable $table
+ * @param AbstractEntity[] $entities
+ * @throws DbException
+ */
+ public function deleteMany(AbstractTable $table, array $entities): void
+ {
+ if (!$entities) {
+ return;
+ }
+ $this->doInTransaction($table, fn () => $table->deleteMany($entities));
+ }
+
+ /**
+ * @throws Exceptions\DbException
+ */
+ public function delete(AbstractTable $table, AbstractEntity $entity): void
+ {
+ $this->doInTransaction($table, fn () => $table->delete($entity));
}
/**
* @throws Exceptions\DbException
*/
- public function delete(AbstractTable $table, AbstractEntity &$entity): void
+ public function try(callable $callback): void
{
try {
- $connectionName = $table->getConnectionName();
- if (empty($this->transactions[$connectionName])) {
- $connection = ConnectionManager::getConnection($connectionName);
- $connection->beginTransaction();
- $this->transactions[$connectionName] = $connection;
- }
- $table->delete($entity);
+ $callback();
} catch (\Throwable $e) {
$this->rollback();
throw new Exceptions\DbException($e->getMessage(), 500, $e);
@@ -67,37 +87,33 @@ public function commit(): void
{
foreach ($this->transactions as $connectionName => $connection) {
try {
- if (!$connection->commit()) {
- throw new Exceptions\DbException("Could not commit transaction for database `$connectionName`");
- }
+ $connection->commit();
+ // I have no idea how to simulate failed commit
+ // @codeCoverageIgnoreStart
} catch (\Throwable $e) {
$this->rollback();
throw new Exceptions\DbException($e->getMessage(), 500, $e);
}
+ // @codeCoverageIgnoreEnd
}
$this->finish();
}
/**
* Pessimistic lock
+ * @param string[] $keyParts
* @throws DbException
+ * @throws InvalidArgumentException
*/
public function lock(CacheInterface $cache, array $keyParts, int $duration = 10): void
{
$this->cache = $cache;
- $this->lockKey = implode('.', array_merge(['composite', 'lock'], $keyParts));
- if (strlen($this->lockKey) > 64) {
- $this->lockKey = sha1($this->lockKey);
+ $this->lockKey = $this->buildLockKey($keyParts);
+ if ($this->cache->get($this->lockKey)) {
+ throw new DbException("Failed to get lock `{$this->lockKey}`");
}
- try {
- if ($this->cache->get($this->lockKey)) {
- throw new DbException("Failed to get lock `{$this->lockKey}`");
- }
- if (!$this->cache->set($this->lockKey, 1, $duration)) {
- throw new DbException("Failed to save lock `{$this->lockKey}`");
- }
- } catch (InvalidArgumentException) {
- throw new DbException("Lock key is invalid `{$this->lockKey}`");
+ if (!$this->cache->set($this->lockKey, 1, $duration)) {
+ throw new DbException("Failed to save lock `{$this->lockKey}`");
}
}
@@ -106,9 +122,41 @@ public function releaseLock(): void
if (!$this->cache || !$this->lockKey) {
return;
}
+ if (!$this->cache->delete($this->lockKey)) {
+ // @codeCoverageIgnoreStart
+ throw new DbException("Failed to release lock `{$this->lockKey}`");
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ private function doInTransaction(AbstractTable $table, callable $callback): void
+ {
try {
- $this->cache->delete($this->lockKey);
- } catch (InvalidArgumentException) {}
+ $connectionName = $table->getConnectionName();
+ if (empty($this->transactions[$connectionName])) {
+ $connection = ConnectionManager::getConnection($connectionName);
+ $connection->beginTransaction();
+ $this->transactions[$connectionName] = $connection;
+ }
+ $callback();
+ } catch (\Throwable $e) {
+ $this->rollback();
+ throw new Exceptions\DbException($e->getMessage(), 500, $e);
+ }
+ }
+
+ /**
+ * @param string[] $keyParts
+ * @return string
+ */
+ private function buildLockKey(array $keyParts): string
+ {
+ $keyParts = array_merge(['composite', 'lock'], $keyParts);
+ $result = implode('.', $keyParts);
+ if (strlen($result) > 64) {
+ $result = sha1($result);
+ }
+ return $result;
}
private function finish(): void
diff --git a/src/Commands/CommandHelperTrait.php b/src/Commands/CommandHelperTrait.php
deleted file mode 100644
index 9c37488..0000000
--- a/src/Commands/CommandHelperTrait.php
+++ /dev/null
@@ -1,108 +0,0 @@
-writeln("$text");
- return Command::SUCCESS;
- }
-
- private function showAlert(OutputInterface $output, string $text): int
- {
- $output->writeln("$text");
- return Command::SUCCESS;
- }
-
- private function showError(OutputInterface $output, string $text): int
- {
- $output->writeln("$text");
- return Command::INVALID;
- }
-
- protected function ask(InputInterface $input, OutputInterface $output, Question $question): mixed
- {
- return (new QuestionHelper())->ask($input, $output, $question);
- }
-
- private function saveClassToFile(InputInterface $input, OutputInterface $output, string $class, string $content): bool
- {
- if (!$filePath = $this->getClassFilePath($class)) {
- return false;
- }
- $fileState = 'new';
- if (file_exists($filePath)) {
- $fileState = 'overwrite';
- if (!$input->getOption('force')
- && !$this->ask($input, $output, new ConfirmationQuestion("File `$filePath` is already exists, do you want to overwrite it?[y/n]: "))) {
- return true;
- }
- }
- if (file_put_contents($filePath, $content)) {
- $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)");
- return true;
- } else {
- $this->showError($output, "Something went wrong can `$filePath` was successfully generated ($fileState)");
- return false;
- }
- }
-
- protected function getClassFilePath(string $class): ?string
- {
- $class = trim($class, '\\');
- $namespaceParts = explode('\\', $class);
-
- $loaders = ClassLoader::getRegisteredLoaders();
- $matchedPrefixes = $matchedDirs = [];
- foreach ($loaders as $loader) {
- foreach ($loader->getPrefixesPsr4() as $prefix => $dir) {
- $prefixParts = explode('\\', trim($prefix, '\\'));
- foreach ($namespaceParts as $i => $namespacePart) {
- if (!isset($prefixParts[$i]) || $prefixParts[$i] !== $namespacePart) {
- break;
- }
- if (!isset($matchedPrefixes[$prefix])) {
- $matchedPrefixes[$prefix] = 0;
- $matchedDirs[$prefix] = $dir;
- }
- $matchedPrefixes[$prefix] += 1;
- }
- }
- }
- if (empty($matchedPrefixes)) {
- throw new \Exception("Failed to determine directory for class `$class` from psr4 autoloading");
- }
- arsort($matchedPrefixes);
- $prefix = key($matchedPrefixes);
- $dirs = $matchedDirs[$prefix];
-
- $namespaceParts = explode('\\', str_replace($prefix, '', $class));
- $filename = array_pop($namespaceParts) . '.php';
-
- $relativeDir = implode(
- DIRECTORY_SEPARATOR,
- array_merge(
- $dirs,
- $namespaceParts,
- )
- );
- if (!$realDir = realpath($relativeDir)) {
- $dirCreateResult = mkdir($relativeDir, 0755, true);
- if (!$dirCreateResult) {
- throw new \Exception("Directory `$relativeDir` not exists and failed to create it, please create it manually.");
- }
- $realDir = realpath($relativeDir);
- }
- return $realDir . DIRECTORY_SEPARATOR . $filename;
- }
-}
diff --git a/src/Commands/GenerateEntityCommand.php b/src/Commands/GenerateEntityCommand.php
deleted file mode 100644
index 16c3cdd..0000000
--- a/src/Commands/GenerateEntityCommand.php
+++ /dev/null
@@ -1,90 +0,0 @@
-addArgument('connection', InputArgument::REQUIRED, 'Connection name')
- ->addArgument('table', InputArgument::REQUIRED, 'Table name')
- ->addArgument('entity', InputArgument::OPTIONAL, 'Entity full class name')
- ->addOption('force', 'f', InputOption::VALUE_NONE, 'If existing file should be overwritten');
- }
-
- /**
- * @throws \Exception
- */
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $connectionName = $input->getArgument('connection');
- $tableName = $input->getArgument('table');
- $connection = ConnectionManager::getConnection($connectionName);
-
- if (!$entityClass = $input->getArgument('entity')) {
- $entityClass = $this->ask($input, $output, new Question('Enter entity full class name: '));
- }
- $entityClass = str_replace('\\\\', '\\', $entityClass);
-
- $schema = SQLSchema::generate($connection, $tableName);
- $enums = [];
- foreach ($schema->enums as $columnName => $sqlEnum) {
- if ($enumClass = $this->generateEnum($input, $output, $entityClass, $sqlEnum)) {
- $enums[$columnName] = $enumClass;
- }
- }
- $entityBuilder = new EntityClassBuilder($schema, $connectionName, $entityClass, $enums);
- $content = $entityBuilder->getClassContent();
-
- $this->saveClassToFile($input, $output, $entityClass, $content);
- return Command::SUCCESS;
- }
-
- private function generateEnum(InputInterface $input, OutputInterface $output, string $entityClass, SQLEnum $enum): ?string
- {
- $name = $enum->name;
- $values = $enum->values;
- $this->showAlert($output, "Found enum `$name` with values [" . implode(', ', $values) . "]");
- if (!$this->ask($input, $output, new ConfirmationQuestion('Do you want to generate Enum class?[y/n]: '))) {
- return null;
- }
- $enumShortClassName = ucfirst((new InflectorFactory())->build()->camelize($name));
- $entityNamespace = ClassHelper::extractNamespace($entityClass);
- $proposedClass = $entityNamespace . '\\Enums\\' . $enumShortClassName;
- $enumClass = $this->ask(
- $input,
- $output,
- new Question("Enter enum full class name [skip to use $proposedClass]: ")
- );
- if (!$enumClass) {
- $enumClass = $proposedClass;
- }
- $enumClassBuilder = new EnumClassBuilder($enumClass, $values);
-
- $content = $enumClassBuilder->getClassContent();
- if (!$this->saveClassToFile($input, $output, $enumClass, $content)) {
- return null;
- }
- return $enumClass;
- }
-}
\ No newline at end of file
diff --git a/src/Commands/GenerateTableCommand.php b/src/Commands/GenerateTableCommand.php
deleted file mode 100644
index fa14cbb..0000000
--- a/src/Commands/GenerateTableCommand.php
+++ /dev/null
@@ -1,95 +0,0 @@
-addArgument('entity', InputArgument::REQUIRED, 'Entity full class name')
- ->addArgument('table', InputArgument::OPTIONAL, 'Table full class name')
- ->addOption('cached', 'c', InputOption::VALUE_NONE, 'Generate cache version')
- ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing table class file');
- }
-
- /**
- * @throws \Exception
- */
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- /** @var class-string $entityClass */
- $entityClass = $input->getArgument('entity');
- $reflection = new \ReflectionClass($entityClass);
-
- if (!$reflection->isSubclassOf(AbstractEntity::class)) {
- return $this->showError($output, "Class `$entityClass` must be subclass of " . AbstractEntity::class);
- }
- $schema = $entityClass::schema();
- $tableConfig = TableConfig::fromEntitySchema($schema);
- $tableName = $tableConfig->tableName;
-
- if (!$tableClass = $input->getArgument('table')) {
- $proposedClass = preg_replace('/\w+$/', 'Tables', $reflection->getNamespaceName()) . "\\{$tableName}Table";
- $tableClass = $this->ask(
- $input,
- $output,
- new Question("Enter table full class name [skip to use $proposedClass]: ")
- );
- if (!$tableClass) {
- $tableClass = $proposedClass;
- }
- }
- if (str_starts_with($tableClass, '\\')) {
- $tableClass = substr($tableClass, 1);
- }
-
- if (!preg_match('/^(.+)\\\(\w+)$/', $tableClass)) {
- return $this->showError($output, "Table class `$tableClass` is incorrect");
- }
- if ($input->getOption('cached')) {
- $template = new CachedTableClassBuilder(
- tableClass: $tableClass,
- schema: $schema,
- tableConfig: $tableConfig,
- );
- } else {
- $template = new TableClassBuilder(
- tableClass: $tableClass,
- schema: $schema,
- tableConfig: $tableConfig,
- );
- }
- $template->generate();
- $fileContent = $template->getFileContent();
-
- $fileState = 'new';
- if (!$filePath = $this->getClassFilePath($tableClass)) {
- return Command::FAILURE;
- }
- if (file_exists($filePath)) {
- if (!$input->getOption('force')) {
- return $this->showError($output, "File `$filePath` already exists, use --force flag to overwrite it");
- }
- $fileState = 'overwrite';
- }
- file_put_contents($filePath, $fileContent);
- return $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)");
- }
-}
diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php
index 5ff27db..13694ef 100644
--- a/src/ConnectionManager.php
+++ b/src/ConnectionManager.php
@@ -3,7 +3,6 @@
namespace Composite\DB;
use Composite\DB\Exceptions\DbException;
-use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
@@ -12,20 +11,28 @@
class ConnectionManager
{
private const CONNECTIONS_CONFIG_ENV_VAR = 'CONNECTIONS_CONFIG_FILE';
+ /** @var array>|null */
private static ?array $configs = null;
+ /** @var array */
private static array $connections = [];
/**
* @throws DbException
*/
- public static function getConnection(string $name, ?Configuration $config = null, ?EventManager $eventManager = null): Connection
+ public static function getConnection(string $name, ?Configuration $config = null): Connection
{
+ if (self::$configs === null) {
+ self::$configs = self::loadConfigs();
+ }
if (!isset(self::$connections[$name])) {
try {
+ $connectionParams = self::$configs[$name] ?? throw new DbException("Connection config `$name` not found");
+ if (!$config && isset($connectionParams['configuration']) && $connectionParams['configuration'] instanceof Configuration) {
+ $config = $connectionParams['configuration'];
+ }
self::$connections[$name] = DriverManager::getConnection(
- params: self::getConnectionParams($name),
+ params: $connectionParams,
config: $config,
- eventManager: $eventManager,
);
} catch (Exception $e) {
throw new DbException($e->getMessage(), $e->getCode(), $e);
@@ -35,50 +42,41 @@ public static function getConnection(string $name, ?Configuration $config = null
}
/**
+ * @return array>
* @throws DbException
*/
- private static function getConnectionParams(string $name): array
+ private static function loadConfigs(): array
{
- if (self::$configs === null) {
- $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true);
- if (empty($configFile)) {
- throw new DbException(sprintf(
- 'ConnectionManager is not configured, please call ConnectionManager::configure() method or setup putenv(\'%s=/path/to/config/file.php\') variable',
- self::CONNECTIONS_CONFIG_ENV_VAR
- ));
- }
- if (!file_exists($configFile)) {
- throw new DbException(sprintf(
- 'Connections config file `%s` does not exist',
- $configFile
- ));
- }
- $configContent = require_once $configFile;
- if (empty($configContent) || !is_array($configContent)) {
- throw new DbException(sprintf(
- 'Connections config file `%s` should return array of connection params',
- $configFile
- ));
- }
- self::configure($configContent);
+ $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true) ?: ($_ENV[self::CONNECTIONS_CONFIG_ENV_VAR] ?? false);
+ if (empty($configFile)) {
+ throw new DbException(sprintf(
+ 'ConnectionManager is not configured, please define ENV variable `%s`',
+ self::CONNECTIONS_CONFIG_ENV_VAR
+ ));
}
- return self::$configs[$name] ?? throw new DbException("Connection config `$name` not found");
- }
-
- /**
- * @throws DbException
- */
- private static function configure(array $configs): void
- {
- foreach ($configs as $name => $connectionConfig) {
+ if (!file_exists($configFile)) {
+ throw new DbException(sprintf(
+ 'Connections config file `%s` does not exist',
+ $configFile
+ ));
+ }
+ $configFileContent = require $configFile;
+ if (empty($configFileContent) || !is_array($configFileContent)) {
+ throw new DbException(sprintf(
+ 'Connections config file `%s` should return array of connection params',
+ $configFile
+ ));
+ }
+ $result = [];
+ foreach ($configFileContent as $name => $connectionConfig) {
if (empty($name) || !is_string($name)) {
throw new DbException('Config has invalid connection name ' . var_export($name, true));
}
if (empty($connectionConfig) || !is_array($connectionConfig)) {
throw new DbException("Connection `$name` has invalid connection params");
}
- self::$configs[$name] = $connectionConfig;
+ $result[$name] = $connectionConfig;
}
- self::$configs = $configs;
+ return $result;
}
}
\ No newline at end of file
diff --git a/src/Exceptions/LockException.php b/src/Exceptions/LockException.php
new file mode 100644
index 0000000..650194d
--- /dev/null
+++ b/src/Exceptions/LockException.php
@@ -0,0 +1,7 @@
+entityClassShortName = ClassHelper::extractShortName($this->schema->class);
- $this->file = new PhpFile();
- }
-
- abstract public function getParentNamespace(): string;
- abstract public function generate(): void;
-
- final public function getFileContent(): string
- {
- return (string)$this->file;
- }
-
- protected function generateGetConfig(): Method
- {
- return (new Method('getConfig'))
- ->setProtected()
- ->setReturnType(TableConfig::class)
- ->setBody('return TableConfig::fromEntitySchema(' . $this->entityClassShortName . '::schema());');
- }
-
- protected function buildVarsList(array $vars): string
- {
- if (count($vars) === 1) {
- $var = current($vars);
- return '$' . $var;
- }
- $vars = array_map(
- fn ($var) => "'$var' => \$" . $var,
- $vars
- );
- return '[' . implode(', ', $vars) . ']';
- }
-
- /**
- * @param AbstractColumn[] $columns
- */
- protected function addMethodParameters(Method $method, array $columns): void
- {
- foreach ($columns as $column) {
- $method
- ->addParameter($column->name)
- ->setType($column->type);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Generator/CachedTableClassBuilder.php b/src/Generator/CachedTableClassBuilder.php
deleted file mode 100644
index 760ad44..0000000
--- a/src/Generator/CachedTableClassBuilder.php
+++ /dev/null
@@ -1,97 +0,0 @@
-file
- ->addNamespace(ClassHelper::extractNamespace($this->tableClass))
- ->addUse(AbstractEntity::class)
- ->addUse(AbstractCachedTable::class)
- ->addUse(TableConfig::class)
- ->addUse($this->schema->class)
- ->addClass(ClassHelper::extractShortName($this->tableClass))
- ->setExtends(AbstractCachedTable::class)
- ->setMethods($this->getMethods());
- }
-
- private function getMethods(): array
- {
- return array_filter([
- $this->generateGetConfig(),
- $this->generateGetFlushCacheKeys(),
- $this->generateFindOne(),
- $this->generateFindAll(),
- $this->generateCountAll(),
- ]);
- }
-
- protected function generateGetFlushCacheKeys(): Method
- {
- $method = (new Method('getFlushCacheKeys'))
- ->setProtected()
- ->setReturnType('array')
- ->addBody('return [')
- ->addBody(' $this->getListCacheKey(),')
- ->addBody(' $this->getCountCacheKey(),')
- ->addBody('];');
-
- $type = $this->schema->class . '|' . AbstractEntity::class;
- $method
- ->addParameter('entity')
- ->setType($type);
- return $method;
- }
-
- protected function generateFindOne(): ?Method
- {
- $primaryColumns = array_map(
- fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."),
- $this->tableConfig->primaryKeys
- );
- if (count($this->tableConfig->primaryKeys) === 1) {
- $body = 'return $this->createEntity($this->findByPkCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
- } else {
- $body = 'return $this->createEntity($this->findOneCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
- }
-
- $method = (new Method('findByPk'))
- ->setPublic()
- ->setReturnType($this->schema->class)
- ->setReturnNullable()
- ->setBody($body);
- $this->addMethodParameters($method, $primaryColumns);
- return $method;
- }
-
- protected function generateFindAll(): Method
- {
- return (new Method('findAll'))
- ->setPublic()
- ->setComment('@return ' . $this->entityClassShortName . '[]')
- ->setReturnType('array')
- ->setBody('return $this->createEntities($this->findAllCachedInternal());');
- }
-
- protected function generateCountAll(): Method
- {
- return (new Method('countAll'))
- ->setPublic()
- ->setReturnType('int')
- ->setBody('return $this->countAllCachedInternal();');
- }
-}
\ No newline at end of file
diff --git a/src/Generator/EntityClassBuilder.php b/src/Generator/EntityClassBuilder.php
deleted file mode 100644
index 96453bd..0000000
--- a/src/Generator/EntityClassBuilder.php
+++ /dev/null
@@ -1,264 +0,0 @@
-renderTemplate('EntityTemplate', $this->getVars());
- }
-
- /**
- * @return array
- * @throws \Exception
- */
- private function getVars(): array
- {
- $traits = $properties = [];
- $constructorParams = $this->getEntityProperties();
- if (!empty($this->schema->columns['deleted_at'])) {
- $traits[] = 'Traits\SoftDelete';
- $this->useNamespaces[] = 'Composite\DB\Traits';
- unset($constructorParams['deleted_at']);
- }
- foreach ($constructorParams as $name => $constructorParam) {
- if ($this->schema->columns[$name]->isAutoincrement) {
- $properties[$name] = $constructorParam;
- unset($constructorParams[$name]);
- }
- }
- if (!preg_match('/^(.+)\\\(\w+)$/', $this->entityClass, $matches)) {
- throw new \Exception("Entity class `$this->entityClass` is incorrect");
- }
-
- return [
- 'phpOpener' => ' $this->connectionName,
- 'tableName' => $this->schema->tableName,
- 'pkNames' => "'" . implode("', '", $this->schema->primaryKeys) . "'",
- 'indexes' => $this->getIndexes(),
- 'traits' => $traits,
- 'entityNamespace' => $matches[1],
- 'entityClassShortname' => $matches[2],
- 'properties' => $properties,
- 'constructorParams' => $constructorParams,
- 'useNamespaces' => array_unique($this->useNamespaces),
- 'useAttributes' => array_unique($this->useAttributes),
- ];
- }
-
- private function getEntityProperties(): array
- {
- $noDefaultValue = $hasDefaultValue = [];
- foreach ($this->schema->columns as $column) {
- $attributes = [];
- if ($this->schema->isPrimaryKey($column->name)) {
- $this->useAttributes[] = 'PrimaryKey';
- $autoIncrement = $column->isAutoincrement ? '(autoIncrement: true)' : '';
- $attributes[] = '#[PrimaryKey' . $autoIncrement . ']';
- }
- if ($columnAttributeProperties = $column->getColumnAttributeProperties()) {
- $this->useAttributes[] = 'Column';
- $attributes[] = '#[Column(' . implode(', ', $columnAttributeProperties) . ')]';
- }
- $propertyParts = [$this->getPropertyVisibility($column)];
- if ($this->isReadOnly($column)) {
- $propertyParts[] = 'readonly';
- }
- $propertyParts[] = $this->getColumnType($column);
- $propertyParts[] = '$' . $column->name;
- if ($column->hasDefaultValue) {
- $defaultValue = $this->getDefaultValue($column);
- $propertyParts[] = '= ' . $defaultValue;
- $hasDefaultValue[$column->name] = [
- 'attributes' => $attributes,
- 'var' => implode(' ', $propertyParts),
- ];
- } else {
- $noDefaultValue[$column->name] = [
- 'attributes' => $attributes,
- 'var' => implode(' ', $propertyParts),
- ];
- }
- }
- return array_merge($noDefaultValue, $hasDefaultValue);
- }
-
- private function getPropertyVisibility(SQLColumn $column): string
- {
- return 'public';
- }
-
- private function isReadOnly(SQLColumn $column): bool
- {
- if ($column->isAutoincrement) {
- return true;
- }
- $readOnlyColumns = array_merge(
- $this->schema->primaryKeys,
- [
- 'created_at',
- 'createdAt',
- ]
- );
- return in_array($column->name, $readOnlyColumns);
- }
-
- private function getColumnType(SQLColumn $column): string
- {
- if ($column->type === ColumnType::Enum) {
- if (!$type = $this->getEnumName($column->name)) {
- $type = 'string';
- }
- } else {
- $type = $column->type->value;
- }
- if ($column->isNullable) {
- $type = '?' . $type;
- }
- return $type;
- }
-
- public function getDefaultValue(SQLColumn $column): mixed
- {
- $defaultValue = $column->defaultValue;
- if ($defaultValue === null) {
- return 'null';
- }
- if ($column->type === ColumnType::Datetime) {
- $currentTimestamp = stripos($defaultValue, 'current_timestamp') === 0 || $defaultValue === 'now()';
- if ($currentTimestamp) {
- $defaultValue = "new \DateTimeImmutable()";
- } else {
- if ($defaultValue === 'epoch') {
- $defaultValue = '1970-01-01 00:00:00';
- } elseif ($defaultValue instanceof \DateTimeInterface) {
- $defaultValue = DateTimeHelper::dateTimeToString($defaultValue);
- }
- $defaultValue = "new \DateTimeImmutable('" . $defaultValue . "')";
- }
- } elseif ($column->type === ColumnType::Enum) {
- if ($enumName = $this->getEnumName($column->name)) {
- $valueName = null;
- /** @var \UnitEnum $enumClass */
- $enumClass = $this->enums[$column->name];
- foreach ($enumClass::cases() as $enumCase) {
- if ($enumCase->name === $defaultValue) {
- $valueName = $enumCase->name;
- }
- }
- if ($valueName) {
- $defaultValue = $enumName . '::' . $valueName;
- } else {
- return 'null';
- }
- } else {
- $defaultValue = "'$defaultValue'";
- }
- } elseif ($column->type === ColumnType::Boolean) {
- if (strcasecmp($defaultValue, 'false') === 0) {
- return 'false';
- }
- if (strcasecmp($defaultValue, 'true') === 0) {
- return 'true';
- }
- return !empty($defaultValue) ? 'true' : 'false';
- } elseif ($column->type === ColumnType::Array) {
- if ($defaultValue === '{}' || $defaultValue === '[]') {
- return '[]';
- }
- if ($decoded = json_decode($defaultValue, true)) {
- return var_export($decoded, true);
- }
- return $defaultValue;
- } else {
- if ($column->type !== ColumnType::Integer && $column->type !== ColumnType::Float) {
- $defaultValue = "'$defaultValue'";
- }
- }
- return $defaultValue;
- }
-
- private function getEnumName(string $columnName): ?string
- {
- if (empty($this->enums[$columnName])) {
- return null;
- }
- $enumClass = $this->enums[$columnName];
- if (!\in_array($enumClass, $this->useNamespaces)) {
- $this->useNamespaces[] = $enumClass;
- }
- return substr(strrchr($enumClass, "\\"), 1);
- }
-
- private function getIndexes(): array
- {
- $result = [];
- foreach ($this->schema->indexes as $index) {
- $properties = [
- "columns: ['" . implode("', '", $index->columns) . "']",
- ];
- if ($index->isUnique) {
- $properties[] = "isUnique: true";
- }
- if ($index->sort) {
- $sortParts = [];
- foreach ($index->sort as $key => $direction) {
- $sortParts[] = "'$key' => '$direction'";
- }
- $properties[] = 'sort: [' . implode(', ', $sortParts) . ']';
- }
- if ($index->name) {
- $properties[] = "name: '" . $index->name . "'";
- }
- $this->useAttributes[] = 'Index';
- $result[] = '#[Index(' . implode(', ', $properties) . ')]';
- }
- return $result;
- }
-
- private function renderTemplate(string $templateName, array $variables = []): string
- {
- $filePath = implode(
- DIRECTORY_SEPARATOR,
- [
- __DIR__,
- 'Templates',
- "$templateName.php",
- ]
- );
- if (!file_exists($filePath)) {
- throw new \Exception("File `$filePath` not found");
- }
- extract($variables, EXTR_SKIP);
- ob_start();
- include $filePath;
- return ob_get_clean();
- }
-}
\ No newline at end of file
diff --git a/src/Generator/EnumClassBuilder.php b/src/Generator/EnumClassBuilder.php
deleted file mode 100644
index b2bd580..0000000
--- a/src/Generator/EnumClassBuilder.php
+++ /dev/null
@@ -1,32 +0,0 @@
-cases as $case) {
- $enumCases[] = new EnumCase($case);
- }
- $file = new PhpFile();
- $file
- ->setStrictTypes()
- ->addEnum($this->enumClass)
- ->setCases($enumCases);
-
- return (string)$file;
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/ColumnType.php b/src/Generator/Schema/ColumnType.php
deleted file mode 100644
index 5f20634..0000000
--- a/src/Generator/Schema/ColumnType.php
+++ /dev/null
@@ -1,15 +0,0 @@
-executeQuery("SHOW CREATE TABLE $tableName")
- ->fetchAssociative();
- $this->sql = $showResult['Create Table'] ?? throw new \Exception("Table `$tableName` not found");
- }
-
- public function getSchema(): SQLSchema
- {
- $columns = $enums = $primaryKeys = $indexes = [];
- $parser = new SQLParser();
- $tokens = $parser->parse($this->sql);
- $table = current($tokens);
- $tableName = $table['name'];
-
- foreach ($table['fields'] as $field) {
- $name = $field['name'];
- $precision = $scale = null;
- $sqlType = $field['type'];
- $size = !empty($field['length']) ? (int)$field['length'] : null;
- $type = $this->getType($sqlType, $size);
-
- if ($type === ColumnType::Enum) {
- $enums[$name] = new SQLEnum(name: $name, values: $field['values']);
- } elseif ($type === ColumnType::Float) {
- $precision = $size;
- $scale = !empty($field['decimals']) ? (int)$field['decimals'] : null;
- $size = null;
- }
- if (isset($field['default'])) {
- $hasDefaultValue = true;
- $defaultValue = $this->getDefaultValue($type, $field['default']);
- } else {
- $hasDefaultValue = false;
- $defaultValue = null;
- }
- $column = new SQLColumn(
- name: $name,
- sql: $sqlType,
- type: $type,
- size: $size,
- precision: $precision,
- scale: $scale,
- isNullable: !empty($field['null']),
- hasDefaultValue: $hasDefaultValue,
- defaultValue: $defaultValue,
- isAutoincrement: !empty($field['auto_increment']),
- );
- $columns[$column->name] = $column;
- }
- foreach ($table['indexes'] as $index) {
- $indexType = strtolower($index['type']);
- $cols = [];
- foreach ($index['cols'] as $col) {
- $colName = $col['name'];
- $cols[] = $colName;
- }
- if ($indexType === 'primary') {
- $primaryKeys = $cols;
- continue;
- }
- $indexes[] = new SQLIndex(
- name: $index['name'] ?? null,
- isUnique: $indexType === 'unique',
- columns: $cols,
- );
- }
- return new SQLSchema(
- tableName: $tableName,
- columns: $columns,
- enums: $enums,
- primaryKeys: array_unique($primaryKeys),
- indexes: $indexes,
- );
- }
-
- private function getType(string $type, ?int $size): ColumnType
- {
- $type = strtolower($type);
- if ($type === 'tinyint' && $size === 1) {
- return ColumnType::Boolean;
- }
- return match ($type) {
- 'integer', 'int', 'smallint', 'tinyint', 'mediumint', 'bigint' => ColumnType::Integer,
- 'float', 'double', 'numeric', 'decimal' => ColumnType::Float,
- 'timestamp', 'datetime' => ColumnType::Datetime,
- 'json', 'set' => ColumnType::Array,
- 'enum' => ColumnType::Enum,
- default => ColumnType::String,
- };
- }
-
- private function getDefaultValue(ColumnType $type, mixed $value): mixed
- {
- if ($value === null || (is_string($value) && strcasecmp($value, 'null') === 0)) {
- return null;
- }
- return $value;
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/Parsers/PostgresSchemaParser.php b/src/Generator/Schema/Parsers/PostgresSchemaParser.php
deleted file mode 100644
index bcf7ebd..0000000
--- a/src/Generator/Schema/Parsers/PostgresSchemaParser.php
+++ /dev/null
@@ -1,221 +0,0 @@
-tableName = $tableName;
- $this->informationSchemaColumns = $connection->executeQuery(
- sql: PostgresSchemaParser::COLUMNS_SQL,
- params: ['tableName' => $tableName],
- )->fetchAllAssociative();
- $this->informationSchemaIndexes = $connection->executeQuery(
- sql: PostgresSchemaParser::INDEXES_SQL,
- params: ['tableName' => $tableName],
- )->fetchAllAssociative();
-
- if ($primaryKeySQL = PostgresSchemaParser::getPrimaryKeySQL($tableName)) {
- $primaryKeys = array_map(
- fn(array $row): string => $row['column_name'],
- $connection->executeQuery($primaryKeySQL)->fetchAllAssociative()
- );
- } else {
- $primaryKeys = [];
- }
- $this->primaryKeys = $primaryKeys;
-
- $allEnumsRaw = $connection->executeQuery(PostgresSchemaParser::ALL_ENUMS_SQL)->fetchAllAssociative();
- $allEnums = [];
- foreach ($allEnumsRaw as $enumRaw) {
- $name = $enumRaw['enum_name'];
- $value = $enumRaw['enum_value'];
- if (!isset($allEnums[$name])) {
- $allEnums[$name] = [];
- }
- $allEnums[$name][] = $value;
- }
- $this->allEnums = $allEnums;
- }
-
- public function getSchema(): SQLSchema
- {
- $columns = $enums = [];
- foreach ($this->informationSchemaColumns as $informationSchemaColumn) {
- $name = $informationSchemaColumn['column_name'];
- $type = $this->getType($informationSchemaColumn);
- $sqlDefault = $informationSchemaColumn['column_default'];
- $isNullable = $informationSchemaColumn['is_nullable'] === 'YES';
- $defaultValue = $this->getDefaultValue($type, $sqlDefault);
- $hasDefaultValue = $defaultValue !== null || $isNullable;
- $isAutoincrement = $sqlDefault && str_starts_with($sqlDefault, 'nextval(');
-
- if ($type === ColumnType::Enum) {
- $udtName = $informationSchemaColumn['udt_name'];
- $enums[$name] = new SQLEnum(name: $udtName, values: $this->allEnums[$udtName]);
- }
- $column = new SQLColumn(
- name: $name,
- sql: $informationSchemaColumn['udt_name'],
- type: $type,
- size: $this->getSize($type, $informationSchemaColumn),
- precision: $this->getPrecision($type, $informationSchemaColumn),
- scale: $this->getScale($type, $informationSchemaColumn),
- isNullable: $isNullable,
- hasDefaultValue: $hasDefaultValue,
- defaultValue: $defaultValue,
- isAutoincrement: $isAutoincrement,
- );
- $columns[$column->name] = $column;
- }
- return new SQLSchema(
- tableName: $this->tableName,
- columns: $columns,
- enums: $enums,
- primaryKeys: $this->primaryKeys,
- indexes: $this->parseIndexes(),
- );
- }
-
- private function getType(array $informationSchemaColumn): ColumnType
- {
- $dataType = $informationSchemaColumn['data_type'];
- $udtName = $informationSchemaColumn['udt_name'];
- if ($dataType === 'USER-DEFINED' && !empty($this->allEnums[$udtName])) {
- return ColumnType::Enum;
- }
- if (preg_match('/^int(\d?)$/', $udtName)) {
- return ColumnType::Integer;
- }
- if (preg_match('/^float(\d?)$/', $udtName)) {
- return ColumnType::Float;
- }
- $matchType = match ($udtName) {
- 'numeric' => ColumnType::Float,
- 'timestamp', 'timestamptz' => ColumnType::Datetime,
- 'json', 'array' => ColumnType::Array,
- 'bool' => ColumnType::Boolean,
- default => null,
- };
- return $matchType ?? ColumnType::String;
- }
-
- private function getSize(ColumnType $type, array $informationSchemaColumn): ?int
- {
- if ($type === ColumnType::String) {
- return $informationSchemaColumn['character_maximum_length'];
- }
- return null;
- }
-
- private function getPrecision(ColumnType $type, array $informationSchemaColumn): ?int
- {
- if ($type !== ColumnType::Float) {
- return null;
- }
- return $informationSchemaColumn['numeric_precision'];
- }
-
- private function getScale(ColumnType $type, array $informationSchemaColumn): ?int
- {
- if ($type !== ColumnType::Float) {
- return null;
- }
- return $informationSchemaColumn['numeric_scale'];
- }
-
- private function getDefaultValue(ColumnType $type, ?string $sqlValue): mixed
- {
- if ($sqlValue === null || strcasecmp($sqlValue, 'null') === 0) {
- return null;
- }
- if (str_starts_with($sqlValue, 'nextval(')) {
- return null;
- }
- $parts = explode('::', $sqlValue);
- return trim($parts[0], '\'');
- }
-
- private function parseIndexes(): array
- {
- $result = [];
- foreach ($this->informationSchemaIndexes as $informationSchemaIndex) {
- $name = $informationSchemaIndex['indexname'];
- $sql = $informationSchemaIndex['indexdef'];
- $isUnique = stripos($sql, ' unique index ') !== false;
-
- if (!preg_match('/\(([`"\',\s\w]+)\)/', $sql, $columnsMatch)) {
- continue;
- }
- $columnsRaw = array_map(
- fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)),
- explode(',', $columnsMatch[1])
- );
- $columns = $sort = [];
- foreach ($columnsRaw as $columnRaw) {
- $parts = explode(' ', $columnRaw);
- $columns[] = $parts[0];
- if (!empty($parts[1])) {
- $sort[$parts[0]] = strtoupper($parts[1]);
- }
- }
- if ($columns === $this->primaryKeys) {
- continue;
- }
- $result[] = new SQLIndex(
- name: $name,
- isUnique: $isUnique,
- columns: $columns,
- );
- }
- return $result;
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php b/src/Generator/Schema/Parsers/SQLiteSchemaParser.php
deleted file mode 100644
index c11c49a..0000000
--- a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php
+++ /dev/null
@@ -1,247 +0,0 @@
-tableSql = $connection->executeQuery(
- sql: self::TABLE_SQL,
- params: ['tableName' => $tableName],
- )->fetchOne();
- $this->indexesSql = $connection->executeQuery(
- sql: self::INDEXES_SQL,
- params: ['tableName' => $tableName],
- )->fetchFirstColumn();
- }
-
- public function getSchema(): SQLSchema
- {
- $columns = $enums = $primaryKeys = [];
- $columnsStarted = false;
- $tableName = '';
- $lines = array_map(
- fn ($line) => trim(preg_replace("/\s+/", " ", $line)),
- explode("\n", $this->tableSql),
- );
- for ($i = 0; $i < count($lines); $i++) {
- $line = $lines[$i];
- if (!$line) {
- continue;
- }
- if (!$tableName && preg_match(self::TABLE_NAME_PATTERN, $line, $matches)) {
- $tableName = $matches[1];
- }
- if (!$columnsStarted) {
- if (str_starts_with($line, '(') || str_ends_with($line, '(')) {
- $columnsStarted = true;
- }
- continue;
- }
- if ($line === ')') {
- break;
- }
- if (!str_ends_with($line, ',')) {
- if (!empty($lines[$i + 1]) && !str_starts_with($lines[$i + 1], ')')) {
- $lines[$i + 1] = $line . ' ' . $lines[$i + 1];
- continue;
- }
- }
- if ($column = $this->parseSQLColumn($line)) {
- $columns[$column->name] = $column;
- }
- $primaryKeys = array_merge($primaryKeys, $this->parsePrimaryKeys($line));
- if ($enum = $this->parseEnum($line)) {
- $enums[$column?->name ?? $enum->name] = $enum;
- }
- }
- return new SQLSchema(
- tableName: $tableName,
- columns: $columns,
- enums: $enums,
- primaryKeys: array_unique($primaryKeys),
- indexes: $this->getIndexes(),
- );
- }
-
- private function parseSQLColumn(string $sqlLine): ?SQLColumn
- {
- if (!preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) {
- return null;
- }
- $name = $matches[1];
- $rawType = $matches[2];
- $rawTypeParams = !empty($matches[4]) ? str_replace(' ', '', $matches[4]) : null;
- $type = $this->getColumnType($rawType) ?? ColumnType::String;
- $hasDefaultValue = stripos($sqlLine, ' default ') !== false;
- return new SQLColumn(
- name: $name,
- sql: $sqlLine,
- type: $type,
- size: $this->getColumnSize($type, $rawTypeParams),
- precision: $this->getColumnPrecision($type, $rawTypeParams),
- scale: $this->getScale($type, $rawTypeParams),
- isNullable: stripos($sqlLine, ' not null') === false,
- hasDefaultValue: $hasDefaultValue,
- defaultValue: $hasDefaultValue ? $this->getDefaultValue($sqlLine) : null,
- isAutoincrement: stripos($sqlLine, ' autoincrement') !== false,
- );
- }
-
- private function getColumnType(string $rawType): ?ColumnType
- {
- if (!preg_match('/^([a-zA-Z]+).*/', $rawType, $matches)) {
- return null;
- }
- $type = strtolower($matches[1]);
- return match ($type) {
- 'integer', 'int' => ColumnType::Integer,
- 'real' => ColumnType::Float,
- 'timestamp' => ColumnType::Datetime,
- 'enum' => ColumnType::Enum,
- default => ColumnType::String,
- };
- }
-
- private function getColumnSize(ColumnType $type, ?string $typeParams): ?int
- {
- if ($type !== ColumnType::String || !$typeParams) {
- return null;
- }
- return (int)$typeParams;
- }
-
- private function getColumnPrecision(ColumnType $type, ?string $typeParams): ?int
- {
- if ($type !== ColumnType::Float || !$typeParams) {
- return null;
- }
- $parts = explode(',', $typeParams);
- return (int)$parts[0];
- }
-
- private function getScale(ColumnType $type, ?string $typeParams): ?int
- {
- if ($type !== ColumnType::Float || !$typeParams) {
- return null;
- }
- $parts = explode(',', $typeParams);
- return !empty($parts[1]) ? (int)$parts[1] : null;
- }
-
- private function getDefaultValue(string $sqlLine): mixed
- {
- $sqlLine = $this->cleanCheckEnum($sqlLine);
- if (preg_match('/default\s+\'(.*)\'/iu', $sqlLine, $matches)) {
- return $matches[1];
- } elseif (preg_match('/default\s+([\w.]+)/iu', $sqlLine, $matches)) {
- $defaultValue = $matches[1];
- if (strtolower($defaultValue) === 'null') {
- return null;
- }
- return $defaultValue;
- }
- return null;
- }
-
- private function parsePrimaryKeys(string $sqlLine): array
- {
- if (preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) {
- $name = $matches[1];
- return stripos($sqlLine, ' primary key') !== false ? [$name] : [];
- }
- if (!preg_match(self::CONSTRAINT_PATTERN, $sqlLine, $matches)
- && !preg_match(self::PRIMARY_KEY_PATTERN, $sqlLine, $matches)) {
- return [];
- }
- $primaryColumnsRaw = $matches[1];
- $primaryColumnsRaw = str_replace(['\'', '"', '`', ' '], '', $primaryColumnsRaw);
- return explode(',', $primaryColumnsRaw);
- }
-
- private function parseEnum(string $sqlLine): ?SQLEnum
- {
- if (!preg_match(self::ENUM_PATTERN, $sqlLine, $matches)) {
- return null;
- }
- $name = $matches[1];
- $values = [];
- $sqlValues = array_map('trim', explode(',', $matches[2]));
- foreach ($sqlValues as $value) {
- $value = trim($value);
- if (str_starts_with($value, '\'')) {
- $value = trim($value, '\'');
- } elseif (str_starts_with($value, '"')) {
- $value = trim($value, '"');
- }
- $values[] = $value;
- }
- return new SQLEnum(name: $name, values: $values);
- }
-
- /**
- * @return SQLIndex[]
- */
- private function getIndexes(): array
- {
- $result = [];
- foreach ($this->indexesSql as $indexSql) {
- if (!$indexSql) continue;
- $indexSql = trim(str_replace("\n", " ", $indexSql));
- $indexSql = preg_replace("/\s+/", " ", $indexSql);
- if (!preg_match('/index\s+(?:`|\"|\')?(\w+)(?:`|\"|\')?/i', $indexSql, $nameMatch)) {
- continue;
- }
- $name = $nameMatch[1];
- if (!preg_match('/\(([`"\',\s\w]+)\)/', $indexSql, $columnsMatch)) {
- continue;
- }
- $columnsRaw = array_map(
- fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)),
- explode(',', $columnsMatch[1])
- );
- $columns = $sort = [];
- foreach ($columnsRaw as $columnRaw) {
- $parts = explode(' ', $columnRaw);
- $columns[] = $parts[0];
- if (!empty($parts[1])) {
- $sort[$parts[0]] = strtolower($parts[1]);
- }
- }
- $result[] = new SQLIndex(
- name: $name,
- isUnique: stripos($indexSql, ' unique index ') !== false,
- columns: $columns,
- sort: $sort,
- );
- }
- return $result;
- }
-
- private function cleanCheckEnum(string $sqlLine): string
- {
- return preg_replace('/ check \(\"\w+\" IN \(.+\)\)/i', '', $sqlLine);
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLColumn.php b/src/Generator/Schema/SQLColumn.php
deleted file mode 100644
index 00ad7d3..0000000
--- a/src/Generator/Schema/SQLColumn.php
+++ /dev/null
@@ -1,45 +0,0 @@
-type !== ColumnType::String) {
- return true;
- }
- if ($this->size === null) {
- return true;
- }
- return $this->size === 255;
- }
-
- public function getColumnAttributeProperties(): array
- {
- $result = [];
- if ($this->size && !$this->sizeIsDefault()) {
- $result[] = 'size: ' . $this->size;
- }
- if ($this->precision) {
- $result[] = 'precision: ' . $this->precision;
- }
- if ($this->scale) {
- $result[] = 'scale: ' . $this->scale;
- }
- return $result;
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Schema/SQLEnum.php b/src/Generator/Schema/SQLEnum.php
deleted file mode 100644
index ba726df..0000000
--- a/src/Generator/Schema/SQLEnum.php
+++ /dev/null
@@ -1,11 +0,0 @@
-getDriver();
- if ($driver instanceof Driver\AbstractSQLiteDriver) {
- $parser = new SQLiteSchemaParser($connection, $tableName);
- return $parser->getSchema();
- } elseif ($driver instanceof Driver\AbstractMySQLDriver) {
- $parser = new MySQLSchemaParser($connection, $tableName);
- return $parser->getSchema();
- } elseif ($driver instanceof Driver\AbstractPostgreSQLDriver) {
- $parser = new PostgresSchemaParser($connection, $tableName);
- return $parser->getSchema();
- } else {
- throw new \Exception("Driver `" . $driver::class . "` is not yet supported");
- }
- }
-
- public function isPrimaryKey(string $name): bool
- {
- return \in_array($name, $this->primaryKeys);
- }
-}
\ No newline at end of file
diff --git a/src/Generator/TableClassBuilder.php b/src/Generator/TableClassBuilder.php
deleted file mode 100644
index ba840f8..0000000
--- a/src/Generator/TableClassBuilder.php
+++ /dev/null
@@ -1,77 +0,0 @@
-file
- ->setStrictTypes()
- ->addNamespace(ClassHelper::extractNamespace($this->tableClass))
- ->addUse(AbstractTable::class)
- ->addUse(TableConfig::class)
- ->addUse($this->schema->class)
- ->addClass(ClassHelper::extractShortName($this->tableClass))
- ->setExtends(AbstractTable::class)
- ->setMethods($this->getMethods());
- }
-
- private function getMethods(): array
- {
- return array_filter([
- $this->generateGetConfig(),
- $this->generateFindOne(),
- $this->generateFindAll(),
- $this->generateCountAll(),
- ]);
- }
-
- protected function generateFindOne(): ?Method
- {
- $primaryColumns = array_map(
- fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."),
- $this->tableConfig->primaryKeys
- );
- if (count($this->tableConfig->primaryKeys) === 1) {
- $body = 'return $this->createEntity($this->findByPkInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
- } else {
- $body = 'return $this->createEntity($this->findOneInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));';
- }
- $method = (new Method('findByPk'))
- ->setPublic()
- ->setReturnType($this->schema->class)
- ->setReturnNullable()
- ->setBody($body);
- $this->addMethodParameters($method, $primaryColumns);
- return $method;
- }
-
- protected function generateFindAll(): Method
- {
- return (new Method('findAll'))
- ->setPublic()
- ->setComment('@return ' . $this->entityClassShortName . '[]')
- ->setReturnType('array')
- ->setBody('return $this->createEntities($this->findAllInternal());');
- }
-
- protected function generateCountAll(): Method
- {
- return (new Method('countAll'))
- ->setPublic()
- ->setReturnType('int')
- ->setBody('return $this->countAllInternal();');
- }
-}
\ No newline at end of file
diff --git a/src/Generator/Templates/EntityTemplate.php b/src/Generator/Templates/EntityTemplate.php
deleted file mode 100644
index 9384d5b..0000000
--- a/src/Generator/Templates/EntityTemplate.php
+++ /dev/null
@@ -1,41 +0,0 @@
-= $phpOpener ?? '' ?>
-
-
-namespace = $entityNamespace ?? '' ?>;
-
-
-use Composite\DB\Attributes\{= implode(', ', $useAttributes) ?>};
-
-
-use =$namespace?>;
-
-
-#[Table(connection: '= $connectionName ?? '' ?>', name: '= $tableName ?? '' ?>')]
-
-=$index?>
-
-
-class =$entityClassShortname??''?> extends AbstractEntity
-{
-
- use = $trait ?>;
-
-
-
-
- = $attribute ?>
-
-
- = $property['var'] ?>;
-
-
- public function __construct(
-
-
- = $attribute ?>
-
-
- = $param['var'] ?>,
-
- ) {}
-}
diff --git a/src/Helpers/ClassHelper.php b/src/Helpers/ClassHelper.php
deleted file mode 100644
index fa7b7f8..0000000
--- a/src/Helpers/ClassHelper.php
+++ /dev/null
@@ -1,18 +0,0 @@
-isPostgreSQL !== null) {
+ return;
+ }
+ $driver = $this->getConnection()->getDriver();
+ if ($driver instanceof Driver\AbstractPostgreSQLDriver) {
+ $this->isPostgreSQL = true;
+ $this->isMySQL = $this->isSQLite = false;
+ } elseif ($driver instanceof Driver\AbstractSQLiteDriver) {
+ $this->isSQLite = true;
+ $this->isPostgreSQL = $this->isMySQL = false;
+ } elseif ($driver instanceof Driver\AbstractMySQLDriver) {
+ $this->isMySQL = true;
+ $this->isPostgreSQL = $this->isSQLite = false;
+ } else {
+ // @codeCoverageIgnoreStart
+ throw new DbException('Unsupported driver ' . $driver::class);
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return array
+ */
+ private function prepareDataForSql(array $data): array
+ {
+ foreach ($data as $columnName => $value) {
+ if (is_bool($value)) {
+ $data[$columnName] = $value ? 1 : 0;
+ }
+ }
+ return $data;
+ }
+
+ protected function escapeIdentifier(string $key): string
+ {
+ $this->identifyPlatform();
+ if ($this->isMySQL) {
+ if (strpos($key, '.')) {
+ return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key)));
+ } else {
+ return "`$key`";
+ }
+ } else {
+ return '"' . $key . '"';
+ }
+ }
+}
diff --git a/src/Helpers/DateTimeHelper.php b/src/Helpers/DateTimeHelper.php
deleted file mode 100644
index 59eea94..0000000
--- a/src/Helpers/DateTimeHelper.php
+++ /dev/null
@@ -1,37 +0,0 @@
-format($withMicro ? self::DATETIME_MICRO_FORMAT : self::DATETIME_FORMAT);
- }
-
- public static function isDefault(mixed $value): bool
- {
- if (!$value) {
- return true;
- }
- if ($value instanceof \DateTimeInterface) {
- $value = self::dateTimeToString($value);
- }
- return in_array(
- $value,
- [self::DEFAULT_TIMESTAMP, self::DEFAULT_TIMESTAMP_MICRO, self::DEFAULT_DATETIME, self::DEFAULT_DATETIME_MICRO]
- );
- }
-}
diff --git a/src/Helpers/SelectRawTrait.php b/src/Helpers/SelectRawTrait.php
new file mode 100644
index 0000000..b02bb89
--- /dev/null
+++ b/src/Helpers/SelectRawTrait.php
@@ -0,0 +1,132 @@
+', '<', '>=', '<=', '<>'];
+
+ private ?QueryBuilder $selectQuery = null;
+
+ protected function select(string $select = '*'): QueryBuilder
+ {
+ if ($this->selectQuery === null) {
+ $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName());
+ }
+ return (clone $this->selectQuery)->select($select);
+ }
+
+ /**
+ * @param array|Where $where
+ * @param array|string $orderBy
+ * @return array|null
+ * @throws \Doctrine\DBAL\Exception
+ */
+ private function _findOneRaw(array|Where $where, array|string $orderBy = []): ?array
+ {
+ $query = $this->select();
+ $this->buildWhere($query, $where);
+ $this->applyOrderBy($query, $orderBy);
+ return $query->fetchAssociative() ?: null;
+ }
+
+ /**
+ * @param array|Where $where
+ * @param array|string $orderBy
+ * @return list>
+ * @throws \Doctrine\DBAL\Exception
+ */
+ private function _findAllRaw(
+ array|Where $where = [],
+ array|string $orderBy = [],
+ ?int $limit = null,
+ ?int $offset = null,
+ ): array
+ {
+ $query = $this->select();
+ $this->buildWhere($query, $where);
+ $this->applyOrderBy($query, $orderBy);
+ if ($limit > 0) {
+ $query->setMaxResults($limit);
+ }
+ if ($offset > 0) {
+ $query->setFirstResult($offset);
+ }
+ return $query->executeQuery()->fetchAllAssociative();
+ }
+
+
+ /**
+ * @param array|Where $where
+ */
+ private function buildWhere(QueryBuilder $query, array|Where $where): void
+ {
+ if (is_array($where)) {
+ foreach ($where as $column => $value) {
+ if ($value instanceof \BackedEnum) {
+ $value = $value->value;
+ } elseif ($value instanceof \UnitEnum) {
+ $value = $value->name;
+ }
+
+ if (is_null($value)) {
+ $query->andWhere($column . ' IS NULL');
+ } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], $this->comparisonSigns)) {
+ $comparisonSign = $value[0];
+ $comparisonValue = $value[1];
+
+ // Handle special case of "!= null"
+ if ($comparisonSign === '!=' && is_null($comparisonValue)) {
+ $query->andWhere($column . ' IS NOT NULL');
+ } else {
+ $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column)
+ ->setParameter($column, $comparisonValue);
+ }
+ } elseif (is_array($value)) {
+ $placeholders = [];
+ foreach ($value as $index => $val) {
+ $placeholders[] = ':' . $column . $index;
+ $query->setParameter($column . $index, $val);
+ }
+ $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')');
+ } else {
+ $query->andWhere($column . ' = :' . $column)
+ ->setParameter($column, $value);
+ }
+ }
+ } else {
+ $query->where($where->condition);
+ foreach ($where->params as $param => $value) {
+ $query->setParameter($param, $value);
+ }
+ }
+ }
+
+ /**
+ * @param array|string $orderBy
+ */
+ private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void
+ {
+ if (!$orderBy) {
+ return;
+ }
+ if (is_array($orderBy)) {
+ foreach ($orderBy as $column => $direction) {
+ $query->addOrderBy($column, $direction);
+ }
+ } else {
+ foreach (explode(',', $orderBy) as $orderByPart) {
+ $orderByPart = trim($orderByPart);
+ if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
+ $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
+ } else {
+ $query->addOrderBy($orderByPart);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MultiQuery/MultiInsert.php b/src/MultiQuery/MultiInsert.php
new file mode 100644
index 0000000..bfd3fe0
--- /dev/null
+++ b/src/MultiQuery/MultiInsert.php
@@ -0,0 +1,62 @@
+ */
+ private array $parameters = [];
+
+ /**
+ * @param string $tableName
+ * @param list> $rows
+ */
+ public function __construct(Connection $connection, string $tableName, array $rows) {
+ if (!$rows) {
+ return;
+ }
+ $this->connection = $connection;
+ $firstRow = reset($rows);
+ $columnNames = array_map(fn ($columnName) => $this->escapeIdentifier($columnName), array_keys($firstRow));
+ $this->sql = "INSERT INTO " . $this->escapeIdentifier($tableName) . " (" . implode(', ', $columnNames) . ") VALUES ";
+ $valuesSql = [];
+
+ $index = 0;
+ foreach ($rows as $row) {
+ $valuePlaceholder = [];
+ foreach ($row as $column => $value) {
+ $valuePlaceholder[] = ":$column$index";
+ $this->parameters["$column$index"] = $value;
+ }
+ $valuesSql[] = '(' . implode(', ', $valuePlaceholder) . ')';
+ $index++;
+ }
+
+ $this->sql .= implode(', ', $valuesSql) . ';';
+ }
+
+ public function getSql(): string
+ {
+ return $this->sql;
+ }
+
+ /**
+ * @return array
+ */
+ public function getParameters(): array
+ {
+ return $this->parameters;
+ }
+
+ private function getConnection(): Connection
+ {
+ return $this->connection;
+ }
+}
\ No newline at end of file
diff --git a/src/MultiQuery/MultiSelect.php b/src/MultiQuery/MultiSelect.php
new file mode 100644
index 0000000..c5171a7
--- /dev/null
+++ b/src/MultiQuery/MultiSelect.php
@@ -0,0 +1,68 @@
+|array $condition
+ * @throws DbException
+ */
+ public function __construct(
+ Connection $connection,
+ TableConfig $tableConfig,
+ array $condition,
+ ) {
+ $query = $connection->createQueryBuilder()->select('*')->from($tableConfig->tableName);
+ /** @var class-string $class */
+ $class = $tableConfig->entityClass;
+
+ $pkColumns = [];
+ foreach ($tableConfig->primaryKeys as $primaryKeyName) {
+ $pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
+ }
+
+ if (count($pkColumns) === 1) {
+ if (!array_is_list($condition)) {
+ throw new DbException('Input argument $pkList must be list');
+ }
+ /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
+ $pkColumn = reset($pkColumns);
+ $preparedPkValues = array_map(fn ($pk) => (string)$pkColumn->uncast($pk), $condition);
+ $query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues));
+ } else {
+ $expressions = [];
+ foreach ($condition as $i => $pkArray) {
+ if (!is_array($pkArray)) {
+ throw new DbException('For tables with composite keys, input array must consist associative arrays');
+ }
+ $pkOrExpr = [];
+ foreach ($pkArray as $pkName => $pkValue) {
+ if (is_string($pkName) && isset($pkColumns[$pkName])) {
+ $preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
+ $pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
+ $query->setParameter($pkName . $i, $preparedPkValue);
+ }
+ }
+ if ($pkOrExpr) {
+ $expressions[] = $query->expr()->and(...$pkOrExpr);
+ }
+ }
+ $query->where($query->expr()->or(...$expressions));
+ }
+ $this->queryBuilder = $query;
+ }
+
+ public function getQueryBuilder(): QueryBuilder
+ {
+ return $this->queryBuilder;
+ }
+}
\ No newline at end of file
diff --git a/src/TableConfig.php b/src/TableConfig.php
index c19c23e..a33b693 100644
--- a/src/TableConfig.php
+++ b/src/TableConfig.php
@@ -9,16 +9,23 @@
class TableConfig
{
+ /** @var array */
+ private readonly array $entityTraits;
+
+ /**
+ * @param class-string $entityClass
+ * @param string[] $primaryKeys
+ */
public function __construct(
public readonly string $connectionName,
public readonly string $tableName,
public readonly string $entityClass,
public readonly array $primaryKeys,
public readonly ?string $autoIncrementKey = null,
- public readonly bool $isSoftDelete = false,
- public readonly bool $isOptimisticLock = false,
)
- {}
+ {
+ $this->entityTraits = array_fill_keys(class_uses($entityClass), true);
+ }
/**
* @throws EntityException
@@ -26,12 +33,7 @@ public function __construct(
public static function fromEntitySchema(Schema $schema): TableConfig
{
/** @var Attributes\Table|null $tableAttribute */
- $tableAttribute = null;
- foreach ($schema->attributes as $attribute) {
- if ($attribute instanceof Attributes\Table) {
- $tableAttribute = $attribute;
- }
- }
+ $tableAttribute = $schema->getFirstAttributeByClass(Attributes\Table::class);
if (!$tableAttribute) {
throw new EntityException(sprintf(
'Attribute `%s` not found in Entity `%s`',
@@ -40,7 +42,6 @@ public static function fromEntitySchema(Schema $schema): TableConfig
}
$primaryKeys = [];
$autoIncrementKey = null;
- $isSoftDelete = $isOptimisticLock = false;
foreach ($schema->columns as $column) {
foreach ($column->attributes as $attribute) {
@@ -52,21 +53,12 @@ public static function fromEntitySchema(Schema $schema): TableConfig
}
}
}
- foreach (class_uses($schema->class) as $traitClass) {
- if ($traitClass === Traits\SoftDelete::class) {
- $isSoftDelete = true;
- } elseif ($traitClass === Traits\OptimisticLock::class) {
- $isOptimisticLock = true;
- }
- }
return new TableConfig(
connectionName: $tableAttribute->connection,
tableName: $tableAttribute->name,
entityClass: $schema->class,
primaryKeys: $primaryKeys,
autoIncrementKey: $autoIncrementKey,
- isSoftDelete: $isSoftDelete,
- isOptimisticLock: $isOptimisticLock,
);
}
@@ -82,4 +74,19 @@ public function checkEntity(AbstractEntity $entity): void
);
}
}
+
+ public function hasSoftDelete(): bool
+ {
+ return !empty($this->entityTraits[Traits\SoftDelete::class]);
+ }
+
+ public function hasOptimisticLock(): bool
+ {
+ return !empty($this->entityTraits[Traits\OptimisticLock::class]);
+ }
+
+ public function hasUpdatedAt(): bool
+ {
+ return !empty($this->entityTraits[Traits\UpdatedAt::class]);
+ }
}
\ No newline at end of file
diff --git a/src/Traits/OptimisticLock.php b/src/Traits/OptimisticLock.php
index 150c328..73e2a9a 100644
--- a/src/Traits/OptimisticLock.php
+++ b/src/Traits/OptimisticLock.php
@@ -4,5 +4,15 @@
trait OptimisticLock
{
- public int $version = 1;
+ protected int $lock_version = 1;
+
+ public function getVersion(): int
+ {
+ return $this->lock_version;
+ }
+
+ public function incrementVersion(): void
+ {
+ $this->lock_version++;
+ }
}
diff --git a/src/Traits/UpdatedAt.php b/src/Traits/UpdatedAt.php
new file mode 100644
index 0000000..1e8d878
--- /dev/null
+++ b/src/Traits/UpdatedAt.php
@@ -0,0 +1,8 @@
+ 0"
+ * @param array $params params with placeholders, which used in $condition, example: ['user_id' => 123],
+ */
+ public function __construct(
+ public readonly string $condition,
+ public readonly array $params,
+ ) {
+ }
+}
\ No newline at end of file
diff --git a/tests/Attributes/PrimaryKeyAttributeTest.php b/tests/Attributes/PrimaryKeyAttributeTest.php
index 26b6c55..1fd6c72 100644
--- a/tests/Attributes/PrimaryKeyAttributeTest.php
+++ b/tests/Attributes/PrimaryKeyAttributeTest.php
@@ -5,10 +5,11 @@
use Composite\DB\TableConfig;
use Composite\Entity\AbstractEntity;
use Composite\DB\Attributes;
+use PHPUnit\Framework\Attributes\DataProvider;
final class PrimaryKeyAttributeTest extends \PHPUnit\Framework\TestCase
{
- public function primaryKey_dataProvider(): array
+ public static function primaryKey_dataProvider(): array
{
return [
[
@@ -34,9 +35,7 @@ public function __construct(
];
}
- /**
- * @dataProvider primaryKey_dataProvider
- */
+ #[DataProvider('primaryKey_dataProvider')]
public function test_primaryKey(AbstractEntity $entity, array $expected): void
{
$schema = $entity::schema();
diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php
new file mode 100644
index 0000000..d17d625
--- /dev/null
+++ b/tests/Connection/ConnectionManagerTest.php
@@ -0,0 +1,70 @@
+assertInstanceOf(Connection::class, $connection);
+ }
+
+ public static function invalidConfig_dataProvider(): array
+ {
+ $testStandConfigsBaseDir = __DIR__ . '/../TestStand/configs/';
+ return [
+ [
+ '',
+ ],
+ [
+ 'invalid/path',
+ ],
+ [
+ $testStandConfigsBaseDir . 'empty_config.php',
+ ],
+ [
+ $testStandConfigsBaseDir . 'wrong_content_config.php',
+ ],
+ [
+ $testStandConfigsBaseDir . 'wrong_name_config.php',
+ ],
+ [
+ $testStandConfigsBaseDir . 'wrong_params_config.php',
+ ],
+ [
+ $testStandConfigsBaseDir . 'wrong_doctrine_config.php',
+ ],
+ ];
+ }
+
+ #[DataProvider('invalidConfig_dataProvider')]
+ public function test_invalidConfig(string $configPath): void
+ {
+ $reflection = new \ReflectionClass(ConnectionManager::class);
+ $reflection->setStaticPropertyValue('configs', null);
+ $currentPath = getenv('CONNECTIONS_CONFIG_FILE');
+ putenv('CONNECTIONS_CONFIG_FILE=' . $configPath);
+
+ try {
+ ConnectionManager::getConnection('db1');
+ $this->fail('This line should not be reached');
+ } catch (DbException) {
+ $this->assertTrue(true);
+ } finally {
+ putenv('CONNECTIONS_CONFIG_FILE=' . $currentPath);
+ $reflection->setStaticPropertyValue('configs', null);
+ }
+ }
+
+ public function test_getConnectionWithMissingName(): void
+ {
+ $this->expectException(DbException::class);
+ ConnectionManager::getConnection('invalid_name');
+ }
+}
\ No newline at end of file
diff --git a/tests/Table/BaseTableTest.php b/tests/Helpers/CacheHelper.php
similarity index 83%
rename from tests/Table/BaseTableTest.php
rename to tests/Helpers/CacheHelper.php
index 2b811c7..a6ad2ec 100644
--- a/tests/Table/BaseTableTest.php
+++ b/tests/Helpers/CacheHelper.php
@@ -1,11 +1,11 @@
format('U') . '_' . uniqid();
+ }
+}
\ No newline at end of file
diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php
new file mode 100644
index 0000000..69631a9
--- /dev/null
+++ b/tests/MultiQuery/MultiInsertTest.php
@@ -0,0 +1,58 @@
+assertEquals($expectedSql, $multiInserter->getSql());
+ $this->assertEquals($expectedParameters, $multiInserter->getParameters());
+ }
+
+ public static function multiInsertQuery_dataProvider()
+ {
+ return [
+ [
+ 'testTable',
+ [],
+ '',
+ []
+ ],
+ [
+ 'testTable',
+ [
+ ['a' => 'value1_1', 'b' => 'value2_1'],
+ ],
+ 'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0);',
+ ['a0' => 'value1_1', 'b0' => 'value2_1']
+ ],
+ [
+ 'testTable',
+ [
+ ['a' => 'value1_1', 'b' => 'value2_1'],
+ ['a' => 'value1_2', 'b' => 'value2_2']
+ ],
+ 'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0), (:a1, :b1);',
+ ['a0' => 'value1_1', 'b0' => 'value2_1', 'a1' => 'value1_2', 'b1' => 'value2_2']
+ ],
+ [
+ 'testTable',
+ [
+ ['column1' => 'value1_1'],
+ ['column1' => 123]
+ ],
+ 'INSERT INTO "testTable" ("column1") VALUES (:column10), (:column11);',
+ ['column10' => 'value1_1', 'column11' => 123]
+ ]
+ ];
+ }
+}
diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php
index 544c09b..d9e706e 100644
--- a/tests/Table/AbstractCachedTableTest.php
+++ b/tests/Table/AbstractCachedTableTest.php
@@ -3,16 +3,21 @@
namespace Composite\DB\Tests\Table;
use Composite\DB\AbstractCachedTable;
-use Composite\DB\AbstractTable;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Tables;
+use Composite\DB\Where;
use Composite\Entity\AbstractEntity;
+use Composite\DB\Tests\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
-final class AbstractCachedTableTest extends BaseTableTest
+final class AbstractCachedTableTest extends \PHPUnit\Framework\TestCase
{
- public function getOneCacheKey_dataProvider(): array
+ public static function getOneCacheKey_dataProvider(): array
{
- $cache = self::getCache();
+ $cache = Helpers\CacheHelper::getCache();
+ $uuid = Uuid::uuid4();
+ $uuidCacheKey = str_replace('-', '_', (string)$uuid);
return [
[
new Tables\TestAutoincrementCachedTable($cache),
@@ -26,24 +31,19 @@ public function getOneCacheKey_dataProvider(): array
],
[
new Tables\TestUniqueCachedTable($cache),
- new Entities\TestUniqueEntity(id: '123abc', name: 'John'),
- 'sqlite.TestUnique.v1.o.id_123abc',
+ new Entities\TestUniqueEntity(id: $uuid, name: 'John'),
+ 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
],
[
- new Tables\TestUniqueCachedTable($cache),
- new Entities\TestUniqueEntity(
- id: implode('', array_fill(0, 100, 'a')),
- name: 'John',
- ),
- 'ed66f06444d851a981a9ddcecbbf4d5860cd3131',
+ new Tables\TestCompositeCachedTable($cache),
+ new Entities\TestCompositeEntity(user_id: PHP_INT_MAX, post_id: PHP_INT_MAX, message: 'Text'),
+ '69b5bbf599d78f0274feb5cb0e6424f35cca0b57',
],
];
}
- /**
- * @dataProvider getOneCacheKey_dataProvider
- */
- public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object, string $expected): void
+ #[DataProvider('getOneCacheKey_dataProvider')]
+ public function test_getOneCacheKey(AbstractCachedTable $table, AbstractEntity $object, string $expected): void
{
$reflectionMethod = new \ReflectionMethod($table, 'getOneCacheKey');
$actual = $reflectionMethod->invoke($table, $object);
@@ -51,89 +51,102 @@ public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object
}
- public function getCountCacheKey_dataProvider(): array
+ public static function getCountCacheKey_dataProvider(): array
{
return [
[
- '',
[],
'sqlite.TestAutoincrement.v1.c.all',
],
[
- 'name = :name',
- ['name' => 'John'],
+ new Where('name = :name', ['name' => 'John']),
'sqlite.TestAutoincrement.v1.c.name_eq_john',
],
[
- ' name = :name ',
['name' => 'John'],
+ 'sqlite.TestAutoincrement.v1.c.name_john',
+ ],
+ [
+ new Where(' name = :name ', ['name' => 'John']),
'sqlite.TestAutoincrement.v1.c.name_eq_john',
],
[
- 'name=:name',
- ['name' => 'John'],
+ new Where('name=:name', ['name' => 'John']),
'sqlite.TestAutoincrement.v1.c.name_eq_john',
],
[
- 'name = :name AND id > :id',
- ['name' => 'John', 'id' => 10],
+ new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
'sqlite.TestAutoincrement.v1.c.name_eq_john_and_id_gt_10',
],
+ [
+ ['name' => 'John', 'id' => ['>', 10]],
+ 'sqlite.TestAutoincrement.v1.c.name_john_id_gt_10',
+ ],
];
}
- /**
- * @dataProvider getCountCacheKey_dataProvider
- */
- public function test_getCountCacheKey(string $whereString, array $whereParams, string $expected): void
+ #[DataProvider('getCountCacheKey_dataProvider')]
+ public function test_getCountCacheKey(array|Where $where, string $expected): void
{
- $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+ $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
$reflectionMethod = new \ReflectionMethod($table, 'getCountCacheKey');
- $actual = $reflectionMethod->invoke($table, $whereString, $whereParams);
+ $actual = $reflectionMethod->invoke($table, $where);
$this->assertEquals($expected, $actual);
}
- public function getListCacheKey_dataProvider(): array
+ public static function getListCacheKey_dataProvider(): array
{
return [
[
- '',
[],
[],
null,
'sqlite.TestAutoincrement.v1.l.all',
],
[
- '',
[],
[],
10,
'sqlite.TestAutoincrement.v1.l.all.limit_10',
],
[
- '',
[],
['id' => 'DESC'],
10,
'sqlite.TestAutoincrement.v1.l.all.ob_id_desc.limit_10',
],
[
- 'name = :name',
+ new Where('name = :name', ['name' => 'John']),
+ [],
+ null,
+ 'sqlite.TestAutoincrement.v1.l.name_eq_john',
+ ],
+ [
['name' => 'John'],
[],
null,
+ 'sqlite.TestAutoincrement.v1.l.name_john',
+ ],
+ [
+ new Where('name = :name', ['name' => 'John']),
+ [],
+ null,
'sqlite.TestAutoincrement.v1.l.name_eq_john',
],
[
- 'name = :name AND id > :id',
- ['name' => 'John', 'id' => 10],
+ new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
[],
null,
'sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10',
],
[
- 'name = :name AND id > :id',
- ['name' => 'John', 'id' => 10],
+ ['name' => 'John', 'id' => ['>', 10]],
+ [],
+ null,
+ 'sqlite.TestAutoincrement.v1.l.name_john_id_gt_10',
+ ],
+ [
+ new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]),
['id' => 'ASC'],
20,
'bbcf331b765b682da02c4d21dbaa3342bf2c3f18', //sha1('sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10.ob_id_asc.limit_20')
@@ -141,19 +154,17 @@ public function getListCacheKey_dataProvider(): array
];
}
- /**
- * @dataProvider getListCacheKey_dataProvider
- */
- public function test_getListCacheKey(string $whereString, array $whereArray, array $orderBy, ?int $limit, string $expected): void
+ #[DataProvider('getListCacheKey_dataProvider')]
+ public function test_getListCacheKey(array|Where $where, array $orderBy, ?int $limit, string $expected): void
{
- $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+ $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
$reflectionMethod = new \ReflectionMethod($table, 'getListCacheKey');
- $actual = $reflectionMethod->invoke($table, $whereString, $whereArray, $orderBy, $limit);
+ $actual = $reflectionMethod->invoke($table, $where, $orderBy, $limit);
$this->assertEquals($expected, $actual);
}
- public function getCustomCacheKey_dataProvider(): array
+ public static function getCustomCacheKey_dataProvider(): array
{
return [
[
@@ -179,23 +190,23 @@ public function getCustomCacheKey_dataProvider(): array
];
}
- /**
- * @dataProvider getCustomCacheKey_dataProvider
- */
+ #[DataProvider('getCustomCacheKey_dataProvider')]
public function test_getCustomCacheKey(array $parts, string $expected): void
{
- $table = new Tables\TestAutoincrementCachedTable(self::getCache());
+ $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
$reflectionMethod = new \ReflectionMethod($table, 'buildCacheKey');
$actual = $reflectionMethod->invoke($table, ...$parts);
$this->assertEquals($expected, $actual);
}
- public function collectCacheKeysByEntity_dataProvider(): array
+ public static function collectCacheKeysByEntity_dataProvider(): array
{
+ $uuid = Uuid::uuid4();
+ $uuidCacheKey = str_replace('-', '_', (string)$uuid);
return [
[
new Entities\TestAutoincrementEntity(name: 'foo'),
- new Tables\TestAutoincrementCachedTable(self::getCache()),
+ new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
[
'sqlite.TestAutoincrement.v1.o.name_foo',
'sqlite.TestAutoincrement.v1.l.name_eq_foo',
@@ -204,7 +215,7 @@ public function collectCacheKeysByEntity_dataProvider(): array
],
[
Entities\TestAutoincrementEntity::fromArray(['id' => 123, 'name' => 'bar']),
- new Tables\TestAutoincrementCachedTable(self::getCache()),
+ new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
[
'sqlite.TestAutoincrement.v1.o.name_bar',
'sqlite.TestAutoincrement.v1.l.name_eq_bar',
@@ -213,33 +224,54 @@ public function collectCacheKeysByEntity_dataProvider(): array
],
],
[
- new Entities\TestUniqueEntity(id: '123abc', name: 'foo'),
- new Tables\TestUniqueCachedTable(self::getCache()),
+ new Entities\TestUniqueEntity(id: $uuid, name: 'foo'),
+ new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
[
'sqlite.TestUnique.v1.l.name_eq_foo',
'sqlite.TestUnique.v1.c.name_eq_foo',
- 'sqlite.TestUnique.v1.o.id_123abc',
+ 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
],
],
[
- Entities\TestUniqueEntity::fromArray(['id' => '456def', 'name' => 'bar']),
- new Tables\TestUniqueCachedTable(self::getCache()),
+ Entities\TestUniqueEntity::fromArray(['id' => $uuid, 'name' => 'bar']),
+ new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
[
'sqlite.TestUnique.v1.l.name_eq_bar',
'sqlite.TestUnique.v1.c.name_eq_bar',
- 'sqlite.TestUnique.v1.o.id_456def',
+ 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey,
],
],
];
}
- /**
- * @dataProvider collectCacheKeysByEntity_dataProvider
- */
+ #[DataProvider('collectCacheKeysByEntity_dataProvider')]
public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCachedTable $table, array $expected): void
{
$reflectionMethod = new \ReflectionMethod($table, 'collectCacheKeysByEntity');
$actual = $reflectionMethod->invoke($table, $entity);
$this->assertEquals($expected, $actual);
}
+
+ public function test_findMulti(): void
+ {
+ $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache());
+ $e1 = new Entities\TestAutoincrementEntity('John');
+ $e2 = new Entities\TestAutoincrementEntity('Constantine');
+
+ $table->save($e1);
+ $table->save($e2);
+
+ $multi1 = $table->findMulti([$e1->id], 'id');
+ $this->assertEquals($e1, $multi1[$e1->id]);
+
+ $multi2 = $table->findMulti([$e1->id, $e2->id]);
+ $this->assertEquals($e1, $multi2[0]);
+ $this->assertEquals($e2, $multi2[1]);
+
+ $e11 = $table->findByPk($e1->id);
+ $this->assertEquals($e1, $e11);
+
+ $e111 = $table->findByPk($e1->id);
+ $this->assertEquals($e1, $e111);
+ }
}
\ No newline at end of file
diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php
index 0a7e78c..6aaa0a0 100644
--- a/tests/Table/AbstractTableTest.php
+++ b/tests/Table/AbstractTableTest.php
@@ -3,17 +3,20 @@
namespace Composite\DB\Tests\Table;
use Composite\DB\AbstractTable;
-use Composite\DB\ConnectionManager;
-use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Tables;
use Composite\Entity\AbstractEntity;
use Composite\Entity\Exceptions\EntityException;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
+use Ramsey\Uuid\UuidInterface;
-final class AbstractTableTest extends BaseTableTest
+final class AbstractTableTest extends \PHPUnit\Framework\TestCase
{
- public function getPkCondition_dataProvider(): array
+ public static function getPkCondition_dataProvider(): array
{
+ $uuid = Uuid::uuid4();
return [
[
new Tables\TestAutoincrementTable(),
@@ -37,156 +40,183 @@ public function getPkCondition_dataProvider(): array
],
[
new Tables\TestUniqueTable(),
- new Entities\TestUniqueEntity(id: '123abc', name: 'John'),
- ['id' => '123abc'],
+ new Entities\TestUniqueEntity(id: $uuid, name: 'John'),
+ ['id' => $uuid->toString()],
],
[
new Tables\TestUniqueTable(),
- '123abc',
- ['id' => '123abc'],
+ $uuid,
+ ['id' => $uuid->toString()],
],
[
new Tables\TestAutoincrementSdTable(),
Entities\TestAutoincrementSdEntity::fromArray(['id' => 123, 'name' => 'John']),
['id' => 123],
],
- [
- new Tables\TestCompositeSdTable(),
- new Entities\TestCompositeSdEntity(user_id: 123, post_id: 456, message: 'Text'),
- ['user_id' => 123, 'post_id' => 456],
- ],
- [
- new Tables\TestUniqueSdTable(),
- new Entities\TestUniqueSdEntity(id: '123abc', name: 'John'),
- ['id' => '123abc'],
- ],
];
}
- /**
- * @dataProvider getPkCondition_dataProvider
- */
- public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity $object, array $expected): void
+ #[DataProvider('getPkCondition_dataProvider')]
+ public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity|UuidInterface $object, array $expected): void
{
$reflectionMethod = new \ReflectionMethod($table, 'getPkCondition');
$actual = $reflectionMethod->invoke($table, $object);
$this->assertEquals($expected, $actual);
}
- public function enrichCondition_dataProvider(): array
+ public function test_illegalEntitySave(): void
{
- return [
- [
- new Tables\TestAutoincrementTable(),
- ['id' => 123],
- ['id' => 123],
- ],
- [
- new Tables\TestCompositeTable(),
- ['user_id' => 123, 'post_id' => 456],
- ['user_id' => 123, 'post_id' => 456],
- ],
- [
- new Tables\TestUniqueTable(),
- ['id' => '123abc'],
- ['id' => '123abc'],
- ],
- [
- new Tables\TestAutoincrementSdTable(),
- ['id' => 123],
- ['id' => 123, 'deleted_at' => null],
- ],
- [
- new Tables\TestCompositeSdTable(),
- ['user_id' => 123, 'post_id' => 456],
- ['user_id' => 123, 'post_id' => 456, 'deleted_at' => null],
- ],
- [
- new Tables\TestUniqueSdTable(),
- ['id' => '123abc'],
- ['id' => '123abc', 'deleted_at' => null],
- ],
- ];
- }
+ $entity = new Entities\TestAutoincrementEntity(name: 'Foo');
+ $compositeTable = new Tables\TestUniqueTable();
- /**
- * @dataProvider enrichCondition_dataProvider
- */
- public function test_enrichCondition(AbstractTable $table, array $condition, array $expected): void
- {
- $reflectionMethod = new \ReflectionMethod($table, 'enrichCondition');
- $reflectionMethod->invokeArgs($table, [&$condition]);
- $this->assertEquals($expected, $condition);
+ $this->expectException(EntityException::class);
+ $compositeTable->save($entity);
}
- public function test_illegalEntitySave(): void
+ public function test_illegalCreateEntity(): void
{
- $entity = new Entities\TestAutoincrementEntity(name: 'Foo');
- $compositeTable = new Tables\TestUniqueTable();
+ $table = new Tables\TestStrictTable();
+ $null = $table->buildEntity(['dti1' => 'abc']);
+ $this->assertNull($null);
+
+ $empty = $table->buildEntities([['dti1' => 'abc']]);
+ $this->assertEmpty($empty);
+
+ $empty = $table->buildEntities([]);
+ $this->assertEmpty($empty);
+
+ $empty = $table->buildEntities(false);
+ $this->assertEmpty($empty);
- $exceptionCatch = false;
- try {
- $compositeTable->save($entity);
- } catch (EntityException) {
- $exceptionCatch = true;
- }
- $this->assertTrue($exceptionCatch);
+ $empty = $table->buildEntities('abc');
+ $this->assertEmpty($empty);
+
+ $empty = $table->buildEntities(['abc']);
+ $this->assertEmpty($empty);
}
- public function test_optimisticLock(): void
+ #[DataProvider('buildWhere_dataProvider')]
+ public function test_buildWhere($where, $expectedSQL, $expectedParams): void
{
- //checking that problem exists
- $aiEntity1 = new Entities\TestAutoincrementEntity(name: 'John');
- $aiTable1 = new Tables\TestAutoincrementTable();
- $aiTable2 = new Tables\TestAutoincrementTable();
+ $table = new Tables\TestStrictTable();
- $aiTable1->save($aiEntity1);
+ $selectReflection = new \ReflectionMethod($table, 'select');
+ $selectReflection->setAccessible(true);
- $aiEntity2 = $aiTable2->findByPk($aiEntity1->id);
+ $queryBuilder = $selectReflection->invoke($table);
- $db = ConnectionManager::getConnection($aiTable1->getConnectionName());
+ $buildWhereReflection = new \ReflectionMethod($table, 'buildWhere');
+ $buildWhereReflection->setAccessible(true);
- $db->beginTransaction();
- $aiEntity1->name = 'John1';
- $aiTable1->save($aiEntity1);
+ $buildWhereReflection->invokeArgs($table, [$queryBuilder, $where]);
- $aiEntity2->name = 'John2';
- $aiTable2->save($aiEntity2);
+ $this->assertEquals($expectedSQL, $queryBuilder->getSQL());
+ $this->assertEquals($expectedParams, $queryBuilder->getParameters());
+ }
- $this->assertTrue($db->commit());
+ public static function buildWhere_dataProvider(): array
+ {
+ return [
+ // Scalar value
+ [
+ ['column' => 1],
+ 'SELECT * FROM Strict WHERE column = :column',
+ ['column' => 1]
+ ],
- $aiEntity3 = $aiTable1->findByPk($aiEntity1->id);
- $this->assertEquals('John2', $aiEntity3->name);
+ // Null value
+ [
+ ['column' => null],
+ 'SELECT * FROM Strict WHERE column IS NULL',
+ []
+ ],
- //Checking optimistic lock
- $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John');
- $olTable1 = new Tables\TestOptimisticLockTable();
- $olTable2 = new Tables\TestOptimisticLockTable();
+ // Greater than comparison
+ [
+ ['column' => ['>', 0]],
+ 'SELECT * FROM Strict WHERE column > :column',
+ ['column' => 0]
+ ],
- $olTable1->init();
+ // Less than comparison
+ [
+ ['column' => ['<', 5]],
+ 'SELECT * FROM Strict WHERE column < :column',
+ ['column' => 5]
+ ],
- $olTable1->save($olEntity1);
+ // Greater than or equal to comparison
+ [
+ ['column' => ['>=', 3]],
+ 'SELECT * FROM Strict WHERE column >= :column',
+ ['column' => 3]
+ ],
- $olEntity2 = $olTable2->findByPk($olEntity1->id);
+ // Less than or equal to comparison
+ [
+ ['column' => ['<=', 7]],
+ 'SELECT * FROM Strict WHERE column <= :column',
+ ['column' => 7]
+ ],
- $db->beginTransaction();
- $olEntity1->name = 'John1';
- $olTable1->save($olEntity1);
+ // Not equal to comparison with scalar value
+ [
+ ['column' => ['<>', 10]],
+ 'SELECT * FROM Strict WHERE column <> :column',
+ ['column' => 10]
+ ],
- $olEntity2->name = 'John2';
+ // Not equal to comparison with null
+ [
+ ['column' => ['!=', null]],
+ 'SELECT * FROM Strict WHERE column IS NOT NULL',
+ []
+ ],
- $exceptionCaught = false;
- try {
- $olTable2->save($olEntity2);
- } catch (DbException) {
- $exceptionCaught = true;
- }
- $this->assertTrue($exceptionCaught);
+ // IN condition
+ [
+ ['column' => [1, 2, 3]],
+ 'SELECT * FROM Strict WHERE column IN(:column0, :column1, :column2)',
+ ['column0' => 1, 'column1' => 2, 'column2' => 3]
+ ],
- $this->assertTrue($db->rollBack());
+ // Multiple conditions
+ [
+ ['column1' => 1, 'column2' => null, 'column3' => ['>', 5]],
+ 'SELECT * FROM Strict WHERE (column1 = :column1) AND (column2 IS NULL) AND (column3 > :column3)',
+ ['column1' => 1, 'column3' => 5]
+ ]
+ ];
+ }
+
+ public function test_databaseSpecific(): void
+ {
+ $mySQLTable = new Tables\TestMySQLTable();
+ $this->assertEquals('`column`', $mySQLTable->escapeIdentifierPub('column'));
+ $this->assertEquals('`Database`.`Table`', $mySQLTable->escapeIdentifierPub('Database.Table'));
+
+ $postgresTable = new Tables\TestPostgresTable();
+ $this->assertEquals('"column"', $postgresTable->escapeIdentifierPub('column'));
+ }
- $olEntity3 = $olTable1->findByPk($olEntity1->id);
- $this->assertEquals(1, $olEntity3->version);
- $this->assertEquals('John', $olEntity3->name);
+ public function test_getPkCondition_throwsExceptionWhenNoPrimaryKeys(): void
+ {
+ $mockTable = new class extends AbstractTable {
+ protected function getConfig(): TableConfig
+ {
+ return new TableConfig(
+ connectionName: 'default',
+ tableName: 'test_table',
+ entityClass: AbstractEntity::class,
+ primaryKeys: [], // Empty primary keys
+ );
+ }
+ };
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage("Primary keys are not defined in `" . get_class($mockTable) . "` table config");
+
+ $reflectionMethod = new \ReflectionMethod($mockTable, 'getPkCondition');
+ $reflectionMethod->invoke($mockTable, 1);
}
-}
\ No newline at end of file
+}
diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php
index c1a3535..03a7569 100644
--- a/tests/Table/AutoIncrementTableTest.php
+++ b/tests/Table/AutoIncrementTableTest.php
@@ -2,19 +2,18 @@
namespace Composite\DB\Tests\Table;
+use Composite\DB\AbstractTable;
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\Helpers;
use Composite\DB\Tests\TestStand\Tables;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use PHPUnit\Framework\Attributes\DataProvider;
-final class AutoIncrementTableTest extends BaseTableTest
+final class AutoIncrementTableTest extends \PHPUnit\Framework\TestCase
{
- public static function setUpBeforeClass(): void
- {
- (new Tables\TestAutoincrementTable())->init();
- (new Tables\TestAutoincrementSdTable())->init();
- }
-
- public function crud_dataProvider(): array
+ public static function crud_dataProvider(): array
{
return [
[
@@ -26,25 +25,28 @@ public function crud_dataProvider(): array
Entities\TestAutoincrementSdEntity::class,
],
[
- new Tables\TestAutoincrementCachedTable(self::getCache()),
+ new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()),
Entities\TestAutoincrementEntity::class,
],
[
- new Tables\TestAutoincrementSdCachedTable(self::getCache()),
+ new Tables\TestAutoincrementSdCachedTable(Helpers\CacheHelper::getCache()),
Entities\TestAutoincrementSdEntity::class,
],
];
}
/**
- * @dataProvider crud_dataProvider
+ * @param class-string $class
*/
- public function test_crud(IAutoincrementTable $table, string $class): void
+ #[DataProvider('crud_dataProvider')]
+ public function test_crud(AbstractTable&IAutoincrementTable $table, string $class): void
{
$table->truncate();
+ $tableConfig = TableConfig::fromEntitySchema($class::schema());
$entity = new $class(
- name: $this->getUniqueName(),
+ name: Helpers\StringHelper::getUniqueName(),
+ is_test: true,
);
$this->assertEntityNotExists($table, PHP_INT_MAX, uniqid());
@@ -62,7 +64,81 @@ public function test_crud(IAutoincrementTable $table, string $class): void
$this->assertEquals($newName, $foundEntity->name);
$table->delete($entity);
- $this->assertEntityNotExists($table, $entity->id, $entity->name);
+ if ($tableConfig->hasSoftDelete()) {
+ /** @var Entities\TestAutoincrementSdEntity $deletedEntity */
+ $deletedEntity = $table->findByPk($entity->id);
+ $this->assertTrue($deletedEntity->isDeleted());
+ } else {
+ $this->assertEntityNotExists($table, $entity->id, $entity->name);
+ }
+
+ $e1 = new $class(Helpers\StringHelper::getUniqueName());
+ $e2 = new $class(Helpers\StringHelper::getUniqueName());
+
+ $table->save($e1);
+ $table->save($e2);
+ $this->assertEntityExists($table, $e1);
+ $this->assertEntityExists($table, $e2);
+
+ $recentEntities = $table->findRecent(2, 0);
+ $this->assertEquals($e2, $recentEntities[0]);
+ $this->assertEquals($e1, $recentEntities[1]);
+ $preLastEntity = $table->findRecent(1, 1);
+ $this->assertEquals($e1, $preLastEntity[0]);
+
+ if ($tableConfig->hasSoftDelete()) {
+ $e1->name = 'Exception';
+ $exceptionThrown = false;
+ try {
+ $table->deleteMany([$e1, $e2]);
+ } catch (\Exception) {
+ $exceptionThrown = true;
+ }
+ $this->assertTrue($exceptionThrown);
+ $e1->name = Helpers\StringHelper::getUniqueName();
+ }
+
+ $table->deleteMany([$e1, $e2]);
+
+ if ($tableConfig->hasSoftDelete()) {
+ /** @var Entities\TestAutoincrementSdEntity $deletedEntity1 */
+ $deletedEntity1 = $table->findByPk($e1->id);
+ $this->assertTrue($deletedEntity1->isDeleted());
+
+ /** @var Entities\TestAutoincrementSdEntity $deletedEntity2 */
+ $deletedEntity2 = $table->findByPk($e2->id);
+ $this->assertTrue($deletedEntity2->isDeleted());
+ } else {
+ $this->assertEntityNotExists($table, $e1->id, $e1->name);
+ $this->assertEntityNotExists($table, $e2->id, $e2->name);
+ }
+ }
+
+ public function test_getMulti(): void
+ {
+ $table = new Tables\TestAutoincrementTable();
+
+ $e1 = new Entities\TestAutoincrementEntity('name1');
+ $e2 = new Entities\TestAutoincrementEntity('name2');
+ $e3 = new Entities\TestAutoincrementEntity('name3');
+
+ $table->save($e1);
+ $table->save($e2);
+ $table->save($e3);
+
+ $multiResult = $table->findMulti([$e1->id, $e2->id, $e3->id]);
+ $this->assertEquals($e1, $multiResult[$e1->id]);
+ $this->assertEquals($e2, $multiResult[$e2->id]);
+ $this->assertEquals($e3, $multiResult[$e3->id]);
+
+ $this->assertEmpty($table->findMulti([]));
+ }
+
+ public function test_illegalGetMulti(): void
+ {
+ $table = new Tables\TestAutoincrementTable();
+ $this->expectException(DbException::class);
+ $table->findMulti(['a' => 1]);
}
private function assertEntityExists(IAutoincrementTable $table, Entities\TestAutoincrementEntity $entity): void
diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php
index 7108179..49d9447 100644
--- a/tests/Table/CombinedTransactionTest.php
+++ b/tests/Table/CombinedTransactionTest.php
@@ -6,8 +6,10 @@
use Composite\DB\Exceptions\DbException;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Tables;
+use Composite\DB\Tests\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
-final class CombinedTransactionTest extends BaseTableTest
+final class CombinedTransactionTest extends \PHPUnit\Framework\TestCase
{
public function test_transactionCommit(): void
{
@@ -19,7 +21,7 @@ public function test_transactionCommit(): void
$e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
$saveTransaction->save($autoIncrementTable, $e1);
- $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000), message: 'Bar');
+ $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar');
$saveTransaction->save($compositeTable, $e2);
$saveTransaction->commit();
@@ -36,6 +38,32 @@ public function test_transactionCommit(): void
$this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
}
+ public function test_saveDeleteMany(): void
+ {
+ $autoIncrementTable = new Tables\TestAutoincrementTable();
+ $compositeTable = new Tables\TestCompositeTable();
+
+ $saveTransaction = new CombinedTransaction();
+
+ $e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
+ $saveTransaction->save($autoIncrementTable, $e1);
+
+ $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Foo');
+ $e3 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar');
+ $saveTransaction->saveMany($compositeTable, [$e2, $e3]);
+
+ $saveTransaction->commit();
+
+ $this->assertNotNull($autoIncrementTable->findByPk($e1->id));
+ $this->assertNotNull($compositeTable->findOne($e2->user_id, $e2->post_id));
+ $this->assertNotNull($compositeTable->findOne($e3->user_id, $e3->post_id));
+
+ $deleteTransaction = new CombinedTransaction();
+ $deleteTransaction->delete($autoIncrementTable, $e1);
+ $deleteTransaction->deleteMany($compositeTable, [$e2, $e3]);
+ $deleteTransaction->commit();
+ }
+
public function test_transactionRollback(): void
{
$autoIncrementTable = new Tables\TestAutoincrementTable();
@@ -55,7 +83,7 @@ public function test_transactionRollback(): void
$this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
}
- public function test_transactionException(): void
+ public function test_failedSave(): void
{
$autoIncrementTable = new Tables\TestAutoincrementTable();
$compositeTable = new Tables\TestCompositeTable();
@@ -69,17 +97,69 @@ public function test_transactionException(): void
try {
$transaction->save($compositeTable, $e2);
$transaction->commit();
- $this->assertFalse(true, 'This line should not be reached');
+ $this->fail('This line should not be reached');
} catch (DbException) {}
$this->assertNull($autoIncrementTable->findByPk($e1->id));
$this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id));
}
+ public function test_failedDelete(): void
+ {
+ $autoIncrementTable = new Tables\TestAutoincrementTable();
+ $compositeTable = new Tables\TestCompositeTable();
+
+ $aiEntity = new Entities\TestAutoincrementEntity(name: 'Foo');
+ $cEntity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');;
+
+ $autoIncrementTable->save($aiEntity);
+ $compositeTable->save($cEntity);
+
+ $transaction = new CombinedTransaction();
+ try {
+ $aiEntity->name = 'Foo1';
+ $cEntity->message = 'Exception';
+
+ $transaction->save($autoIncrementTable, $aiEntity);
+ $transaction->delete($compositeTable, $cEntity);
+
+ $transaction->commit();
+ $this->fail('This line should not be reached');
+ } catch (DbException) {}
+
+ $this->assertEquals('Foo', $autoIncrementTable->findByPk($aiEntity->id)->name);
+ $this->assertNotNull($compositeTable->findOne($cEntity->user_id, $cEntity->post_id));
+ }
+
+ public function test_try(): void
+ {
+ $compositeTable = new Tables\TestCompositeTable();
+ $entity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');;
+
+ try {
+ $transaction = new CombinedTransaction();
+ $transaction->save($compositeTable, $entity);
+ $transaction->try(fn() => throw new \Exception('test'));
+ $transaction->commit();
+ } catch (DbException) {}
+ $this->assertNull($compositeTable->findOne($entity->user_id, $entity->post_id));
+ }
+
+ public function test_lockFailed(): void
+ {
+ $cache = new Helpers\FalseCache();
+ $keyParts = [uniqid()];
+ $transaction = new CombinedTransaction();
+
+ $this->expectException(DbException::class);
+ $transaction->lock($cache, $keyParts);
+ }
+
public function test_lock(): void
{
- $cache = self::getCache();
+ $cache = Helpers\CacheHelper::getCache();
$table = new Tables\TestAutoincrementTable();
+
$e1 = new Entities\TestAutoincrementEntity(name: 'Foo');
$e2 = new Entities\TestAutoincrementEntity(name: 'Bar');
@@ -90,8 +170,10 @@ public function test_lock(): void
$transaction2 = new CombinedTransaction();
try {
$transaction2->lock($cache, $keyParts);
- $this->assertFalse(false, 'Lock should not be free');
- } catch (DbException) {}
+ $this->fail('Lock should not be free');
+ } catch (DbException) {
+ $this->assertTrue(true);
+ }
$transaction1->save($table, $e1);
$transaction1->commit();
@@ -103,4 +185,61 @@ public function test_lock(): void
$this->assertNotEmpty($table->findByPk($e1->id));
$this->assertNotEmpty($table->findByPk($e2->id));
}
-}
\ No newline at end of file
+
+ #[DataProvider('buildLockKey_dataProvider')]
+ public function test_buildLockKey($keyParts, $expectedResult)
+ {
+ $reflection = new \ReflectionClass(CombinedTransaction::class);
+ $object = new CombinedTransaction();
+ $result = $reflection->getMethod('buildLockKey')->invoke($object, $keyParts);
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public static function buildLockKey_dataProvider()
+ {
+ return [
+ 'empty array' => [[], 'composite.lock'],
+ 'one element' => [['element'], 'composite.lock.element'],
+ 'exact length' => [[str_repeat('a', 49)], 'composite.lock.' . str_repeat('a', 49)],
+ 'more than max length' => [[str_repeat('a', 55)], sha1('composite.lock.' . str_repeat('a', 55))],
+ ];
+ }
+
+ public function test_saveWithNoChanges(): void
+ {
+ $autoIncrementTable = new Tables\TestAutoincrementTable();
+
+ $entity = new Entities\TestAutoincrementEntity(name: 'TestNoChanges');
+ $autoIncrementTable->save($entity);
+
+ $transaction = new CombinedTransaction();
+ $transaction->save($autoIncrementTable, $entity);
+ $transaction->commit();
+
+ $this->assertNotNull($autoIncrementTable->findByPk($entity->id));
+
+ $autoIncrementTable->delete($entity);
+ }
+
+ public function test_saveManyWithEmptyEntities(): void
+ {
+ $autoIncrementTable = new Tables\TestAutoincrementTable();
+
+ $transaction = new CombinedTransaction();
+ $transaction->saveMany($autoIncrementTable, []);
+ $transaction->commit();
+
+ $this->assertTrue(true);
+ }
+
+ public function test_deleteManyWithEmptyEntities(): void
+ {
+ $autoIncrementTable = new Tables\TestAutoincrementTable();
+
+ $transaction = new CombinedTransaction();
+ $transaction->deleteMany($autoIncrementTable, []);
+ $transaction->commit();
+
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php
index 87bc767..09f784b 100644
--- a/tests/Table/CompositeTableTest.php
+++ b/tests/Table/CompositeTableTest.php
@@ -2,19 +2,18 @@
namespace Composite\DB\Tests\Table;
+use Composite\DB\AbstractTable;
+use Composite\DB\Exceptions\DbException;
+use Composite\DB\TableConfig;
+use Composite\DB\Tests\Helpers;
use Composite\DB\Tests\TestStand\Tables;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable;
+use PHPUnit\Framework\Attributes\DataProvider;
-final class CompositeTableTest extends BaseTableTest
+final class CompositeTableTest extends \PHPUnit\Framework\TestCase
{
- public static function setUpBeforeClass(): void
- {
- (new Tables\TestCompositeTable())->init();
- (new Tables\TestCompositeSdTable())->init();
- }
-
- public function crud_dataProvider(): array
+ public static function crud_dataProvider(): array
{
return [
[
@@ -22,31 +21,25 @@ public function crud_dataProvider(): array
Entities\TestCompositeEntity::class,
],
[
- new Tables\TestCompositeSdTable(),
- Entities\TestCompositeSdEntity::class,
- ],
- [
- new Tables\TestCompositeCachedTable(self::getCache()),
+ new Tables\TestCompositeCachedTable(Helpers\CacheHelper::getCache()),
Entities\TestCompositeEntity::class,
],
- [
- new Tables\TestCompositeSdCachedTable(self::getCache()),
- Entities\TestCompositeSdEntity::class,
- ],
];
}
/**
- * @dataProvider crud_dataProvider
+ * @param class-string $class
+ * @throws \Throwable
*/
- public function test_crud(ICompositeTable $table, string $class): void
+ #[DataProvider('crud_dataProvider')]
+ public function test_crud(AbstractTable&ICompositeTable $table, string $class): void
{
$table->truncate();
$entity = new $class(
user_id: mt_rand(1, 1000000),
post_id: mt_rand(1, 1000000),
- message: $this->getUniqueName(),
+ message: Helpers\StringHelper::getUniqueName(),
);
$this->assertEntityNotExists($table, $entity);
$table->save($entity);
@@ -59,13 +52,74 @@ public function test_crud(ICompositeTable $table, string $class): void
$table->delete($entity);
$this->assertEntityNotExists($table, $entity);
- $newEntity = new $entity(
- user_id: $entity->user_id,
- post_id: $entity->post_id,
- message: 'Hello User',
+ $e1 = new $class(
+ user_id: mt_rand(1, 1000000),
+ post_id: mt_rand(1, 1000000),
+ message: Helpers\StringHelper::getUniqueName(),
+ );
+ $e2 = new $class(
+ user_id: mt_rand(1, 1000000),
+ post_id: mt_rand(1, 1000000),
+ message: Helpers\StringHelper::getUniqueName(),
+ );
+
+ $table->saveMany([$e1, $e2]);
+ $e1->resetChangedColumns();
+ $e2->resetChangedColumns();
+
+ $this->assertEntityExists($table, $e1);
+ $this->assertEntityExists($table, $e2);
+
+ $table->deleteMany([$e1, $e2]);
+
+ $this->assertEntityNotExists($table, $e1);
+ $this->assertEntityNotExists($table, $e2);
+ }
+
+ public function test_getMulti(): void
+ {
+ $table = new Tables\TestCompositeTable();
+ $userId = mt_rand(1, 1000000);
+
+ $e1 = new Entities\TestCompositeEntity(
+ user_id: $userId,
+ post_id: mt_rand(1, 1000000),
+ message: Helpers\StringHelper::getUniqueName(),
);
- $table->save($newEntity);
- $this->assertEntityExists($table, $newEntity);
+
+ $e2 = new Entities\TestCompositeEntity(
+ user_id: $userId,
+ post_id: mt_rand(1, 1000000),
+ message: Helpers\StringHelper::getUniqueName(),
+ );
+
+ $e3 = new Entities\TestCompositeEntity(
+ user_id: $userId,
+ post_id: mt_rand(1, 1000000),
+ message: Helpers\StringHelper::getUniqueName(),
+ );
+
+ $table->saveMany([$e1, $e2, $e3]);
+
+ $e1->resetChangedColumns();
+ $e2->resetChangedColumns();
+ $e3->resetChangedColumns();
+
+ $multiResult = $table->findMulti([
+ ['user_id' => $e1->user_id, 'post_id' => $e1->post_id],
+ ['user_id' => $e2->user_id, 'post_id' => $e2->post_id],
+ ['user_id' => $e3->user_id, 'post_id' => $e3->post_id],
+ ]);
+ $this->assertEquals($e1, $multiResult[$e1->post_id]);
+ $this->assertEquals($e2, $multiResult[$e2->post_id]);
+ $this->assertEquals($e3, $multiResult[$e3->post_id]);
+ }
+
+ public function test_illegalGetMulti(): void
+ {
+ $table = new Tables\TestCompositeTable();
+ $this->expectException(DbException::class);
+ $table->findMulti(['a']);
}
private function assertEntityExists(ICompositeTable $table, Entities\TestCompositeEntity $entity): void
diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php
index b4c558e..e74f318 100644
--- a/tests/Table/TableConfigTest.php
+++ b/tests/Table/TableConfigTest.php
@@ -6,6 +6,7 @@
use Composite\DB\TableConfig;
use Composite\DB\Traits;
use Composite\Entity\AbstractEntity;
+use Composite\Entity\Exceptions\EntityException;
use Composite\Entity\Schema;
final class TableConfigTest extends \PHPUnit\Framework\TestCase
@@ -26,12 +27,28 @@ public function __construct(
private \DateTimeImmutable $dt = new \DateTimeImmutable(),
) {}
};
- $schema = Schema::build($class::class);
+ $schema = new Schema($class::class);
$tableConfig = TableConfig::fromEntitySchema($schema);
$this->assertNotEmpty($tableConfig->connectionName);
$this->assertNotEmpty($tableConfig->tableName);
- $this->assertTrue($tableConfig->isSoftDelete);
+ $this->assertTrue($tableConfig->hasSoftDelete());
$this->assertCount(1, $tableConfig->primaryKeys);
$this->assertSame('id', $tableConfig->autoIncrementKey);
}
+
+ public function test_missingAttribute(): void
+ {
+ $class = new
+ class extends AbstractEntity {
+ #[Attributes\PrimaryKey(autoIncrement: true)]
+ public readonly int $id;
+
+ public function __construct(
+ public string $str = 'abc',
+ ) {}
+ };
+ $schema = new Schema($class::class);
+ $this->expectException(EntityException::class);
+ TableConfig::fromEntitySchema($schema);
+ }
}
\ No newline at end of file
diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php
index cb834c3..abeb9ff 100644
--- a/tests/Table/UniqueTableTest.php
+++ b/tests/Table/UniqueTableTest.php
@@ -2,19 +2,17 @@
namespace Composite\DB\Tests\Table;
+use Composite\DB\AbstractTable;
+use Composite\DB\Tests\Helpers;
use Composite\DB\Tests\TestStand\Entities;
use Composite\DB\Tests\TestStand\Tables;
use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Ramsey\Uuid\Uuid;
-final class UniqueTableTest extends BaseTableTest
+final class UniqueTableTest extends \PHPUnit\Framework\TestCase
{
- public static function setUpBeforeClass(): void
- {
- (new Tables\TestUniqueTable())->init();
- (new Tables\TestUniqueSdTable())->init();
- }
-
- public function crud_dataProvider(): array
+ public static function crud_dataProvider(): array
{
return [
[
@@ -22,30 +20,23 @@ public function crud_dataProvider(): array
Entities\TestUniqueEntity::class,
],
[
- new Tables\TestUniqueSdTable(),
- Entities\TestUniqueSdEntity::class,
- ],
- [
- new Tables\TestUniqueCachedTable(self::getCache()),
+ new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()),
Entities\TestUniqueEntity::class,
],
- [
- new Tables\TestUniqueSdCachedTable(self::getCache()),
- Entities\TestUniqueSdEntity::class,
- ],
];
}
/**
- * @dataProvider crud_dataProvider
+ * @param class-string $class
*/
- public function test_crud(IUniqueTable $table, string $class): void
+ #[DataProvider('crud_dataProvider')]
+ public function test_crud(AbstractTable&IUniqueTable $table, string $class): void
{
$table->truncate();
$entity = new $class(
- id: uniqid(),
- name: $this->getUniqueName(),
+ id: Uuid::uuid4(),
+ name: Helpers\StringHelper::getUniqueName(),
);
$this->assertEntityNotExists($table, $entity);
$table->save($entity);
@@ -57,16 +48,55 @@ public function test_crud(IUniqueTable $table, string $class): void
$table->delete($entity);
$this->assertEntityNotExists($table, $entity);
+ }
- $newEntity = new $entity(
- id: $entity->id,
- name: $entity->name . ' new',
+ public function test_multiSave(): void
+ {
+ $e1 = new Entities\TestUniqueEntity(
+ id: Uuid::uuid4(),
+ name: Helpers\StringHelper::getUniqueName(),
);
- $table->save($newEntity);
- $this->assertEntityExists($table, $newEntity);
+ $e2 = new Entities\TestUniqueEntity(
+ id: Uuid::uuid4(),
+ name: Helpers\StringHelper::getUniqueName(),
+ );
+ $e3 = new Entities\TestUniqueEntity(
+ id: Uuid::uuid4(),
+ name: Helpers\StringHelper::getUniqueName(),
+ );
+ $e4 = new Entities\TestUniqueEntity(
+ id: Uuid::uuid4(),
+ name: Helpers\StringHelper::getUniqueName(),
+ );
+ $table = new Tables\TestUniqueTable();
+ $table->saveMany([$e1, $e2]);
+
+ $this->assertEntityExists($table, $e1);
+ $this->assertEntityExists($table, $e2);
+
+ $e1->resetChangedColumns();
+ $e2->resetChangedColumns();
+
+ $e1->name = 'Exception';
+
+ $exceptionThrown = false;
+ try {
+ $table->saveMany([$e1, $e2, $e3, $e4]);
+ } catch (\Exception) {
+ $exceptionThrown = true;
+ }
+ $this->assertTrue($exceptionThrown);
+ $this->assertEntityNotExists($table, $e3);
+ $this->assertEntityNotExists($table, $e4);
+
+ $e1->name = 'NonException';
+
+ $table->saveMany([$e1, $e2, $e3, $e4]);
- $table->delete($newEntity);
- $this->assertEntityNotExists($table, $newEntity);
+ $this->assertEntityExists($table, $e1);
+ $this->assertEntityExists($table, $e2);
+ $this->assertEntityExists($table, $e3);
+ $this->assertEntityExists($table, $e4);
}
private function assertEntityExists(IUniqueTable $table, Entities\TestUniqueEntity $entity): void
diff --git a/tests/TestStand/Entities/Castable/TestCastableIntObject.php b/tests/TestStand/Entities/Castable/TestCastableIntObject.php
deleted file mode 100644
index 56baddf..0000000
--- a/tests/TestStand/Entities/Castable/TestCastableIntObject.php
+++ /dev/null
@@ -1,32 +0,0 @@
-format('U');
- return $unixTime === 0 ? null : $unixTime ;
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Castable/TestCastableStringObject.php b/tests/TestStand/Entities/Castable/TestCastableStringObject.php
deleted file mode 100644
index 2a09e03..0000000
--- a/tests/TestStand/Entities/Castable/TestCastableStringObject.php
+++ /dev/null
@@ -1,26 +0,0 @@
-value ? '_' . $this->value . '_' : null;
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Entities/Enums/TestBackedEnum.php b/tests/TestStand/Entities/Enums/TestBackedEnum.php
new file mode 100644
index 0000000..107d79f
--- /dev/null
+++ b/tests/TestStand/Entities/Enums/TestBackedEnum.php
@@ -0,0 +1,12 @@
+init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema());
@@ -19,26 +26,34 @@ protected function getFlushCacheKeys(TestAutoincrementEntity|AbstractEntity $ent
{
$keys = [
$this->getOneCacheKey(['name' => $entity->name]),
- $this->getListCacheKey('name = :name', ['name' => $entity->name]),
- $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+ $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+ $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
];
$oldName = $entity->getOldValue('name');
if (!$entity->isNew() && $oldName !== $entity->name) {
$keys[] = $this->getOneCacheKey(['name' => $oldName]);
- $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]);
- $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]);
+ $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName]));
+ $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName]));
}
return $keys;
}
public function findByPk(int $id): ?TestAutoincrementEntity
{
- return $this->createEntity($this->findByPkCachedInternal($id));
+ return $this->_findByPkCached($id);
}
public function findOneByName(string $name): ?TestAutoincrementEntity
{
- return $this->createEntity($this->findOneCachedInternal(['name' => $name]));
+ return $this->_findOneCached(['name' => $name]);
+ }
+
+ public function delete(TestAutoincrementEntity|AbstractEntity $entity): void
+ {
+ if ($entity->name === 'Exception') {
+ throw new \Exception('Test Exception');
+ }
+ parent::delete($entity);
}
/**
@@ -46,20 +61,34 @@ public function findOneByName(string $name): ?TestAutoincrementEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- ));
+ return $this->_findAllCached(new Where('name = :name', ['name' => $name]));
}
- public function countAllByName(string $name): int
+ /**
+ * @return TestAutoincrementEntity[]
+ */
+ public function findRecent(int $limit, int $offset): array
{
- return $this->countAllCachedInternal(
- 'name = :name',
- ['name' => $name],
+ return $this->_findAll(
+ orderBy: ['id' => 'DESC'],
+ limit: $limit,
+ offset: $offset,
);
}
+ public function countAllByName(string $name): int
+ {
+ return $this->_countByAllCached(new Where('name = :name', ['name' => $name]));
+ }
+
+ /**
+ * @return TestAutoincrementEntity[]
+ */
+ public function findMulti(array $ids, ?string $keyColumnName = null): array
+ {
+ return $this->_findMultiCached(ids: $ids, keyColumnName: $keyColumnName);
+ }
+
public function truncate(): void
{
$this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
index 007a187..07ba91d 100644
--- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php
@@ -6,10 +6,17 @@
use Composite\DB\TableConfig;
use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity;
use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use Composite\DB\Where;
use Composite\Entity\AbstractEntity;
class TestAutoincrementSdCachedTable extends AbstractCachedTable implements IAutoincrementTable
{
+ public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+ {
+ parent::__construct($cache);
+ (new TestAutoincrementSdTable)->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema());
@@ -19,26 +26,34 @@ protected function getFlushCacheKeys(TestAutoincrementSdEntity|AbstractEntity $e
{
$keys = [
$this->getOneCacheKey(['name' => $entity->name]),
- $this->getListCacheKey('name = :name', ['name' => $entity->name]),
- $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+ $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+ $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
];
$oldName = $entity->getOldValue('name');
if ($oldName !== null && $oldName !== $entity->name) {
$keys[] = $this->getOneCacheKey(['name' => $oldName]);
- $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]);
- $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]);
+ $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName]));
+ $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName]));
}
return $keys;
}
public function findByPk(int $id): ?TestAutoincrementSdEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
public function findOneByName(string $name): ?TestAutoincrementSdEntity
{
- return $this->createEntity($this->findOneCachedInternal(['name' => $name]));
+ return $this->_findOneCached(['name' => $name]);
+ }
+
+ public function delete(TestAutoincrementSdEntity|AbstractEntity $entity): void
+ {
+ if ($entity->name === 'Exception') {
+ throw new \Exception('Test Exception');
+ }
+ parent::delete($entity);
}
/**
@@ -46,18 +61,27 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- ));
+ return $this->_findAllCached(new Where('name = :name', ['name' => $name, 'deleted_at' => null]));
+ }
+
+ /**
+ * @return TestAutoincrementSdEntity[]
+ */
+ public function findRecent(int $limit, int $offset): array
+ {
+ return $this->_findAll(
+ orderBy: 'id DESC',
+ limit: $limit,
+ offset: $offset,
+ );
}
public function countAllByName(string $name): int
{
- return $this->countAllCachedInternal(
+ return $this->_countByAllCached(new Where(
'name = :name',
['name' => $name],
- );
+ ));
}
public function truncate(): void
diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php
index 8f68332..f878db9 100644
--- a/tests/TestStand/Tables/TestAutoincrementSdTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php
@@ -4,9 +4,16 @@
use Composite\DB\TableConfig;
use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity;
+use Composite\DB\Where;
class TestAutoincrementSdTable extends TestAutoincrementTable
{
+ public function __construct()
+ {
+ parent::__construct();
+ $this->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema());
@@ -14,12 +21,12 @@ protected function getConfig(): TableConfig
public function findByPk(int $id): ?TestAutoincrementSdEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
public function findOneByName(string $name): ?TestAutoincrementSdEntity
{
- return $this->createEntity($this->findOneInternal(['name' => $name]));
+ return $this->_findOne(['name' => $name, 'deleted_at' => null]);
}
/**
@@ -27,10 +34,20 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllInternal(
- 'name = :name',
- ['name' => $name]
- ));
+ return $this->_findAll(new Where('name = :name', ['name' => $name]));
+ }
+
+ /**
+ * @return TestAutoincrementSdEntity[]
+ */
+ public function findRecent(int $limit, int $offset): array
+ {
+ return $this->_findAll(
+ where: ['deleted_at' => null],
+ orderBy: 'id DESC',
+ limit: $limit,
+ offset: $offset,
+ );
}
public function init(): bool
@@ -41,6 +58,7 @@ public function init(): bool
(
`id` INTEGER NOT NULL CONSTRAINT TestAutoincrementSd_pk PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL,
+ `is_test` INTEGER NOT NULL DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
`deleted_at` TIMESTAMP NULL DEFAULT NULL
);
diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php
index 900e576..bef47de 100644
--- a/tests/TestStand/Tables/TestAutoincrementTable.php
+++ b/tests/TestStand/Tables/TestAutoincrementTable.php
@@ -6,9 +6,17 @@
use Composite\DB\TableConfig;
use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity;
use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable;
+use Composite\DB\Where;
+use Composite\Entity\AbstractEntity;
class TestAutoincrementTable extends AbstractTable implements IAutoincrementTable
{
+ public function __construct()
+ {
+ parent::__construct();
+ $this->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema());
@@ -16,12 +24,20 @@ protected function getConfig(): TableConfig
public function findByPk(int $id): ?TestAutoincrementEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
public function findOneByName(string $name): ?TestAutoincrementEntity
{
- return $this->createEntity($this->findOneInternal(['name' => $name]));
+ return $this->_findOne(['name' => $name]);
+ }
+
+ public function delete(AbstractEntity|TestAutoincrementEntity $entity): void
+ {
+ if ($entity->name === 'Exception') {
+ throw new \Exception('Test Exception');
+ }
+ parent::delete($entity);
}
/**
@@ -29,20 +45,39 @@ public function findOneByName(string $name): ?TestAutoincrementEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllInternal(
- 'name = :name',
- ['name' => $name],
- ));
+ return $this->_findAll(
+ where: new Where('name = :name', ['name' => $name]),
+ orderBy: 'id',
+ );
}
- public function countAllByName(string $name): int
+ /**
+ * @return TestAutoincrementEntity[]
+ */
+ public function findRecent(int $limit, int $offset): array
{
- return $this->countAllInternal(
- 'name = :name',
- ['name' => $name]
+ return $this->_findAll(
+ orderBy: ['id' => 'DESC'],
+ limit: $limit,
+ offset: $offset,
);
}
+ public function countAllByName(string $name): int
+ {
+ return $this->_countAll(new Where('name = :name', ['name' => $name]));
+ }
+
+ /**
+ * @param int[] $ids
+ * @return TestAutoincrementEntity[]
+ * @throws \Composite\DB\Exceptions\DbException
+ */
+ public function findMulti(array $ids): array
+ {
+ return $this->_findMulti($ids, 'id');
+ }
+
public function init(): bool
{
$this->getConnection()->executeStatement(
@@ -51,6 +86,7 @@ public function init(): bool
(
`id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL,
+ `is_test` INTEGER NOT NULL DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
"
diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php
index a2519da..6b5996a 100644
--- a/tests/TestStand/Tables/TestCompositeCachedTable.php
+++ b/tests/TestStand/Tables/TestCompositeCachedTable.php
@@ -9,6 +9,12 @@
class TestCompositeCachedTable extends \Composite\DB\AbstractCachedTable implements ICompositeTable
{
+ public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+ {
+ parent::__construct($cache);
+ (new TestCompositeTable())->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestCompositeEntity::schema());
@@ -17,17 +23,17 @@ protected function getConfig(): TableConfig
protected function getFlushCacheKeys(TestCompositeEntity|AbstractEntity $entity): array
{
return [
- $this->getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
- $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
+ $this->getListCacheKey(['user_id' => $entity->user_id]),
+ $this->getCountCacheKey(['user_id' => $entity->user_id]),
];
}
public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
{
- return $this->createEntity($this->findOneCachedInternal([
+ return $this->_findOneCached([
'user_id' => $user_id,
'post_id' => $post_id,
- ]));
+ ]);
}
/**
@@ -35,21 +41,12 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
*/
public function findAllByUser(int $userId): array
{
- return array_map(
- fn (array $data) => TestCompositeEntity::fromArray($data),
- $this->findAllCachedInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- )
- );
+ return $this->_findAllCached(['user_id' => $userId]);
}
public function countAllByUser(int $userId): int
{
- return $this->countAllCachedInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- );
+ return $this->_countByAllCached(['user_id' => $userId]);
}
public function truncate(): void
diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php
deleted file mode 100644
index 97f8512..0000000
--- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php
+++ /dev/null
@@ -1,59 +0,0 @@
-getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
- $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]),
- ];
- }
-
- public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity
- {
- return $this->createEntity($this->findOneCachedInternal([
- 'user_id' => $user_id,
- 'post_id' => $post_id,
- ]));
- }
-
- /**
- * @return TestCompositeSdEntity[]
- */
- public function findAllByUser(int $userId): array
- {
- return array_map(
- fn (array $data) => TestCompositeSdEntity::fromArray($data),
- $this->findAllCachedInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- )
- );
- }
-
- public function countAllByUser(int $userId): int
- {
- return $this->countAllCachedInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- );
- }
-
- public function truncate(): void
- {
- $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestCompositeSdTable.php b/tests/TestStand/Tables/TestCompositeSdTable.php
deleted file mode 100644
index a35bd1e..0000000
--- a/tests/TestStand/Tables/TestCompositeSdTable.php
+++ /dev/null
@@ -1,59 +0,0 @@
-createEntity($this->findOneInternal([
- 'user_id' => $user_id,
- 'post_id' => $post_id,
- ]));
- }
-
- /**
- * @return TestCompositeSdEntity[]
- */
- public function findAllByUser(int $userId): array
- {
- return $this->createEntities($this->findAllInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- ));
- }
-
- public function countAllByUser(int $userId): int
- {
- return $this->countAllInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- );
- }
-
- public function init(): bool
- {
- $this->getConnection()->executeStatement(
- "
- CREATE TABLE IF NOT EXISTS {$this->getTableName()}
- (
- `user_id` integer not null,
- `post_id` integer not null,
- `message` VARCHAR(255) DEFAULT '' NOT NULL,
- `created_at` TIMESTAMP NOT NULL,
- `deleted_at` TIMESTAMP NULL DEFAULT NULL,
- CONSTRAINT TestCompositeSd PRIMARY KEY (`user_id`, `post_id`, `deleted_at`)
- );
- "
- );
- return true;
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php
index 931ff55..8a1e9da 100644
--- a/tests/TestStand/Tables/TestCompositeTable.php
+++ b/tests/TestStand/Tables/TestCompositeTable.php
@@ -3,6 +3,7 @@
namespace Composite\DB\Tests\TestStand\Tables;
use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\Enums\TestUnitEnum;
use Composite\DB\Tests\TestStand\Entities\TestCompositeEntity;
use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable;
use Composite\Entity\AbstractEntity;
@@ -14,7 +15,7 @@ protected function getConfig(): TableConfig
return TableConfig::fromEntitySchema(TestCompositeEntity::schema());
}
- public function save(AbstractEntity|TestCompositeEntity &$entity): void
+ public function save(AbstractEntity|TestCompositeEntity $entity): void
{
if ($entity->message === 'Exception') {
throw new \Exception('Test Exception');
@@ -22,9 +23,17 @@ public function save(AbstractEntity|TestCompositeEntity &$entity): void
parent::save($entity);
}
+ public function delete(AbstractEntity|TestCompositeEntity $entity): void
+ {
+ if ($entity->message === 'Exception') {
+ throw new \Exception('Test Exception');
+ }
+ parent::delete($entity);
+ }
+
public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
{
- return $this->createEntity($this->findOneInternal(['user_id' => $user_id, 'post_id' => $post_id]));
+ return $this->_findOne(['user_id' => $user_id, 'post_id' => $post_id]);
}
/**
@@ -32,29 +41,22 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity
*/
public function findAllByUser(int $userId): array
{
- return $this->createEntities($this->findAllInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- ));
+ return $this->_findAll(['user_id' => $userId, 'status' => TestUnitEnum::ACTIVE]);
}
public function countAllByUser(int $userId): int
{
- return $this->countAllInternal(
- 'user_id = :user_id',
- ['user_id' => $userId],
- );
+ return $this->_countAll(['user_id' => $userId]);
}
- public function test(): array
+ /**
+ * @param array $ids
+ * @return TestCompositeEntity[]
+ * @throws \Composite\DB\Exceptions\DbException
+ */
+ public function findMulti(array $ids): array
{
- $rows = $this
- ->select()
- ->where()
- ->orWhere()
- ->orderBy()
- ->fetchAllAssociative();
- return $this->createEntities($rows);
+ return $this->_findMulti($ids, 'post_id');
}
public function init(): bool
@@ -66,6 +68,7 @@ public function init(): bool
`user_id` integer not null,
`post_id` integer not null,
`message` VARCHAR(255) DEFAULT '' NOT NULL,
+ `status` VARCHAR(16) DEFAULT 'ACTIVE' NOT NULL,
`created_at` TIMESTAMP NOT NULL,
CONSTRAINT TestComposite PRIMARY KEY (`user_id`, `post_id`)
);
diff --git a/tests/TestStand/Tables/TestMySQLTable.php b/tests/TestStand/Tables/TestMySQLTable.php
new file mode 100644
index 0000000..e918355
--- /dev/null
+++ b/tests/TestStand/Tables/TestMySQLTable.php
@@ -0,0 +1,25 @@
+escapeIdentifier($key);
+ }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php
index beb6123..1ede3bc 100644
--- a/tests/TestStand/Tables/TestOptimisticLockTable.php
+++ b/tests/TestStand/Tables/TestOptimisticLockTable.php
@@ -8,6 +8,12 @@
class TestOptimisticLockTable extends AbstractTable
{
+ public function __construct()
+ {
+ parent::__construct();
+ $this->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestOptimisticLockEntity::schema());
@@ -15,7 +21,7 @@ protected function getConfig(): TableConfig
public function findByPk(int $id): ?TestOptimisticLockEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
public function init(): bool
@@ -26,7 +32,7 @@ public function init(): bool
(
`id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL,
- `version` INTEGER NOT NULL DEFAULT 1,
+ `lock_version` INTEGER NOT NULL DEFAULT 1,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
"
diff --git a/tests/TestStand/Tables/TestPostgresTable.php b/tests/TestStand/Tables/TestPostgresTable.php
new file mode 100644
index 0000000..94ab839
--- /dev/null
+++ b/tests/TestStand/Tables/TestPostgresTable.php
@@ -0,0 +1,25 @@
+escapeIdentifier($key);
+ }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestStrictTable.php b/tests/TestStand/Tables/TestStrictTable.php
new file mode 100644
index 0000000..81a2c4b
--- /dev/null
+++ b/tests/TestStand/Tables/TestStrictTable.php
@@ -0,0 +1,28 @@
+createEntity($data);
+ }
+
+ /**
+ * @return TestStrictEntity[]
+ */
+ public function buildEntities(mixed $data): array
+ {
+ return $this->createEntities($data);
+ }
+}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php
index f95d102..8fe714f 100644
--- a/tests/TestStand/Tables/TestUniqueCachedTable.php
+++ b/tests/TestStand/Tables/TestUniqueCachedTable.php
@@ -6,10 +6,18 @@
use Composite\DB\TableConfig;
use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity;
use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use Composite\DB\Where;
use Composite\Entity\AbstractEntity;
+use Ramsey\Uuid\UuidInterface;
class TestUniqueCachedTable extends AbstractCachedTable implements IUniqueTable
{
+ public function __construct(\Psr\SimpleCache\CacheInterface $cache)
+ {
+ parent::__construct($cache);
+ (new TestUniqueTable())->init();
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestUniqueEntity::schema());
@@ -18,14 +26,14 @@ protected function getConfig(): TableConfig
protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): array
{
return [
- $this->getListCacheKey('name = :name', ['name' => $entity->name]),
- $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
+ $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])),
+ $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])),
];
}
- public function findByPk(string $id): ?TestUniqueEntity
+ public function findByPk(UuidInterface $id): ?TestUniqueEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
/**
@@ -33,18 +41,12 @@ public function findByPk(string $id): ?TestUniqueEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- ));
+ return $this->_findAllCached(new Where('name = :name', ['name' => $name]));
}
public function countAllByName(string $name): int
{
- return $this->countAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- );
+ return $this->_countByAllCached(new Where('name = :name', ['name' => $name]));
}
public function truncate(): void
diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php
deleted file mode 100644
index 278cb37..0000000
--- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php
+++ /dev/null
@@ -1,54 +0,0 @@
-getListCacheKey('name = :name', ['name' => $entity->name]),
- $this->getCountCacheKey('name = :name', ['name' => $entity->name]),
- ];
- }
-
- public function findByPk(string $id): ?TestUniqueSdEntity
- {
- return $this->createEntity($this->findByPkInternal($id));
- }
-
- /**
- * @return TestUniqueSdEntity[]
- */
- public function findAllByName(string $name): array
- {
- return $this->createEntities($this->findAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- ));
- }
-
- public function countAllByName(string $name): int
- {
- return $this->countAllCachedInternal(
- 'name = :name',
- ['name' => $name],
- );
- }
-
- public function truncate(): void
- {
- $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php
deleted file mode 100644
index 6bed15f..0000000
--- a/tests/TestStand/Tables/TestUniqueSdTable.php
+++ /dev/null
@@ -1,47 +0,0 @@
-createEntity($this->findByPkInternal($id));
- }
-
- /**
- * @return TestUniqueSdEntity[]
- */
- public function findAllByName(string $name): array
- {
- return $this->createEntities($this->findAllInternal(
- 'name = :name',
- ['name' => $name],
- ));
- }
-
- public function init(): bool
- {
- $this->getConnection()->executeStatement(
- "
- CREATE TABLE IF NOT EXISTS {$this->getTableName()}
- (
- `id` VARCHAR(255) NOT NULL,
- `name` VARCHAR(255) NOT NULL,
- `created_at` TIMESTAMP NOT NULL,
- `deleted_at` TIMESTAMP NULL DEFAULT NULL,
- CONSTRAINT TestUniqueSd PRIMARY KEY (`id`, `deleted_at`)
- );
- "
- );
- return true;
- }
-}
\ No newline at end of file
diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php
index 317985c..9387104 100644
--- a/tests/TestStand/Tables/TestUniqueTable.php
+++ b/tests/TestStand/Tables/TestUniqueTable.php
@@ -4,19 +4,37 @@
use Composite\DB\AbstractTable;
use Composite\DB\TableConfig;
+use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedEnum;
use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity;
use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable;
+use Composite\DB\Where;
+use Composite\Entity\AbstractEntity;
+use Ramsey\Uuid\UuidInterface;
class TestUniqueTable extends AbstractTable implements IUniqueTable
{
+ public function __construct()
+ {
+ parent::__construct();
+ $this->init();
+ }
+
+ public function save(AbstractEntity|TestUniqueEntity $entity): void
+ {
+ if ($entity->name === 'Exception') {
+ throw new \Exception('Test Exception');
+ }
+ parent::save($entity);
+ }
+
protected function getConfig(): TableConfig
{
return TableConfig::fromEntitySchema(TestUniqueEntity::schema());
}
- public function findByPk(string $id): ?TestUniqueEntity
+ public function findByPk(UuidInterface $id): ?TestUniqueEntity
{
- return $this->createEntity($this->findByPkInternal($id));
+ return $this->_findByPk($id);
}
/**
@@ -24,18 +42,12 @@ public function findByPk(string $id): ?TestUniqueEntity
*/
public function findAllByName(string $name): array
{
- return $this->createEntities($this->findAllInternal(
- 'name = :name',
- ['name' => $name],
- ));
+ return $this->_findAll(['name' => $name, 'status' => TestBackedEnum::ACTIVE]);
}
public function countAllByName(string $name): int
{
- return $this->countAllInternal(
- 'name = :name',
- ['name' => $name],
- );
+ return $this->_countAll(new Where('name = :name', ['name' => $name]));
}
public function init(): bool
@@ -46,6 +58,7 @@ public function init(): bool
(
`id` VARCHAR(255) NOT NULL CONSTRAINT TestUnique_pk PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
+ `status` VARCHAR(16) DEFAULT 'Active' NOT NULL,
`created_at` TIMESTAMP NOT NULL
);
"
diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php
new file mode 100644
index 0000000..ea114bd
--- /dev/null
+++ b/tests/TestStand/Tables/TestUpdateAtTable.php
@@ -0,0 +1,47 @@
+init();
+ }
+
+ protected function getConfig(): TableConfig
+ {
+ return TableConfig::fromEntitySchema(TestUpdatedAtEntity::schema());
+ }
+
+ public function findByPk(string $id): ?TestUpdatedAtEntity
+ {
+ return $this->_findByPk($id);
+ }
+
+ public function init(): bool
+ {
+ $this->getConnection()->executeStatement(
+ "
+ CREATE TABLE IF NOT EXISTS {$this->getTableName()}
+ (
+ `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `created_at` TIMESTAMP NOT NULL,
+ `updated_at` TIMESTAMP NOT NULL
+ );
+ "
+ );
+ return true;
+ }
+
+ public function truncate(): void
+ {
+ $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1");
+ }
+}
\ No newline at end of file
diff --git a/tests/TestStand/configs/empty_config.php b/tests/TestStand/configs/empty_config.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/TestStand/configs/wrong_content_config.php b/tests/TestStand/configs/wrong_content_config.php
new file mode 100644
index 0000000..27cdc70
--- /dev/null
+++ b/tests/TestStand/configs/wrong_content_config.php
@@ -0,0 +1,2 @@
+ [],
+];
\ No newline at end of file
diff --git a/tests/TestStand/configs/wrong_name_config.php b/tests/TestStand/configs/wrong_name_config.php
new file mode 100644
index 0000000..36b9da1
--- /dev/null
+++ b/tests/TestStand/configs/wrong_name_config.php
@@ -0,0 +1,4 @@
+ [],
+];
\ No newline at end of file
diff --git a/tests/TestStand/configs/wrong_params_config.php b/tests/TestStand/configs/wrong_params_config.php
new file mode 100644
index 0000000..cee852e
--- /dev/null
+++ b/tests/TestStand/configs/wrong_params_config.php
@@ -0,0 +1,7 @@
+ [
+ 'driver' => 'pdo_nothing',
+ 'path' => __DIR__ . '/runtime/sqlite/database.db',
+ ],
+];
\ No newline at end of file
diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php
new file mode 100644
index 0000000..16f95d1
--- /dev/null
+++ b/tests/Traits/OptimisticLockTest.php
@@ -0,0 +1,66 @@
+save($aiEntity1);
+
+ $aiEntity2 = $aiTable2->findByPk($aiEntity1->id);
+
+ $db = ConnectionManager::getConnection($aiTable1->getConnectionName());
+
+ $db->beginTransaction();
+ $aiEntity1->name = 'John1';
+ $aiTable1->save($aiEntity1);
+
+ $aiEntity2->name = 'John2';
+ $aiTable2->save($aiEntity2);
+
+ $db->commit();
+
+ $aiEntity3 = $aiTable1->findByPk($aiEntity1->id);
+ $this->assertEquals('John2', $aiEntity3->name);
+
+ //Checking optimistic lock
+ $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John');
+ $olTable1 = new Tables\TestOptimisticLockTable();
+ $olTable2 = new Tables\TestOptimisticLockTable();
+
+ $olTable1->save($olEntity1);
+
+ $olEntity2 = $olTable2->findByPk($olEntity1->id);
+
+ $db->beginTransaction();
+ $olEntity1->name = 'John1';
+ $olTable1->save($olEntity1);
+
+ $olEntity2->name = 'John2';
+
+ $exceptionCaught = false;
+ try {
+ $olTable2->save($olEntity2);
+ } catch (DbException) {
+ $exceptionCaught = true;
+ }
+ $this->assertTrue($exceptionCaught);
+
+ $db->rollBack();
+
+ $olEntity3 = $olTable1->findByPk($olEntity1->id);
+ $this->assertEquals(1, $olEntity3->getVersion());
+ $this->assertEquals('John', $olEntity3->name);
+ }
+}
\ No newline at end of file
diff --git a/tests/Traits/UpdatedAtTest.php b/tests/Traits/UpdatedAtTest.php
new file mode 100644
index 0000000..bb149c5
--- /dev/null
+++ b/tests/Traits/UpdatedAtTest.php
@@ -0,0 +1,36 @@
+assertNull($entity->updated_at);
+
+ $table = new TestUpdateAtTable();
+ $table->save($entity);
+
+ $this->assertNotNull($entity->updated_at);
+
+ $dbEntity = $table->findByPk($entity->id);
+ $this->assertNotNull($dbEntity);
+
+ $this->assertEquals($entity->updated_at, $dbEntity->updated_at);
+
+
+ $entity->name = 'Richard';
+ $table->save($entity);
+
+ $this->assertNotEquals($entity->updated_at, $dbEntity->updated_at);
+ $lastUpdatedAt = $entity->updated_at;
+
+ //should not update entity
+ $table->save($entity);
+ $this->assertEquals($lastUpdatedAt, $entity->updated_at);
+ }
+}
\ No newline at end of file
diff --git a/tests/config.php b/tests/config.php
index 0e156e6..74cd265 100644
--- a/tests/config.php
+++ b/tests/config.php
@@ -1,5 +1,7 @@
[
'driver' => 'pdo_sqlite',
@@ -11,6 +13,7 @@
'user' => 'test',
'password' => 'test',
'host' => '127.0.0.1',
+ 'configuration' => new Configuration(),
],
'postgres' => [
'driver' => 'pdo_pgsql',