diff --git a/README.md b/README.md new file mode 100644 index 0000000..5948674 --- /dev/null +++ b/README.md @@ -0,0 +1,486 @@ +# Cycle ORM bridge +This package provides a set of Symfony commands, classes for integration [Cycle ORM v1](https://cycle-orm.dev) with any framework. + +## Requirements +* PHP >= 7.2 +* Cycle ORM 1.x + +## Installation +1. Install the package via composer: +```bash +composer require wakebit/cycle-bridge +``` + +## Example of usage with PHP-DI +1. Declare config `config/config.php` and fill database credentials: +```php + env('APP_DEBUG', 'true'), + + // Database + 'db.connection' => env('DB_CONNECTION', 'mysql'), + 'db.host' => env('DB_HOST', 'localhost'), + 'db.database' => env('DB_DATABASE', ''), + 'db.username' => env('DB_USERNAME', ''), + 'db.password' => env('DB_PASSWORD', ''), +]; +``` + +2. Declare ORM config `config/cycle.php`. Dont forget to set up correct paths to entities path, migrations path: +```php + static function (ContainerInterface $container): DatabaseConfig { + return new DatabaseConfig([ + 'default' => 'default', + + 'databases' => [ + 'default' => [ + 'connection' => $container->get('config')['db.connection'], + ], + ], + + 'connections' => [ + 'sqlite' => [ + 'driver' => \Spiral\Database\Driver\SQLite\SQLiteDriver::class, + 'options' => [ + 'connection' => 'sqlite::memory:', + 'username' => '', + 'password' => '', + ], + ], + 'mysql' => [ + 'driver' => \Spiral\Database\Driver\MySQL\MySQLDriver::class, + 'options' => [ + 'connection' => sprintf( + 'mysql:host=%s;dbname=%s', + $container->get('config')['db.host'], + $container->get('config')['db.database'] + ), + 'username' => $container->get('config')['db.username'], + 'password' => $container->get('config')['db.password'], + ], + ], + 'postgres' => [ + 'driver' => \Spiral\Database\Driver\Postgres\PostgresDriver::class, + 'options' => [ + 'connection' => sprintf( + 'pgsql:host=%s;dbname=%s', + $container->get('config')['db.host'], + $container->get('config')['db.database'] + ), + 'username' => $container->get('config')['db.username'], + 'password' => $container->get('config')['db.password'], + ], + ], + 'sqlServer' => [ + 'driver' => \Spiral\Database\Driver\SQLServer\SQLServerDriver::class, + 'options' => [ + 'connection' => sprintf( + 'sqlsrv:Server=%s;Database=%s', + $container->get('config')['db.host'], + $container->get('config')['db.database'] + ), + 'username' => $container->get('config')['db.username'], + 'password' => $container->get('config')['db.password'], + ], + ], + ], + ]); + }, + + 'orm' => [ + 'schema' => static function (): SchemaConfig { + return new SchemaConfig(); + }, + + 'tokenizer' => static function (): TokenizerConfig { + return new TokenizerConfig([ + 'directories' => [ + __DIR__ . '/../src/Entity', + ], + + 'exclude' => [ + ], + ]); + }, + ], + + 'migrations' => static function (ContainerInterface $container): MigrationConfig { + return new MigrationConfig([ + 'directory' => __DIR__ . '/../resources/migrations', + 'table' => 'migrations', + 'safe' => filter_var($container->get('config')['debug'], FILTER_VALIDATE_BOOLEAN), + ]); + }, +]; +``` + +3. Declare dependencies for PHP-DI container `config/container.php`: +```php + require 'config.php', + 'cycle' => require 'cycle.php', + + DatabaseConfig::class => static function (ContainerInterface $container): DatabaseConfig { + return $container->get('cycle')['database']; + }, + + SchemaConfig::class => static function (ContainerInterface $container): SchemaConfig { + return $container->get('cycle')['orm']['schema']; + }, + + TokenizerConfig::class => static function (ContainerInterface $container): TokenizerConfig { + return $container->get('cycle')['orm']['tokenizer']; + }, + + MigrationConfig::class => static function (ContainerInterface $container): MigrationConfig { + return $container->get('cycle')['migrations']; + }, + + DatabaseProviderInterface::class => autowire(DatabaseManager::class), + DatabaseInterface::class => static function (ContainerInterface $container): DatabaseInterface { + return $container->get(DatabaseProviderInterface::class)->database(); + }, + DatabaseManager::class => get(DatabaseProviderInterface::class), + + ClassLocator::class => get(ClassesInterface::class), + ClassesInterface::class => static function (ContainerInterface $container): ClassesInterface { + return $container->get(Tokenizer::class)->classLocator(); + }, + + FactoryInterface::class => autowire(Factory::class), + CacheManagerInterface::class => static function (): CacheManagerInterface { + // Here you need to pass PSR-16 compatible cache pool. See example with cache file below. + // Packages: league/flysystem, cache/filesystem-adapter + $filesystemAdapter = new \League\Flysystem\Adapter\Local(__DIR__ . '/../var/cache'); + $filesystem = new \League\Flysystem\Filesystem($filesystemAdapter); + $pool = new \Cache\Adapter\Filesystem\FilesystemCachePool($filesystem); + + return new CacheManager($pool); + }, + + GeneratorQueueInterface::class => autowire(GeneratorQueue::class), + CompilerInterface::class => autowire(Compiler::class), + SchemaInterface::class => factory([SchemaFactory::class, 'create']), + ORMInterface::class => autowire(ORM::class), + TransactionInterface::class => autowire(Transaction::class), + RepositoryInterface::class => autowire(FileRepository::class), +]; +``` + +4. Now, you need to load a dependencies array created in the step above to PHP-DI. After you are free to use dependencies, write your code. + +Let's look at quick example. Define entity: +```php +id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } +} +``` + +You can take DBAL, ORM and Transaction from the container. Quick example of usage: + +```php +dbal = $dbal; + $this->orm = $orm; + $this->transaction = $transaction; + } + + public function __invoke() + { + // DBAL + $tables = $this->dbal->database()->getTables(); + $tableNames = array_map(function (\Spiral\Database\TableInterface $table): string { + return $table->getName(); + }, $tables); + + dump($tableNames); + + // Create, modify, delete entities using Transaction + $user = new \App\Entity\User(); + $user->setName("Hello World"); + $this->transaction->persist($user); + $this->transaction->run(); + dump($user); + + // ORM + $repository = $this->orm->getRepository(\App\Entity\User::class); + $users = $repository->findAll(); + dump($users); + + $user = $repository->findByPK(1); + dump($user); + } +} +``` +See more on [the official Cycle ORM documentation](https://cycle-orm.dev/docs/readme/1.x/en). + +## Console commands +### Working with ORM schema +- Generate ORM schema migrations: + ``` + Wakebit\CycleBridge\Console\Command\Schema\MigrateCommand + ``` + ```bash + cycle:schema:migrate + ``` + Options: + - `--run`: Automatically run generated migration. + - `-v`: Verbose output. +- Compile and cache ORM schema: + ``` + Wakebit\CycleBridge\Console\Command\Schema\CacheCommand + ``` + ```bash + cycle:schema:cache + ``` +- Clear cached schema (schema will be generated every request now): + ``` + Wakebit\CycleBridge\Console\Command\Schema\ClearCommand + ``` + ```bash + cycle:schema:clear + ``` +- Sync ORM schema with database without intermediate migration (risk operation!): + ``` + Wakebit\CycleBridge\Console\Command\Schema\SyncCommand + ``` + ```bash + cycle:schema:sync + ``` + +### Database migrations +- Initialize migrator. This command creates a table for migrations: + ``` + Wakebit\CycleBridge\Console\Command\Migrate\InitCommand + ``` + ```bash + cycle:migrate:init + ``` +- Run all outstanding migrations: + ``` + Wakebit\CycleBridge\Console\Command\Migrate\MigrateCommand + ``` + ```bash + cycle:migrate + ``` + Options: + - `--one`: Execute only one (first) migration. + - `--force`: Force the operation to run when in production. +- Rollback the last migration: + ``` + Wakebit\CycleBridge\Console\Command\Migrate\RollbackCommand + ``` + ```bash + cycle:migrate:rollback + ``` + Options: + - `--all`: Rollback all executed migrations. + - `--force`: Force the operation to run when in production. +- Get a list of available migrations: + ``` + Wakebit\CycleBridge\Console\Command\Migrate\StatusCommand + ``` + ```bash + cycle:migrate:status + ``` + +### Database commands +- Get list of available databases, their tables and records count: + ``` + Wakebit\CycleBridge\Console\Command\Database\ListCommand + ``` + ```bash + cycle:db:list + ``` + Options: + - `--database`: Database name. +- Describe table schema of specific database: + ``` + Wakebit\CycleBridge\Console\Command\Database\TableCommand + ``` + ```bash + cycle:db:table + ``` + Arguments: + - `table`: Table name. + + Options: + - `--database`: Database name. + +## Writing functional tests +If you are using memory database (SQLite) you can just run migrations in the `setUp` method of the your test calling the console command `cycle:migrate`. +For another databases follow [this instruction](https://cycle-orm.dev/docs/advanced-testing/1.x/en) and drop all tables in the `tearDown` method. + +## Advanced +If you want to use a manually defined ORM schema you can define it in the `cycle.php` `orm.schema.map` config key: +```php +use Wakebit\CycleBridge\Schema\Config\SchemaConfig; + +return [ + // ... + 'orm' => [ + 'schema' => static function (): SchemaConfig { + return new SchemaConfig([ + 'map' => require __DIR__ . '/../orm_schema.php', + ]); + }, + ] + // ... +] +``` +Manually defined schema should be presented as array. It will be passed to `\Cycle\ORM\Schema` constructor. See more [here](https://cycle-orm.dev/docs/advanced-manual/1.x/en). + +Also, you can redefine the ORM schema compilation generators in the `cycle.php` `orm.schema.generators` config key: +```php +use Wakebit\CycleBridge\Schema\Config\SchemaConfig; + +return [ + // ... + 'orm' => [ + 'schema' => static function (): SchemaConfig { + return new SchemaConfig([ + 'generators' => [ + 'index' => [], + 'render' => [ + \Cycle\Schema\Generator\ResetTables::class, // re-declared table schemas (remove columns) + \Cycle\Schema\Generator\GenerateRelations::class, // generate entity relations + \Cycle\Schema\Generator\ValidateEntities::class, // make sure all entity schemas are correct + \Cycle\Schema\Generator\RenderTables::class, // declare table schemas + \Cycle\Schema\Generator\RenderRelations::class, // declare relation keys and indexes + ], + 'postprocess' => [ + \Cycle\Schema\Generator\GenerateTypecast::class, // typecast non string columns + ], + ] + ]); + }, + ] + // ... +] +``` +Classes will be resolved by DI container. Default pipeline you can see [here](https://github.com/wakebit/cycle-bridge/blob/v1.x/src/Schema/Config/SchemaConfig.php#L32). + +# Credits +- [Cycle ORM](https://github.com/cycle), PHP DataMapper ORM and Data Modelling Engine by SpiralScout. +- [Spiral Scout](https://github.com/spiral), author of the Cycle ORM. +- [Spiral Framework Cycle Bridge](https://github.com/spiral/cycle-bridge/) for code samples, example of usage. +