diff --git a/composer.json b/composer.json
index 300ff37c1..966cb4510 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,8 @@
"yiisoft/html": "^3.0@dev",
"yiisoft/security": "^3.0@dev",
"cycle/orm": "^1.1",
- "cycle/annotated": "^2.0"
+ "cycle/annotated": "^2.0",
+ "cycle/migrations": "^1.0"
},
"require-dev": {
"hiqdev/composer-config-plugin": "^1.0@dev"
diff --git a/config/common.php b/config/common.php
index bfa44be2f..8c7cff154 100644
--- a/config/common.php
+++ b/config/common.php
@@ -1,10 +1,13 @@
'@root/src',
'@runtime' => '@root/runtime',
],
- CacheInterface::class => [
- '__class' => Cache::class,
- 'handler' => [
- '__class' => ArrayCache::class,
- ],
- ],
+ Psr\SimpleCache\CacheInterface::class => ArrayCache::class,
+ CacheInterface::class => Cache::class,
LoggerInterface::class => new LoggerFactory(),
FileRotatorInterface::class => [
'__class' => FileRotator::class,
@@ -36,9 +35,9 @@
10
]
],
- \Swift_Transport::class => \Swift_SmtpTransport::class,
- \Swift_SmtpTransport::class => [
- '__class' => \Swift_SmtpTransport::class,
+ Swift_Transport::class => Swift_SmtpTransport::class,
+ Swift_SmtpTransport::class => [
+ '__class' => Swift_SmtpTransport::class,
'__construct()' => [
'host' => $params['mailer.host'],
'port' => $params['mailer.port'],
@@ -49,6 +48,17 @@
],
MailerInterface::class => new MailerFactory(),
+
+ // Cycle DBAL
+ DatabaseManager::class => new CycleDbalFactory(),
// Cycle ORM
ORMInterface::class => new CycleOrmFactory(),
+ // Cycle Entity Finder
+ CycleOrmHelper::class => [
+ '__class' => CycleOrmHelper::class,
+ 'addEntityPaths()' => [
+ 'paths' => $params['entityPaths'],
+ ],
+ ],
+
];
diff --git a/config/console.php b/config/console.php
index f279d0262..f1aeb8913 100644
--- a/config/console.php
+++ b/config/console.php
@@ -1,8 +1,29 @@
CreateUser::class,
+
+ // Cycle Migrations
+ Migrator::class => new CycleMigratorFactory(),
+ // Migration Config
+ MigrationConfig::class => function (ContainerInterface $container) {
+ $aliases = $container->get(Aliases::class);
+ return new MigrationConfig([
+ 'directory' => $aliases->get('@src/Console/Migration'),
+ 'namespace' => 'App\Console\Migration',
+ 'table' => 'migration',
+ 'safe' => false,
+ ]);
+ }
];
diff --git a/config/params.php b/config/params.php
index 5cac1ad2a..2d0370de9 100644
--- a/config/params.php
+++ b/config/params.php
@@ -1,6 +1,10 @@
'smtp.example.com',
@@ -13,5 +17,13 @@
'commands' => [
'user/create' => CreateUser::class,
+ 'migrate/generate' => MigrateGenerate::class,
+ 'migrate/up' => MigrateUp::class,
+ 'migrate/down' => MigrateDown::class,
+ 'migrate/list' => MigrateList::class,
],
+
+ 'entityPaths' => [
+ '@src/Entity'
+ ]
];
diff --git a/src/ConsoleCommand/CreateUser.php b/src/Console/Command/CreateUser.php
similarity index 98%
rename from src/ConsoleCommand/CreateUser.php
rename to src/Console/Command/CreateUser.php
index f733c543b..ac95499c1 100644
--- a/src/ConsoleCommand/CreateUser.php
+++ b/src/Console/Command/CreateUser.php
@@ -1,8 +1,7 @@
config = $conf;
+ $this->migrator = $migrator;
+ $this->cycleOrmHelper = $cycleOrmHelper;
+ }
+
+ public function configure(): void
+ {
+ $this
+ ->setDescription('Rollback last migration');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // drop cached schema
+ $this->cycleOrmHelper->dropCurrentSchemaCache();
+
+ $list = $this->migrator->getMigrations();
+ $output->writeln('' . count($list) . ' migrations found in ' . $this->config->getDirectory() . '');
+
+ $statuses = [
+ State::STATUS_UNDEFINED => 'undefined',
+ State::STATUS_PENDING => 'pending',
+ State::STATUS_EXECUTED => 'executed',
+ ];
+ try {
+ $migration = $this->migrator->rollback();
+ if (!$migration instanceof MigrationInterface) {
+ throw new \Exception('Migration not found');
+ }
+
+ $state = $migration->getState();
+ $status = $state->getStatus();
+ $output->writeln($state->getName() . ': ' . ($statuses[$status] ?? $status));
+ } catch (\Throwable $e) {
+ $output->writeln([
+ '===================',
+ 'Error!',
+ $e->getMessage(),
+ ]);
+ return;
+ }
+ }
+}
diff --git a/src/Console/Command/MigrateGenerate.php b/src/Console/Command/MigrateGenerate.php
new file mode 100644
index 000000000..78f7389d6
--- /dev/null
+++ b/src/Console/Command/MigrateGenerate.php
@@ -0,0 +1,54 @@
+migrator = $migrator;
+ $this->config = $conf;
+ $this->cycleHelper = $cycleHelper;
+ }
+
+ public function configure(): void
+ {
+ $this->setDescription('Generates a migration');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): void
+ {
+ // check existing unapplied migrations
+ $list = $this->migrator->getMigrations();
+ foreach ($list as $migration) {
+ if ($migration->getState()->getStatus() !== State::STATUS_EXECUTED) {
+ $output->writeln('Outstanding migrations found, run `migrate/up` first.');
+ return;
+ }
+ }
+
+ $this->cycleHelper->generateMigration($this->migrator, $this->config);
+ }
+}
diff --git a/src/Console/Command/MigrateList.php b/src/Console/Command/MigrateList.php
new file mode 100644
index 000000000..3ccd0b74d
--- /dev/null
+++ b/src/Console/Command/MigrateList.php
@@ -0,0 +1,54 @@
+config = $conf;
+ $this->migrator = $migrator;
+ parent::__construct();
+ }
+
+ public function configure(): void
+ {
+ $this
+ ->setDescription('Print list of all migrations');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $list = $this->migrator->getMigrations();
+ $output->writeln('' . count($list) . ' migrations found in ' . $this->config->getDirectory() . ':');
+
+ $statuses = [
+ State::STATUS_UNDEFINED => 'undefined',
+ State::STATUS_PENDING => 'pending',
+ State::STATUS_EXECUTED => 'executed',
+ ];
+ $list = $this->migrator->getMigrations();
+
+ foreach ($list as $migration) {
+ $state = $migration->getState();
+ $output->writeln($state->getName() . ' [' . ($statuses[$state->getStatus()] ?? '?') . ']');
+ }
+ }
+}
diff --git a/src/Console/Command/MigrateUp.php b/src/Console/Command/MigrateUp.php
new file mode 100644
index 000000000..c993bd980
--- /dev/null
+++ b/src/Console/Command/MigrateUp.php
@@ -0,0 +1,80 @@
+config = $conf;
+ $this->migrator = $migrator;
+ $this->cycleOrmHelper = $cycleOrmHelper;
+ }
+
+ public function configure(): void
+ {
+ $this
+ ->setDescription('Execute all new migrations');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // drop cached schema
+ $this->cycleOrmHelper->dropCurrentSchemaCache();
+
+ $list = $this->migrator->getMigrations();
+ $output->writeln('' . count($list) . ' migrations found in ' . $this->config->getDirectory() . '');
+
+ $limit = PHP_INT_MAX;
+ $statuses = [
+ State::STATUS_UNDEFINED => 'undefined',
+ State::STATUS_PENDING => 'pending',
+ State::STATUS_EXECUTED => 'executed',
+ ];
+ try {
+ do {
+ $migration = $this->migrator->run();
+ if (!$migration instanceof MigrationInterface) {
+ break;
+ }
+
+ $state = $migration->getState();
+ $status = $state->getStatus();
+ $output->writeln($state->getName() . ': ' . ($statuses[$status] ?? $status));
+ } while (--$limit > 0);
+ } catch (\Throwable $e) {
+ $output->writeln([
+ '===================',
+ 'Error!',
+ $e->getMessage(),
+ ]);
+ return;
+ }
+ }
+}
diff --git a/src/Console/Migration/.gitignore b/src/Console/Migration/.gitignore
new file mode 100644
index 000000000..f935021a8
--- /dev/null
+++ b/src/Console/Migration/.gitignore
@@ -0,0 +1 @@
+!.gitignore
diff --git a/src/Controller.php b/src/Controller.php
index 35cf6db3b..ccb77a3d8 100644
--- a/src/Controller.php
+++ b/src/Controller.php
@@ -64,7 +64,7 @@ private function findLayoutFile(?string $file): ?string
return $file;
}
- return $file . '.' . $this->view->defaultExtension;
+ return $file . '.' . $this->view->getDefaultExtension();
}
abstract protected function getId(): string;
diff --git a/src/Entity/User.php b/src/Entity/User.php
index ee204c82d..bf3587678 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -8,7 +8,7 @@
use Yiisoft\Yii\Web\User\IdentityInterface;
/**
- * @Entity
+ * @Entity(repository="App\Repository\UserRepository")
*/
class User implements IdentityInterface
{
diff --git a/src/Factory/CycleDbalFactory.php b/src/Factory/CycleDbalFactory.php
new file mode 100644
index 000000000..294bdb7f5
--- /dev/null
+++ b/src/Factory/CycleDbalFactory.php
@@ -0,0 +1,37 @@
+get(Aliases::class);
+ $databasePath = $aliases->get('@runtime/database.db');
+
+ $dbal = new DatabaseManager(
+ new DatabaseConfig([
+ 'default' => 'default',
+ 'databases' => [
+ 'default' => ['connection' => 'sqlite']
+ ],
+ 'connections' => [
+ 'sqlite' => [
+ 'driver' => SQLiteDriver::class,
+ 'connection' => 'sqlite:' . $databasePath,
+ 'username' => '',
+ 'password' => '',
+ ]
+ ]
+ ])
+ );
+
+ return $dbal;
+ }
+}
diff --git a/src/Factory/CycleMigratorFactory.php b/src/Factory/CycleMigratorFactory.php
new file mode 100644
index 000000000..e2a3d7677
--- /dev/null
+++ b/src/Factory/CycleMigratorFactory.php
@@ -0,0 +1,24 @@
+get(MigrationConfig::class);
+
+ $dbal = $container->get(DatabaseManager::class);
+
+ $migrator = new Migrator($migConf, $dbal, new FileRepository($migConf));
+ // Init migration table
+ $migrator->configure();
+ return $migrator;
+ }
+}
diff --git a/src/Factory/CycleOrmFactory.php b/src/Factory/CycleOrmFactory.php
index ef92ba22a..830b33e55 100644
--- a/src/Factory/CycleOrmFactory.php
+++ b/src/Factory/CycleOrmFactory.php
@@ -2,86 +2,25 @@
namespace App\Factory;
-use Cycle\Annotated;
+use App\Helper\CycleOrmHelper;
use Cycle\ORM\Factory;
use Cycle\ORM\ORM;
use Cycle\ORM\Schema;
-use Cycle\ORM\SchemaInterface;
-use Cycle\Schema\Compiler;
-use Cycle\Schema\Generator\GenerateRelations;
-use Cycle\Schema\Generator\GenerateTypecast;
-use Cycle\Schema\Generator\RenderRelations;
-use Cycle\Schema\Generator\RenderTables;
-use Cycle\Schema\Generator\ResetTables;
-use Cycle\Schema\Generator\SyncTables;
-use Cycle\Schema\Generator\ValidateEntities;
-use Cycle\Schema\Registry;
-use Doctrine\Common\Annotations\AnnotationRegistry;
use Psr\Container\ContainerInterface;
-use Spiral\Database\Config\DatabaseConfig;
use Spiral\Database\DatabaseManager;
-use Spiral\Database\Driver\SQLite\SQLiteDriver;
-use Spiral\Tokenizer\ClassLocator;
-use Symfony\Component\Finder\Finder;
-use Yiisoft\Aliases\Aliases;
class CycleOrmFactory
{
+ /** @var ContainerInterface */
+ private $container;
+
public function __invoke(ContainerInterface $container)
{
- $aliases = $container->get(Aliases::class);
-
- $entityPaths = [
- $aliases->get('@src/Entity')
- ];
-
- $databasePath = $aliases->get('@runtime/database.db');
+ $this->container = $container;
+ $dbal = $container->get(DatabaseManager::class);
- $dbal = new DatabaseManager(
- new DatabaseConfig([
- 'default' => 'default',
- 'databases' => [
- 'default' => ['connection' => 'sqlite']
- ],
- 'connections' => [
- 'sqlite' => [
- 'driver' => SQLiteDriver::class,
- 'connection' => 'sqlite:' . $databasePath,
- 'username' => '',
- 'password' => '',
- ]
- ]
- ])
- );
-
- // autoload annotations
- AnnotationRegistry::registerLoader('class_exists');
-
- $schema = $this->getSchema($entityPaths, $dbal);
+ $schema = new Schema($this->container->get(CycleOrmHelper::class)->getCurrentSchemaArray());
return (new ORM(new Factory($dbal)))->withSchema($schema);
}
-
- private function getSchema(array $entityPaths, DatabaseManager $dbal): SchemaInterface
- {
- $finder = (new Finder())
- ->files()
- ->in($entityPaths);
-
- $classLocator = new ClassLocator($finder);
-
- $schema = (new Compiler())->compile(new Registry($dbal), [
- new Annotated\Embeddings($classLocator), // register embeddable entities
- new Annotated\Entities($classLocator), // register annotated entities
- new ResetTables(), // re-declared table schemas (remove columns)
- new GenerateRelations(), // generate entity relations
- new ValidateEntities(), // make sure all entity schemas are correct
- new RenderTables(), // declare table schemas
- new RenderRelations(), // declare relation keys and indexes
- new SyncTables(), // sync table changes to database
- new GenerateTypecast(), // typecast non string columns
- ]);
-
- return new Schema($schema);
- }
}
diff --git a/src/Helper/CycleOrmHelper.php b/src/Helper/CycleOrmHelper.php
new file mode 100644
index 000000000..0ea30ab1d
--- /dev/null
+++ b/src/Helper/CycleOrmHelper.php
@@ -0,0 +1,129 @@
+aliases = $aliases;
+ $this->dbal = $dbal;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @param string|string[] $paths
+ */
+ public function addEntityPaths($paths): void
+ {
+ $paths = (array)$paths;
+ foreach ($paths as $path) {
+ $this->entityPaths[] = $path;
+ }
+ }
+
+ public function dropCurrentSchemaCache(): void
+ {
+ $this->cache->delete($this->cacheKey);
+ }
+
+ public function generateMigration(Migrator $migrator, MigrationConfig $config): void
+ {
+ $classLocator = $this->getEntityClassLocator();
+
+ // autoload annotations
+ AnnotationRegistry::registerLoader('class_exists');
+
+ (new Compiler())->compile(new Registry($this->dbal), [
+ new Annotated\Embeddings($classLocator), // register embeddable entities
+ new Annotated\Entities($classLocator, null, $this->tableNaming), // register annotated entities
+ new ResetTables(), // re-declared table schemas (remove columns)
+ new GenerateRelations(), // generate entity relations
+ new ValidateEntities(), // make sure all entity schemas are correct
+ new RenderTables(), // declare table schemas
+ new RenderRelations(), // declare relation keys and indexes
+ new GenerateMigrations($migrator->getRepository(), $config), // generate migrations
+ new GenerateTypecast(), // typecast non string columns
+ ]);
+ }
+
+ public function getCurrentSchemaArray($fromCache = true): array
+ {
+ $getSchemaArray = function () {
+ $classLocator = $this->getEntityClassLocator();
+ // autoload annotations
+ AnnotationRegistry::registerLoader('class_exists');
+
+ return (new Compiler())->compile(new Registry($this->dbal), [
+ new Annotated\Embeddings($classLocator), // register embeddable entities
+ new Annotated\Entities($classLocator, null, $this->tableNaming), // register annotated entities
+ new ResetTables(), // re-declared table schemas (remove columns)
+ new GenerateRelations(), // generate entity relations
+ new ValidateEntities(), // make sure all entity schemas are correct
+ new RenderTables(), // declare table schemas
+ new RenderRelations(), // declare relation keys and indexes
+ new SyncTables(), // sync table changes to database
+ new GenerateTypecast(), // typecast non string columns
+ ]);
+ };
+
+ if ($fromCache) {
+ return $this->cache->getOrSet($this->cacheKey, $getSchemaArray);
+ } else {
+ $schema = $getSchemaArray();
+ $this->cache->set($this->cacheKey, $schema);
+ return $schema;
+ }
+ }
+
+ private function getEntityClassLocator(): ClassLocator
+ {
+ $list = [];
+ foreach ($this->entityPaths as $path) {
+ $list[] = $this->aliases->get($path);
+ }
+ $finder = (new Finder())
+ ->files()
+ ->in($list);
+
+ return new ClassLocator($finder);
+ }
+}
diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php
index 9de33ddd2..0fa55a640 100644
--- a/src/Repository/UserRepository.php
+++ b/src/Repository/UserRepository.php
@@ -4,26 +4,25 @@
use App\Entity\User;
use Cycle\ORM\ORMInterface;
+use Cycle\ORM\Select;
use Yiisoft\Yii\Web\User\IdentityInterface;
use Yiisoft\Yii\Web\User\IdentityRepositoryInterface;
-class UserRepository implements IdentityRepositoryInterface
+class UserRepository extends Select\Repository implements IdentityRepositoryInterface
{
- private $orm;
-
- public function __construct(ORMInterface $orm)
+ public function __construct(ORMInterface $orm, $role = User::class)
{
- $this->orm = $orm;
+ parent::__construct(new Select($orm, $role));
}
private function findIdentityBy(string $field, string $value): ?IdentityInterface
{
- return $this->orm->getRepository(User::class)->findOne([$field => $value]);
+ return $this->findOne([$field => $value]);
}
public function findIdentity(string $id): ?IdentityInterface
{
- return $this->findIdentityBy('id', $id);
+ return $this->findByPK($id);
}
public function findIdentityByToken(string $token, string $type): ?IdentityInterface
@@ -31,7 +30,7 @@ public function findIdentityByToken(string $token, string $type): ?IdentityInter
return $this->findIdentityBy('token', $token);
}
- public function findByLogin(string $login): ?User
+ public function findByLogin(string $login): ?IdentityInterface
{
return $this->findIdentityBy('login', $login);
}