Completed
Push — master ( 1235b0...90fadd )
by Tim
15s
created

SimpleConfigurationLoader::getContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Cli\SimpleConfigurationLoader
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2016 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import-cli-simple
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Cli;
22
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\DependencyInjection\ContainerInterface;
25
use TechDivision\Import\App\Simple;
26
use TechDivision\Import\Utils\CommandNames;
27
use TechDivision\Import\Utils\EntityTypeCodes;
28
use TechDivision\Import\Cli\Command\InputOptionKeys;
29
use TechDivision\Import\Cli\Configuration\LibraryLoader;
30
use TechDivision\Import\Cli\Utils\DependencyInjectionKeys;
31
use TechDivision\Import\Cli\Utils\MagentoConfigurationKeys;
32
use TechDivision\Import\Utils\Mappings\CommandNameToEntityTypeCode;
33
use TechDivision\Import\ConfigurationFactoryInterface;
34
use TechDivision\Import\Configuration\Jms\Configuration;
35
use TechDivision\Import\Configuration\Jms\ConfigurationFactory;
36
37
/**
38
 * The configuration loader implementation.
39
 *
40
 * @author    Tim Wagner <[email protected]>
41
 * @copyright 2016 TechDivision GmbH <[email protected]>
42
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
43
 * @link      https://github.com/techdivision/import-cli-simple
44
 * @link      http://www.techdivision.com
45
 */
