Completed
Push — master ( 33d1bb...57b6f9 )
by Gaetano
10:55
created

GenerateCommand::getMigrationDirectory()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 0
cp 0
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 12
nc 5
nop 1
crap 30
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Symfony\Component\Console\Input\InputArgument;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Symfony\Component\HttpFoundation\File\Exception\FileException;
10
use Symfony\Component\Yaml\Yaml;
11
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
12
use Kaliop\eZMigrationBundle\API\MatcherInterface;
13
14
class GenerateCommand extends AbstractCommand
15
{
16
    const DIR_CREATE_PERMISSIONS = 0755;
17
18
    private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
19
    private $availableModes = array('create', 'update', 'delete');
20
    private $thisBundle = 'EzMigrationBundle';
21
22
    /**
23
     * Configure the console command
24
     */
25
    protected function configure()
26 20
    {
27
        $this->setName('kaliop:migration:generate')
28 20
            ->setDescription('Generate a blank migration definition file.')
29 20
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
30 20
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (role, content_type, content, generic, db, php)', '')
31 20
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
32 20
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
33 20
            ->addOption('match-value', null, InputOption::VALUE_REQUIRED, 'The identifier value used to find the entity to generate the migration for. Can have many values separated by commas', null)
34 20
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
35 20
            ->addOption('lang', null, InputOption::VALUE_REQUIRED, 'The language of the migration (eng-GB, ger-DE, ...)', 'eng-GB')
36 20
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
37
            ->addOption('role', null, InputOption::VALUE_REQUIRED, 'Deprecated: The role identifier (or id) that you would like to update, for type=role', null)
38
            ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
39
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
40
            ->setHelp(<<<EOT
41
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
42
43
    <info>php ezpublish/console kaliop:migration:generate bundlename</info>
44
45
You can optionally specify the file type to generate with <info>--format</info>, as well a name for the migration:
46
47
    <info>php ezpublish/console kaliop:migration:generate --format=json bundlename migrationname</info>
48
49
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
50
51
    <info>php ezpublish/console kaliop:migration:generate --format=sql bundlename</info>
52
53
For role/content/content_type migrations you need to specify the entity that you want to generate the migration for:
54
55
    <info>php ezpublish/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 bundlename</info>
56
57
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies
58 20
you wish for the role. Any not defined will be removed.
59 20
60
    <info>php ezpublish/console kaliop:migration:generate --type=role --match-value=Anonymous bundlename</info>
61
62
For freeform php migrations, you will receive a php class definition
63
64
    <info>php ezpublish/console kaliop:migration:generate --format=php bundlename classname</info>
65
66
Note that you can pass in a custom directory path instead of a bundle name, but, if you do, you will have to use the <info>--path</info>
67
option when you run the <info>migrate</info> command.
68
EOT
69 1
            );
70
    }
71 1
72 1
    /**
73 1
     * Run the command and display the results.
74 1
     *
75 1
     * @param InputInterface $input
76 1
     * @param OutputInterface $output
77
     * @return null|int null or 0 if everything went fine, or an error code
78 1
     * @throws \InvalidArgumentException When an unsupported file type is selected
79
     */
80
    public function execute(InputInterface $input, OutputInterface $output)
81
    {
82 1
        $bundleName = $input->getArgument('bundle');
83 1
        $name = $input->getArgument('name');
84
        $fileType = $input->getOption('format');
85 1
        $migrationType = $input->getOption('type');
86 1
        $role = $input->getOption('role');
87 1
        $matchType = $input->getOption('match-type');
88 1
        $matchValue = $input->getOption('match-value');
89
        $matchExcept = $input->getOption('match-except');
90
        $mode = $input->getOption('mode');
91
        $dbServer = $input->getOption('dbserver');
92 1
93 1
        if ($role != '') {
94
            $output->writeln('<error>The "role" option is deprecated since version 3.2 and will be removed in 4.0. Use "type=role", "match-type=identifier" and "match-value" instead.</error>');
95
            $migrationType = 'role';
96 1
            $matchType = 'identifier';
97 1
            $matchValue = $role;
98 1
            if ($mode == '') {
99 1
                $mode = 'update';
100 1
            }
101 1
        }
102 1
103 1
        if ($bundleName == $this->thisBundle) {
104 1
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
105
        }
106 1
107
        // be kind to lazy users
108 1
        if ($migrationType == '') {
109
            if ($fileType == 'sql') {
110
                $migrationType = 'db';
111
            } elseif ($fileType == 'php') {
112 1
                $migrationType = 'php';
113
            } else {
114
                $migrationType = 'generic';
115
            }
116 1
        }
117 1
118 1
        if (!in_array($fileType, $this->availableMigrationFormats)) {
119
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
120 1
        }
121
122 1
        if (!in_array($mode, $this->availableModes)) {
123 1
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
124 1
        }
125
126 1
        $migrationDirectory = $this->getMigrationDirectory($bundleName);
127 1
128
        if (!is_dir($migrationDirectory)) {
129
            $output->writeln(sprintf(
130
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
131
                $migrationDirectory
132
            ));
133 1
134
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
135
                $output->writeln(sprintf(
136 1
                    "Migrations directory <info>%s</info> has been created",
137 1
                    $migrationDirectory
138 1
                ));
139
            } else {
140 1
                throw new FileException(sprintf(
141
                    "Failed to create migrations directory %s.",
142
                    $migrationDirectory
143 1
                ));
144
            }
145 1
        }
146 1
147 1
        // allow to generate migrations for many entities
148 1
        if (strpos($matchValue, ',') !== false ) {
149 1
            $matchValue = explode(',', $matchValue);
150
        }
151 1
152
        $parameters = array(
153 1
            'dbserver' => $dbServer,
154 1
            'matchType' => $matchType,
155 1
            'matchValue' => $matchValue,
156 1
            'matchExcept' => $matchExcept,
157
            'mode' => $mode,
158 1
            'lang' => $input->getOption('lang')
159 1
        );
160
161
        $date = date('YmdHis');
162 1
163 1
        switch ($fileType) {
164
            case 'sql':
165 1
                /// @todo this logic should come from the DefinitionParser, really
166 1
                if ($name != '') {
167 1
                    $name = '_' . ltrim($name, '_');
168
                }
169 1
                $fileName = $date . '_' . $dbServer . $name . '.sql';
170 1
                break;
171 1
172 1
            case 'php':
173 1
                /// @todo this logic should come from the DefinitionParser, really
174 1
                $className = ltrim($name, '_');
175
                if ($className == '') {
176 1
                    $className = 'Migration';
177
                }
178 1
                // Make sure that php class names are unique, not only migration definition file names
179
                $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
180 1
                if ($existingMigrations) {
181 1
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
182
                }
183
                $parameters = array_merge($parameters, array(
184
                    'class_name' => $className
185
                ));
186
                $fileName = $date . '_' . $className . '.php';
187
                break;
188
189
            default:
190
                if ($name == '') {
191
                    $name = 'placeholder';
192
                }
193 1
                $fileName = $date . '_' . $name . '.' . $fileType;
194
        }
195 1
196 1
        $path = $migrationDirectory . '/' . $fileName;
197 1
198
        $warning = $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
199
200
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
201 1
202 1
        if ($warning != '') {
203 1
            $output->writeln("<comment>$warning</comment>");
204 1
        }
205
    }
