Completed
Pull Request — master (#91)
by
unknown
06:19
created

GenerateCommand::configure()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 36
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 36
ccs 14
cts 14
cp 1
rs 8.8571
cc 1
eloc 14
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
6
use Symfony\Component\Console\Input\InputArgument;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\HttpFoundation\File\Exception\FileException;
11
use Symfony\Component\Yaml\Yaml;
12
13
class GenerateCommand extends AbstractCommand
14
{
15
    const DIR_CREATE_PERMISSIONS = 0755;
16
17
    private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
18
    private $availableModes = array('create', 'update');
19
    private $thisBundle = 'EzMigrationBundle';
20
21
    /**
22
     * Configure the console command
23
     */
24 27
    protected function configure()
25
    {
26 27
        $this->setName('kaliop:migration:generate')
27 27
            ->setDescription('Generate a blank migration definition file.')
28 27
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (yml, php, sql, json)', 'yml')
29 27
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (role, content_type, generic, db, php)', '')
30 27
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, for type=db (mysql, postgresql, ...)', 'mysql')
31 27
            ->addOption('role', null, InputOption::VALUE_REQUIRED, 'Deprecated: The role identifier (or id) that you would like to update, for type=role', null)
32 27
            ->addOption('identifier', null, InputOption::VALUE_REQUIRED, 'The identifier that you would like to update', null)
33 27
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (create, update)', 'create')
34 27
            ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
35 27
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
36 27
            ->setHelp(<<<EOT
37
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
38
39
    <info>./ezpublish/console kaliop:migration:generate bundlename</info>
40
41
You can optionally specify the file type to generate with <info>--format</info>:
42
43
    <info>./ezpublish/console kaliop:migration:generate --format=yml bundlename migrationname</info>
44
45
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
46
47
    <info>./ezpublish/console kaliop:migration:generate --format=sql bundlename migrationname</info>
48
49
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies you wish for the role. Any not defined will be removed.
50
51
    <info>./ezpublish/console kaliop:migration:generate --role=Anonymous bundlename migrationname
52
53
For freeform php migrations, you will receive a php class definition
54
55
    <info>./ezpublish/console kaliop:migration:generate --format=php bundlename classname</info>
56
57
EOT
58 27
            );
59 27
    }
60
61
    /**
62
     * Run the command and display the results.
63
     *
64
     * @param InputInterface $input
65
     * @param OutputInterface $output
66
     * @return null|int null or 0 if everything went fine, or an error code
67
     * @throws \InvalidArgumentException When an unsupported file type is selected
68
     */
69 1
    public function execute(InputInterface $input, OutputInterface $output)
70
    {
71 1
        $bundleName = $input->getArgument('bundle');
72 1
        $name = $input->getArgument('name');
73 1
        $fileType = $input->getOption('format');
74 1
        $migrationType = $input->getOption('type');
75 1
        $role = $input->getOption('role');
76 1
        $identifier = $input->getOption('identifier');
77 1
        $mode = $input->getOption('mode');
78 1
        $dbServer = $input->getOption('dbserver');
79
80 1
        if ($role != '') {
81 1
            $output->writeln('<error>The "role" option is deprecated since version 3.2 and will be removed in 4.0. Use "identifier" instead.</error>');
82 1
            $migrationType = 'role';
83 1
            $identifier = $role;
84 1
        }
85
86 1
        if ($bundleName == $this->thisBundle) {
87
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
88
        }
89
90 1
        $activeBundles = array();
91 1
        foreach($this->getApplication()->getKernel()->getBundles() as $bundle)
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
92
        {
93 1
            $activeBundles[] = $bundle->getName();
94 1
        }
95 1
        asort($activeBundles);
96 1
        if (!in_array($bundleName, $activeBundles)) {
97
            throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles));
98
        }
99
100 1
        $bundle = $this->getApplication()->getKernel()->getBundle($bundleName);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
101 1
        $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->getParameter('kaliop_bundle_migration.version_directory');
102
103
        // be kind to lazy users
104 1
        if ($migrationType == '') {
105 1
            if ($fileType == 'sql') {
106 1
                $migrationType = 'db';
107 1
            } elseif ($fileType == 'php') {
108 1
                $migrationType = 'php';
109 1
            } else {
110 1
                $migrationType = 'generic';
111
            }
112 1
        }
113
114 1
        if (!in_array($fileType, $this->availableMigrationFormats)) {
115
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
116
        }
117
118 1
        if (!in_array($mode, $this->availableModes)) {
119
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
120
        }
121
122 1
        if (!is_dir($migrationDirectory)) {
123 1
            $output->writeln(sprintf(
124 1
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
125
                $migrationDirectory
126 1
            ));
127
128 1
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
129 1
                $output->writeln(sprintf(
130 1
                    "Migrations directory <info>%s</info> has been created",
131
                    $migrationDirectory
132 1
                ));
133 1
            } else {
134
                throw new FileException(sprintf(
135
                    "Failed to create migrations directory %s.",
136
                    $migrationDirectory
137
                ));
138
            }
139 1
        }
