Skip to content

Commit

Permalink
Merge pull request #17 from msmakouz/feature/scaffolders
Browse files Browse the repository at this point in the history
Add scaffolders for Entities, Repositories and Migraions from SF
  • Loading branch information
roxblnfk committed Mar 18, 2022
2 parents 3410e2a + 02f2146 commit f4ceb85
Show file tree
Hide file tree
Showing 15 changed files with 687 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -62,6 +62,9 @@ protected const LOAD = [

// Database Token Storage (Optional)
CycleBridge\AuthTokensBootloader::class,

// Migrations and Cycle Scaffolders (Optional)
CycleBridge\ScaffolderBootloader::class,
];
```

Expand Down
5 changes: 4 additions & 1 deletion composer.json
Expand Up @@ -25,8 +25,11 @@
"cycle/orm": "^2.0.2",
"cycle/schema-migrations-generator": "^2.0",
"cycle/schema-renderer": "^1.1",
"doctrine/inflector": "^1.4|^2.0",
"spiral/attributes": "^2.10",
"spiral/framework": "^2.10"
"spiral/framework": "^2.10",
"spiral/reactor": "^2.10",
"spiral/scaffolder": "^2.10"
},
"require-dev": {
"doctrine/collections": "^1.6",
Expand Down
3 changes: 3 additions & 0 deletions src/Bootloader/BridgeBootloader.php
Expand Up @@ -30,5 +30,8 @@ final class BridgeBootloader extends Bootloader

// Database Token Storage (Optional)
AuthTokensBootloader::class,

// Migrations and Cycle Scaffolders (Optional)
ScaffolderBootloader::class,
];
}
19 changes: 18 additions & 1 deletion src/Bootloader/CommandBootloader.php
Expand Up @@ -9,11 +9,13 @@
use Cycle\ORM\ORMInterface;
use Psr\Container\ContainerInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Bootloader\ConsoleBootloader;
use Spiral\Console\Bootloader\ConsoleBootloader;
use Spiral\Config\ConfiguratorInterface;
use Spiral\Core\Container;
use Spiral\Cycle\Console\Command\CycleOrm;
use Spiral\Cycle\Console\Command\Database;
use Spiral\Cycle\Console\Command\Migrate;
use Spiral\Cycle\Console\Command\Scaffolder;

final class CommandBootloader extends Bootloader
{
Expand All @@ -24,6 +26,7 @@ final class CommandBootloader extends Bootloader

public function boot(
ConsoleBootloader $console,
ConfiguratorInterface $config,
Container $container
): void {
$this->configureExtensions($console, $container);
Expand All @@ -42,6 +45,8 @@ private function configureExtensions(ConsoleBootloader $console, Container $cont
if ($container->has(Migrator::class)) {
$this->configureMigrations($console);
}

$this->configureScaffolders($console, $container);
}

private function configureDatabase(ConsoleBootloader $console): void
Expand Down Expand Up @@ -75,4 +80,16 @@ private function configureMigrations(ConsoleBootloader $console): void
$console->addCommand(Migrate\RollbackCommand::class);
$console->addCommand(Migrate\ReplayCommand::class);
}

public function configureScaffolders(ConsoleBootloader $console, ContainerInterface $container): void
{
if ($container->has(Migrator::class)) {
$console->addCommand(Scaffolder\MigrationCommand::class);
}

if ($container->has(ORMInterface::class)) {
$console->addCommand(Scaffolder\EntityCommand::class);
$console->addCommand(Scaffolder\RepositoryCommand::class);
}
}
}
41 changes: 41 additions & 0 deletions src/Bootloader/ScaffolderBootloader.php
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Spiral\Cycle\Bootloader;

use Spiral\Console\Bootloader\ConsoleBootloader;
use Spiral\Cycle\Scaffolder\Declaration;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Scaffolder\Bootloader\ScaffolderBootloader as BaseScaffolderBootloader;

class ScaffolderBootloader extends Bootloader
{
public const DEPENDENCIES = [
ConsoleBootloader::class,
BaseScaffolderBootloader::class
];

public function boot(BaseScaffolderBootloader $scaffolder): void
{
$scaffolder->addDeclaration('migration', [
'namespace' => '',
'postfix' => 'Migration',
'class' => Declaration\MigrationDeclaration::class,
]);

$scaffolder->addDeclaration('entity', [
'namespace' => 'Database',
'postfix' => '',
'options' => [
'annotated' => Declaration\Entity\AnnotatedDeclaration::class,
],
]);

$scaffolder->addDeclaration('repository', [
'namespace' => 'Repository',
'postfix' => 'Repository',
'class' => Declaration\RepositoryDeclaration::class,
]);
}
}
155 changes: 155 additions & 0 deletions src/Console/Command/Scaffolder/EntityCommand.php
@@ -0,0 +1,155 @@
<?php

declare(strict_types=1);

namespace Spiral\Cycle\Console\Command\Scaffolder;

use Spiral\Console\Console;
use Spiral\Cycle\Scaffolder\Declaration\Entity\AnnotatedDeclaration;
use Spiral\Reactor\AbstractDeclaration;
use Spiral\Scaffolder\Command\AbstractCommand;
use Spiral\Scaffolder\Config\ScaffolderConfig;
use Spiral\Scaffolder\Exception\ScaffolderException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Throwable;

use function Spiral\Scaffolder\trimPostfix;

class EntityCommand extends AbstractCommand
{
protected const ELEMENT = 'entity';

protected const NAME = 'create:entity';
protected const DESCRIPTION = 'Create entity declaration';
protected const ARGUMENTS = [
['name', InputArgument::REQUIRED, 'Entity name'],
['format', InputArgument::OPTIONAL, 'Declaration format (annotated, xml?, yaml?, php?)', 'annotated'],
];
protected const OPTIONS = [
[
'role',
'r',
InputOption::VALUE_OPTIONAL,
'Entity role, defaults to lowercase class name without a namespace',
],
[
'mapper',
'm',
InputOption::VALUE_OPTIONAL,
'Mapper class name, defaults to Cycle\ORM\Mapper\Mapper',
],
[
'repository',
'e',
InputOption::VALUE_NONE,
'Repository class to represent read operations for an entity, defaults to Cycle\ORM\Select\Repository',
],
[
'table',
't',
InputOption::VALUE_OPTIONAL,
'Entity source table, defaults to plural form of entity role',
],
[
'database',
'd',
InputOption::VALUE_OPTIONAL,
'Database name, defaults to null (default database)',
],
[
'accessibility',
'a',
InputOption::VALUE_OPTIONAL,
'Accessibility accessor (public, protected, private)',
AbstractDeclaration::ACCESS_PUBLIC,
],
[
'inflection',
'i',
InputOption::VALUE_OPTIONAL,
'Column name inflection, allowed values: tableize (t), camelize (c)',
'tableize',
],
[
'field',
'f',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Add field in a format "name:type"',
],
[
'comment',
'c',
InputOption::VALUE_OPTIONAL,
'Optional comment to add as class header',
],
];

/**
* Create entity declaration.
*
* @throws Throwable
*/
public function perform(Console $console, ScaffolderConfig $config): int
{
$accessibility = (string)$this->option('accessibility');
$this->validateAccessibility($accessibility);

/** @var AnnotatedDeclaration $declaration */
$declaration = $this->createDeclaration();

$repository = trimPostfix((string)$this->argument('name'), 'repository');
if ($this->option('repository')) {
$repositoryClass = $config->className(RepositoryCommand::ELEMENT, $repository);
$repositoryNamespace = $config->classNamespace(RepositoryCommand::ELEMENT, $repository);
$declaration->setRepository("\\$repositoryNamespace\\$repositoryClass");
}

$declaration->setRole((string)$this->option('role'));
$declaration->setMapper((string)$this->option('mapper'));
$declaration->setTable((string)$this->option('table'));
$declaration->setDatabase((string)$this->option('database'));
$declaration->setInflection((string)$this->option('inflection'));

foreach ($this->option('field') as $field) {
if (\strpos($field, ':') === false) {
throw new ScaffolderException("Field definition must in 'name:type' or 'name:type' form");
}

$parts = \explode(':', $field);
[$name, $type] = $parts;

$declaration->addField($name, $accessibility, $type);
}

$declaration->declareSchema();

$this->writeDeclaration($declaration);

if ($this->option('repository')) {
$console->run('create:repository', [
'name' => !empty($repository) ? $repository : $this->argument('name'),
]);
}

return self::SUCCESS;
}

protected function declarationClass(string $element): string
{
return $this->config->declarationOptions($element)[(string)$this->argument('format')];
}

private function validateAccessibility(string $accessibility): void
{
if (
!\in_array($accessibility, [
AbstractDeclaration::ACCESS_PUBLIC,
AbstractDeclaration::ACCESS_PROTECTED,
AbstractDeclaration::ACCESS_PRIVATE,
], true)
) {
throw new ScaffolderException("Invalid accessibility value `$accessibility`");
}
}
}
87 changes: 87 additions & 0 deletions src/Console/Command/Scaffolder/MigrationCommand.php
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Spiral\Cycle\Console\Command\Scaffolder;

use Spiral\Cycle\Scaffolder\Declaration\MigrationDeclaration;
use Cycle\Migrations\Migrator;
use Spiral\Reactor\FileDeclaration;
use Spiral\Scaffolder\Command\AbstractCommand;
use Spiral\Scaffolder\Exception\ScaffolderException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class MigrationCommand extends AbstractCommand
{
protected const ELEMENT = 'migration';

protected const NAME = 'create:migration';
protected const DESCRIPTION = 'Create migration declaration';
protected const ARGUMENTS = [
['name', InputArgument::REQUIRED, 'Migration name'],
];
protected const OPTIONS = [
[
'table',
't',
InputOption::VALUE_OPTIONAL,
'Table to be created table',
],
[
'field',
'f',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Create field in a format "name:type"',
],
[
'comment',
null,
InputOption::VALUE_OPTIONAL,
'Optional comment to add as class header',
],
];

/**
* Create migration declaration.
*
* @throws ScaffolderException
*/
public function perform(Migrator $migrator): int
{
/** @var MigrationDeclaration $declaration */
$declaration = $this->createDeclaration();

if (!empty($this->option('table'))) {
$fields = [];
foreach ($this->option('field') as $field) {
if (!\str_contains($field, ':')) {
throw new ScaffolderException("Field definition must in 'name:type' form");
}

[$name, $type] = \explode(':', $field);
$fields[$name] = $type;
}

$declaration->declareCreation((string)$this->option('table'), $fields);
}

$file = new FileDeclaration($this->getNamespace());
$file->setDirectives('strict_types=1');
$file->setComment($this->config->headerLines());
$file->addElement($declaration);

$filename = $migrator->getRepository()->registerMigration(
(string)$this->argument('name'),
$declaration->getName(),
$file->render()
);

$this->writeln(
"Declaration of '<info>{$declaration->getName()}</info>' "
. "has been successfully written into '<comment>{$filename}</comment>'."
);

return self::SUCCESS;
}
}

0 comments on commit f4ceb85

Please sign in to comment.