206
207 1
    /**
208 1
     * Generates a migration definition file.
209
     *
210
     * @param string $path filename to file to generate (full path)
211
     * @param string $fileType The type of migration file to generate
212
     * @param string $migrationType The type of migration to generate
213
     * @param array $parameters passed on to twig
214
     * @return string A warning message in case file generation was OK but there was something weird
215
     * @throws \Exception
216 1
     */
217
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
218
    {
219 1
        $warning = '';
220 1
221 1
        switch ($migrationType) {
222
            case 'db':
223
            case 'generic':
224 1
            case 'php':
225
                // Generate migration file by template
226
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
227 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...
228
                if (!is_file($templatePath . $template)) {
229
                    throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
230 1
                }
231
232 1
                $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:' . $template, $parameters);
233
                break;
234 1
235
            default:
236 1
                // Generate migration file by executor
237
                $executors = $this->getGeneratingExecutors();
238
                if (!in_array($migrationType, $executors)) {
239 1
                    throw new \Exception("It is not possible to generate a migration of type '$migrationType': executor not found or not a generator");
240
                }
241 1
                $executor = $this->getMigrationService()->getExecutor($migrationType);
242 1
243
                $executor->setLanguageCode($parameters['lang']);
244 1
245 1
                $matchCondition = array($parameters['matchType'] => $parameters['matchValue']);
246 1
                if ($parameters['matchExcept']) {
247
                    $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition);
248 1
                }
249 1
                $data = $executor->generateMigration($matchCondition, $parameters['mode']);
250
251
                if (!is_array($data) || !count($data)) {
252 1
                    $warning = 'Note: the generated migration is empty';
253 1
                }
254 1
255
                switch ($fileType) {
256 1
                    case 'yml':
257
                        $code = Yaml::dump($data, 5);
258 1
                        break;
259
                    case 'json':
260
                        $code = json_encode($data, JSON_PRETTY_PRINT);
261
                        break;
262
                    default:
263
                        throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
264
                }
265
        }
266
267
        file_put_contents($path, $code);
268
269
        return $warning;
270
    }
271
272
    /**
273
     * @param string $bundleName a bundle name or filesystem path to a directory
274
     * @return string
275
     */
276
    protected function getMigrationDirectory($bundleName)
277
    {
278
        // Allow direct usage of a directory path instead of a bundle name
279
        if (strpos($bundleName, '/') !== false && is_dir($bundleName)) {
280
            return rtrim($bundleName, '/');
281
        }
282
283
        $activeBundles = array();
284
        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...
285
            $activeBundles[] = $bundle->getName();
286
        }
287
        asort($activeBundles);
288
        if (!in_array($bundleName, $activeBundles)) {
289
            throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles));
290
        }
291
292
        $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...
293
        $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->getParameter('kaliop_bundle_migration.version_directory');
294
295
        return $migrationDirectory;
296
    }
297
298
    protected function getGeneratingExecutors()
299
    {
300
        $migrationService = $this->getMigrationService();
301
        $executors = $migrationService->listExecutors();
302
        foreach($executors as $key => $name) {
303
            $executor = $migrationService->getExecutor($name);
304
            if (!$executor instanceof MigrationGeneratorInterface) {
305
                unset($executors[$key]);
306
            }
307
        }
308
        return $executors;
309
    }
310
}
311