140
141
        $parameters = array(
142 1
            'dbserver' => $dbServer,
143 1
            'identifier' => $identifier,
144
            'mode' => $mode
145 1
        );
146
147 1
        $date = date('YmdHis');
148
149
        switch ($fileType) {
150 1
            case 'sql':
151
                /// @todo this logic should come from the DefinitionParser, really
152 1
                if ($name != '') {
153 1
                    $name = '_' . ltrim($name, '_');
154 1
                }
155 1
                $fileName = $date . '_' . $dbServer . $name . '.sql';
156 1
                break;
157
158 1
            case 'php':
159
                /// @todo this logic should come from the DefinitionParser, really
160 1
                $className = ltrim($name, '_');
161 1
                if ($className == '') {
162 1
                    $className = 'Migration';
163 1
                }
164
                // Make sure that php class names are unique, not only migration definition file names
165 1
                $existingMigrations = count(glob($migrationDirectory.'/*_' . $className .'*.php'));
166 1
                if ($existingMigrations) {
167
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
168
                }
169 1
                $parameters = array_merge($parameters, array(
170 1
                    'namespace' => $bundle->getNamespace(),
171
                    'class_name' => $className
172 1
                ));
173 1
                $fileName = $date . '_' . $className . '.php';
174 1
                break;
175
176 1
            default:
177 1
                if ($name == '') {
178 1
                    $name = 'placeholder';
179 1
                }
180 1
                $fileName = $date . '_' . $name . '.' . $fileType;
181 1
        }
182
183 1
        $path = $migrationDirectory . '/' . $fileName;
184
185 1
        $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
186
187 1
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
188 1
    }
189
190
    /**
191
     * Generates a migration definition file.
192
     *
193
     * @param string $path filename to file to generate (full path)
194
     * @param string $fileType The type of migration file to generate
195
     * @param string $migrationType The type of migration to generate
196
     * @param array $parameters passed on to twig
197
     * @return string The path to the migration file
198
     * @throws \Exception
199
     */
200 1
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
201
    {
202
        switch ($migrationType) {
203 1
            case 'db':
204 1
            case 'generic':
205 1
            case 'php':
206
                // Generate migration file by template
207 1
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
208 1
                $templatePath = $this->getApplication()->getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/';
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
209 1
                if (!is_file($templatePath . $template)) {
210
                    throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
211
                }
212
213 1
                $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:'.$template, $parameters);
214 1
                break;
215 1
            default:
216
                // Generate migration file by executor
217 1
                $migrationService = $this->getMigrationService();
218 1
                $executor = $migrationService->getExecutor($migrationType);
219 1
                if (!$executor instanceof MigrationGeneratorInterface) {
220
                    throw new \Exception("The executor '$migrationType' can not generate a migration");
221
                }
222 1
                $data = $executor->generateMigration($parameters['identifier'], $parameters['mode']);
223
224
                switch ($fileType) {
225 1
                    case 'yml':
226 1
                        $code = Yaml::dump($data, 5);
227 1
                        break;
228
                    case 'json':
229
                        $code = json_encode($data, JSON_PRETTY_PRINT);
230
                        break;
231
                    default:
232
                        throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
233
                }
234 1
        }
235
236 1
        file_put_contents($path, $code);
237 1
    }
238
}
239