46
class SimpleConfigurationLoader implements ConfigurationLoaderInterface
47
{
48
49
    /**
50
     * The composer name => Magento Edition mappings.
51
     *
52
     * @var array
53
     */
54
    protected $editionMappings = array(
55
        'magento2ce'                 => 'CE',
56
        'project-community-edition'  => 'CE',
57
        'magento2ee'                 => 'EE',
58
        'project-enterprise-edition' => 'EE'
59
    );
60
61
    /**
62
     * The array with the default entity type => configuration mapping.
63
     *
64
     * @var array
65
     */
66
    protected $defaultConfigurations = array(
67
        'ce' => array(
68
            EntityTypeCodes::NONE             => 'techdivision/import-product',
69
            EntityTypeCodes::EAV_ATTRIBUTE    => 'techdivision/import-attribute',
70
            EntityTypeCodes::CATALOG_PRODUCT  => 'techdivision/import-product',
71
            EntityTypeCodes::CATALOG_CATEGORY => 'techdivision/import-category'
72
        ),
73
        'ee' => array(
74
            EntityTypeCodes::NONE             => 'techdivision/import-product-ee',
75
            EntityTypeCodes::EAV_ATTRIBUTE    => 'techdivision/import-attribute',
76
            EntityTypeCodes::CATALOG_PRODUCT  => 'techdivision/import-product-ee',
77
            EntityTypeCodes::CATALOG_CATEGORY => 'techdivision/import-category-ee'
78
        )
79
    );
80
81
    /**
82
     * The container instance.
83
     *
84
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
85
     */
86
    protected $container;
87
88
    /**
89
     * The actual input instance.
90
     *
91
     * @var \Symfony\Component\Console\Input\InputInterface
92
     */
93
    protected $input;
94
95
    /**
96
     * The library loader instance.
97
     *
98
     * @param \TechDivision\Import\Cli\LibraryLoader
99
     */
100
    protected $libraryLoader;
101
102
    /**
103
     * The configuration factory instance.
104
     *
105
     * @var \TechDivision\Import\ConfigurationFactoryInterface
106
     */
107
    protected $configurationFactory;
108
109
    /**
110
     * The available command names.
111
     *
112
     * @var \TechDivision\Import\Utils\CommandNames
113
     */
114
    protected $commandNames;
115
116
    /**
117
     * The mapping of the command names to the entity type codes
118
     *
119
     * @var \TechDivision\Import\Utils\Mappings\CommandNameToEntityTypeCode
120
     */
121
    protected $commandNameToEntityTypeCode;
122
123
    /**
124
     * Initializes the configuration loader.
125
     *
126
     * @param \Symfony\Component\Console\Input\InputInterface                 $input                        The input instance
127
     * @param \Symfony\Component\DependencyInjection\ContainerInterface       $container                    The container instance
128
     * @param \TechDivision\Import\Cli\LibraryLoader                          $libraryLoader                The configuration loader instance
129
     * @param \TechDivision\Import\ConfigurationFactoryInterface              $configurationFactory         The configuration factory instance
130
     * @param \TechDivision\Import\Utils\CommandNames                         $commandNames                 The available command names
131
     * @param \TechDivision\Import\Utils\Mappings\CommandNameToEntityTypeCode $commandNameToEntityTypeCodes The mapping of the command names to the entity type codes
132
     */
133
    public function __construct(
134
        InputInterface $input,
135
        ContainerInterface $container,
136
        LibraryLoader $libraryLoader,
137
        ConfigurationFactoryInterface $configurationFactory,
138
        CommandNames $commandNames,
139
        CommandNameToEntityTypeCode $commandNameToEntityTypeCodes
140
    ) {
141
142
        // set the passed instances
143
        $this->input = $input;
144
        $this->container = $container;
145
        $this->libraryLoader = $libraryLoader;
146
        $this->configurationFactory = $configurationFactory;
147
        $this->commandNames = $commandNames;
148
        $this->commandNameToEntityTypeCode = $commandNameToEntityTypeCodes;
149
    }
150
151
    /**
152
     * Factory implementation to create a new initialized configuration instance.
153
     *
154
     * If command line options are specified, they will always override the
155
     * values found in the configuration file.
156
     *
157
     * @return \TechDivision\Import\Cli\Configuration The configuration instance
158
     * @throws \Exception Is thrown, if the specified configuration file doesn't exist or the mandatory arguments/options to run the requested operation are not available
159
     */
160
    public function load()
161
    {
162
163
        // load the actual vendor directory and entity type code
164
        $vendorDir = $this->getVendorDir();
165
166
        // the path of the JMS serializer directory, relative to the vendor directory
167
        $jmsDir = DIRECTORY_SEPARATOR . 'jms' . DIRECTORY_SEPARATOR . 'serializer' . DIRECTORY_SEPARATOR . 'src';
168
169
        // try to find the path to the JMS Serializer annotations
170
        if (!file_exists($annotationDir = $vendorDir . DIRECTORY_SEPARATOR . $jmsDir)) {
171
            // stop processing, if the JMS annotations can't be found
172
            throw new \Exception(
173
                sprintf(
174
                    'The jms/serializer libarary can not be found in one of "%s"',
175
                    implode(', ', $vendorDir)
176
                )
177
            );
178
        }
179
180
        // register the autoloader for the JMS serializer annotations
181
        \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
182
            'JMS\Serializer\Annotation',
183
            $annotationDir
184
        );
185
186
        // query whether or not, a configuration file has been specified
187
        if ($configuration = $this->input->getOption(InputOptionKeys::CONFIGURATION)) {
188
            // load the configuration from the file with the given filename
189
            $instance = $this->configurationFactory->factory($configuration);
190
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
191
        } elseif ($magentoEdition = $this->input->getOption(InputOptionKeys::MAGENTO_EDITION)) {
192
            // use the Magento Edition that has been specified as option
193
            $instance = $this->configurationFactory->factory($this->getDefaultConfiguration($magentoEdition, $this->getEntityTypeCode()));
194
195
            // override the Magento Edition
196
            $instance->setMagentoEdition($magentoEdition);
197
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
198
        } else {
199
            // finally, query whether or not the installation directory is a valid Magento root directory
200
            if (!$this->isMagentoRootDir($installationDir = $this->input->getOption(InputOptionKeys::INSTALLATION_DIR))) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class TechDivision\Import\Cli\SimpleConfigurationLoader as the method isMagentoRootDir() does only exist in the following sub-classes of TechDivision\Import\Cli\SimpleConfigurationLoader: TechDivision\Import\Cli\ConfigurationLoader. 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...
201
                throw new \Exception(
202
                    sprintf(
203
                        'Directory "%s" specified with option "--installation-dir" is not a valid Magento root directory',
204
                        $installationDir
205
                    )
206
                );
207
            }
208
209
            // load the composer file from the Magento root directory
210
            $composer = json_decode(file_get_contents($composerFile = sprintf('%s/composer.json', $installationDir)), true);
211
212
            // try to load and explode the Magento Edition identifier from the Composer name
213
            $explodedVersion = explode('/', $composer[MagentoConfigurationKeys::COMPOSER_EDITION_NAME_ATTRIBUTE]);
214
215
            // try to locate Magento Edition
216
            if (!isset($this->editionMappings[$possibleEdition = end($explodedVersion)])) {
217
                throw new \Exception(
218
                    sprintf(
219
                        '"%s" detected in "%s" is not a valid Magento Edition, please set Magento Edition with the "--magento-edition" option',
220
                        $possibleEdition,
221
                        $composerFile
222
                    )
223
                );
224
            }
225
226
            // if Magento Edition/Version are available, load them
227
            $magentoEdition = $this->editionMappings[$possibleEdition];
228
229
            // use the Magento Edition that has been detected by the installation directory
230
            $instance = $this->configurationFactory->factory($this->getDefaultConfiguration($magentoEdition, $this->getEntityTypeCode()));
231
232
            // override the Magento Edition, if NOT explicitly specified
233
            $instance->setMagentoEdition($magentoEdition);
234
        }
