Completed
Pull Request — master (#877)
by Kévin
01:55
created

ImportMappingDoctrineCommand   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 11

Importance

Changes 0
Metric Value
wmc 14
lcom 2
cbo 11
dl 0
loc 141
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A configure() 0 42 1
D execute() 0 76 12
1
<?php
2
3
namespace Doctrine\Bundle\DoctrineBundle\Command;
4
5
use Doctrine\Common\Persistence\ManagerRegistry;
6
use Doctrine\ORM\Mapping\Driver\DatabaseDriver;
7
use Doctrine\ORM\Tools\Console\MetadataFilter;
8
use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory;
9
use Doctrine\ORM\Tools\Export\ClassMetadataExporter;
10
use InvalidArgumentException;
11
use Symfony\Component\Console\Input\InputArgument;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
16
/**
17
 * Import Doctrine ORM metadata mapping information from an existing database.
18
 *
19
 * @final
20
 */
21
class ImportMappingDoctrineCommand extends DoctrineCommand
22
{
23
    /** @var string[] */
24
    private $bundles;
25
26
    /**
27
     * @param string[] $bundles
28
     */
29
    public function __construct(ManagerRegistry $doctrine, array $bundles)
30
    {
31
        parent::__construct($doctrine);
32
33
        $this->bundles = $bundles;
34
    }
35
36
    /**
37
     * {@inheritDoc}
38
     */
39
    protected function configure()
40
    {
41
        $this
42
            ->setName('doctrine:mapping:import')
43
            ->addArgument('name', InputArgument::REQUIRED, 'The bundle or namespace to import the mapping information to')
44
            ->addArgument('mapping-type', InputArgument::OPTIONAL, 'The mapping type to export the imported mapping information to')
45
            ->addOption('em', null, InputOption::VALUE_OPTIONAL, 'The entity manager to use for this command')
46
            ->addOption('shard', null, InputOption::VALUE_REQUIRED, 'The shard connection to use for this command')
47
            ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be mapped.')
48
            ->addOption('force', null, InputOption::VALUE_NONE, 'Force to overwrite existing mapping files.')
49
            ->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path where the files would be generated (not used when a bundle is passed).')
50
            ->setDescription('Imports mapping information from an existing database')
51
            ->setHelp(<<<EOT
52
The <info>%command.name%</info> command imports mapping information
53
from an existing database:
54
55
Generate annotation mappings into the src/ directory using App as the namespace:
56
<info>php %command.full_name% App\\\Entity annotation --path=src/Entity</info>
57
58
Generate xml mappings into the config/doctrine/ directory using App as the namespace:
59
<info>php %command.full_name% App\\\Entity xml --path=config/doctrine</info>
60
61
Generate XML mappings into a bundle:
62
<info>php %command.full_name% "MyCustomBundle" xml</info>
63
64
You can also optionally specify which entity manager to import from with the
65
<info>--em</info> option:
66
67
<info>php %command.full_name% "MyCustomBundle" xml --em=default</info>
68
69
If you don't want to map every entity that can be found in the database, use the
70
<info>--filter</info> option. It will try to match the targeted mapped entity with the
71
provided pattern string.
72
73
<info>php %command.full_name% "MyCustomBundle" xml --filter=MyMatchedEntity</info>
74
75
Use the <info>--force</info> option, if you want to override existing mapping files:
76
77
<info>php %command.full_name% "MyCustomBundle" xml --force</info>
78
EOT
79
        );
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85
    protected function execute(InputInterface $input, OutputInterface $output)
86
    {
87
        $type = $input->getArgument('mapping-type') ?: 'xml';
88
        if ($type === 'yaml') {
89
            $type = 'yml';
90
        }
91
92
        $namespaceOrBundle = $input->getArgument('name');
93
        if (isset($this->bundles[$namespaceOrBundle])) {
94
            $bundle    = $this->getApplication()->getKernel()->getBundle($namespaceOrBundle);
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...
95
            $namespace = $bundle->getNamespace() . '\Entity';
96
97
            $destPath = $bundle->getPath();
98
            if ($type === 'annotation') {
99
                $destPath .= '/Entity';
100
            } else {
101
                $destPath .= '/Resources/config/doctrine';
102
            }
103
        } else {
104
            // assume a namespace has been passed
105
            $namespace = $namespaceOrBundle;
106
            $destPath  = $input->getOption('path');
107
            if ($destPath === null) {
108
                throw new InvalidArgumentException('The --path option is required when passing a namespace (e.g. --path=src). If you intended to pass a bundle name, check your spelling.');
109
            }
110
        }
111
112
        $cme      = new ClassMetadataExporter();
113
        $exporter = $cme->getExporter($type);
0 ignored issues
show
Bug introduced by
It seems like $type defined by $input->getArgument('mapping-type') ?: 'xml' on line 87 can also be of type array<integer,string>; however, Doctrine\ORM\Tools\Expor...Exporter::getExporter() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
114
        $exporter->setOverwriteExistingFiles($input->getOption('force'));
115
116
        if ($type === 'annotation') {
117
            $entityGenerator = $this->getEntityGenerator();
118
            $exporter->setEntityGenerator($entityGenerator);
119
        }
120
121
        $em = $this->getEntityManager($input->getOption('em'), $input->getOption('shard'));
122
123
        $databaseDriver = new DatabaseDriver($em->getConnection()->getSchemaManager());
124
        $em->getConfiguration()->setMetadataDriverImpl($databaseDriver);
125
126
        $emName = $input->getOption('em');
127
        $emName = $emName ? $emName : 'default';
128
129
        $cmf = new DisconnectedClassMetadataFactory();
130
        $cmf->setEntityManager($em);
131
        $metadata = $cmf->getAllMetadata();
132
        $metadata = MetadataFilter::filter($metadata, $input->getOption('filter'));
133
        if ($metadata) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
134
            $output->writeln(sprintf('Importing mapping information from "<info>%s</info>" entity manager', $emName));
135
            foreach ($metadata as $class) {
136
                $className   = $class->name;
137
                $class->name = $namespace . '\\' . $className;
138
                if ($type === 'annotation') {
139
                    $path = $destPath . '/' . str_replace('\\', '.', $className) . '.php';
140
                } else {
141
                    $path = $destPath . '/' . str_replace('\\', '.', $className) . '.orm.' . $type;
142
                }
143
                $output->writeln(sprintf('  > writing <comment>%s</comment>', $path));
144
                $code = $exporter->exportClassMetadata($class);
145
                $dir  = dirname($path);
146
                if (! is_dir($dir)) {
147
                    mkdir($dir, 0775, true);
148
                }
149
                file_put_contents($path, $code);
150
                chmod($path, 0664);
151
            }
152
153
            return 0;
154
        }
155
156
        $output->writeln('Database does not have any mapping information.');
157
        $output->writeln('');
158
159
        return 1;
160
    }
161
}
162