235
236
        // query whether or not a system name has been specified as command line option, if yes override the value from the configuration file
237 View Code Duplication
        if (($this->input->hasOptionSpecified(InputOptionKeys::SYSTEM_NAME) && $this->input->getOption(InputOptionKeys::SYSTEM_NAME)) || $instance->getSystemName() === null) {
0 ignored issues
show
Bug introduced by
The method hasOptionSpecified() does not exist on Symfony\Component\Console\Input\InputInterface. Did you maybe mean hasOption()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238
            $instance->setSystemName($this->input->getOption(InputOptionKeys::SYSTEM_NAME));
239
        }
240
241
        // query whether or not a PID filename has been specified as command line option, if yes override the value from the configuration file
242 View Code Duplication
        if (($this->input->hasOptionSpecified(InputOptionKeys::PID_FILENAME) && $this->input->getOption(InputOptionKeys::PID_FILENAME)) || $instance->getPidFilename() === null) {
0 ignored issues
show
Bug introduced by
The method hasOptionSpecified() does not exist on Symfony\Component\Console\Input\InputInterface. Did you maybe mean hasOption()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
            $instance->setPidFilename($this->input->getOption(InputOptionKeys::PID_FILENAME));
244
        }
245
246
        // query whether or not a Magento installation directory has been specified as command line option, if yes override the value from the configuration file
247 View Code Duplication
        if (($this->input->hasOptionSpecified(InputOptionKeys::INSTALLATION_DIR) && $this->input->getOption(InputOptionKeys::INSTALLATION_DIR)) || $instance->getInstallationDir() === null) {
0 ignored issues
show
Bug introduced by
The method hasOptionSpecified() does not exist on Symfony\Component\Console\Input\InputInterface. Did you maybe mean hasOption()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
248
            $instance->setInstallationDir($this->input->getOption(InputOptionKeys::INSTALLATION_DIR));
249
        }
250
251
        // query whether or not a Magento edition has been specified as command line option, if yes override the value from the configuration file
252 View Code Duplication
        if (($this->input->hasOptionSpecified(InputOptionKeys::MAGENTO_EDITION) && $this->input->getOption(InputOptionKeys::MAGENTO_EDITION)) || $instance->getMagentoEdition() === null) {
0 ignored issues
show
Bug introduced by
The method hasOptionSpecified() does not exist on Symfony\Component\Console\Input\InputInterface. Did you maybe mean hasOption()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
            $instance->setMagentoEdition($this->input->getOption(InputOptionKeys::MAGENTO_EDITION));
254
        }
255
256
        // query whether or not a directory for the source files has been specified as command line option, if yes override the value from the configuration file
257 View Code Duplication
        if (($this->input->hasOptionSpecified(InputOptionKeys::SOURCE_DIR) && $this->input->getOption(InputOptionKeys::SOURCE_DIR)) || $instance->getSourceDir() === null) {
0 ignored issues
show
Bug introduced by
The method hasOptionSpecified() does not exist on Symfony\Component\Console\Input\InputInterface. Did you maybe mean hasOption()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
258
            $instance->setSourceDir($this->input->getOption(InputOptionKeys::SOURCE_DIR));
259
        }
260
261
        // return the initialized configuration instance
262
        return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $instance; (TechDivision\Import\Conf...ation\Jms\Configuration) is incompatible with the return type declared by the interface TechDivision\Import\Cli\...onLoaderInterface::load of type TechDivision\Import\Cli\Configuration.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
263
    }
264
265
    /**
266
     * Return's the DI container instance.
267
     *
268
     * @return \Symfony\Component\DependencyInjection\ContainerInterface The DI container instance
269
     */
270
    protected function getContainer()
271
    {
272
        return $this->container;
273
    }
274
275
    /**
276
     * Return's the absolute path to the actual vendor directory.
277
     *
278
     * @return string The absolute path to the actual vendor directory
279
     * @throws \Exception Is thrown, if none of the possible vendor directories can be found
280
     */
281
    protected function getVendorDir()
282
    {
283
        return $this->getContainer()->getParameter(DependencyInjectionKeys::CONFIGURATION_VENDOR_DIR);
284
    }
285
286
    /**
287
     * Return's the actual command name.
288
     *
289
     * @return string The actual command name
290
     */
291
    protected function getCommandName()
292
    {
293
        return $this->input->getArgument('command');
294
    }
295
296
    /**
297
     * Return's the command's entity type code.
298
     *
299
     * @return string The command's entity type code
300
     * @throws \Exception Is thrown, if the command name can not be mapped
301
     */
302
    protected function getEntityTypeCode()
303
    {
304
305
        // try to map the command name to a entity type code
306
        if (array_key_exists($commandName = $this->getCommandName(), $this->commandNameToEntityTypeCode)) {
307
            return $this->commandNameToEntityTypeCode[$commandName];
308
        }
309
310
        // throw an exception if not possible
311
        throw new \Exception(sprintf('Can\' map command name %s to a entity type', $commandName));
312
    }
313
314
    /**
315
     * Return's the default configuration for the passed Magento Edition and the actual entity type.
316
     *
317
     * @param string $magentoEdition The Magento Edition to return the configuration for
318
     * @param string $entityTypeCode The entity type code to use
319
     *
320
     * @return string The path to the default configuration
321
     */
322
    protected function getDefaultConfiguration($magentoEdition, $entityTypeCode)
323
    {
324
        return sprintf(
325
            '%s/%s/etc/techdivision-import.json',
326
            $this->getVendorDir(),
327
            $this->getDefaultConfigurationLibrary(
328
                $magentoEdition,
329
                $entityTypeCode
330
            )
331
        );
332
    }
333
334
    /**
335
     * Return's the Magento Edition and entity type's specific default library that contains
336
     * the configuration file.
337
     *
338
     * @param string $magentoEdition The Magento Edition to return the default library for
339
     * @param string $entityTypeCode The entity type code to return the default library file for
340
     *
341
     * @return string The name of the library that contains the default configuration file for the passed Magento Edition and entity type code
342
     * @throws \Exception Is thrown, if no default configuration for the passed entity type code is available
343
     */
344
    protected function getDefaultConfigurationLibrary($magentoEdition, $entityTypeCode)
345
    {
346
347
        // query whether or not, a default configuration file for the passed entity type is available
348
        if (isset($this->defaultConfigurations[$edition = strtolower($magentoEdition)])) {
349
            if (isset($this->defaultConfigurations[$edition][$entityTypeCode])) {
350
                return $this->defaultConfigurations[$edition][$entityTypeCode];
351
            }
352
353
            // throw an exception, if the passed entity type is not supported
354
            throw new \Exception(
355
                sprintf(
356
                    'Entity Type Code \'%s\' not supported by entity type code \'%s\' (MUST be one of catalog_product, catalog_category or eav_attribute)',
357
                    $edition,
358
                    $entityTypeCode
359
                )
360
            );
361
        }
362
363
        // throw an exception, if the passed edition is not supported
364
        throw new \Exception(
365
            sprintf(
366
                'Default configuration for Magento \'%s\' not supported (MUST be one of CE or EE)',
367
                $magentoEdition
368
            )
369
        );
370
    }
371
}
372