Completed
Push — master ( 2e9827...ef00d6 )
by Rafał
10:01
created

TenantAwareThemeLoader::hydrateThemes()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.7537
c 0
b 0
f 0
cc 6
nc 17
nop 2
1
<?php
2
3
/*
4
 * This file is part of the Superdesk Web Publisher Core Bundle.
5
 *
6
 * Copyright 2016 Sourcefabric z.u. and contributors.
7
 *
8
 * For the full copyright and license information, please see the
9
 * AUTHORS and LICENSE files distributed with this source code.
10
 *
11
 * @copyright 2016 Sourcefabric z.ú
12
 * @license http://www.superdesk.org/license
13
 */
14
15
namespace SWP\Bundle\CoreBundle\Theme\Loader;
16
17
use Sylius\Bundle\ThemeBundle\Configuration\ConfigurationProviderInterface;
18
use Sylius\Bundle\ThemeBundle\Factory\ThemeAuthorFactoryInterface;
19
use Sylius\Bundle\ThemeBundle\Factory\ThemeFactoryInterface;
20
use Sylius\Bundle\ThemeBundle\Factory\ThemeScreenshotFactoryInterface;
21
use Sylius\Bundle\ThemeBundle\Loader\CircularDependencyCheckerInterface;
22
use Sylius\Bundle\ThemeBundle\Loader\CircularDependencyFoundException;
23
use Sylius\Bundle\ThemeBundle\Loader\ThemeLoaderInterface;
24
use Sylius\Bundle\ThemeBundle\Loader\ThemeLoadingFailedException;
25
use Sylius\Bundle\ThemeBundle\Model\ThemeAuthor;
26
use Sylius\Bundle\ThemeBundle\Model\ThemeInterface;
27
use Sylius\Bundle\ThemeBundle\Model\ThemeScreenshot;
28
29
/**
30
 * @author Kamil Kokot <[email protected]>
31
 * @author Paweł Mikołajczuk <[email protected]>
32
 */
33
final class TenantAwareThemeLoader implements ThemeLoaderInterface
34
{
35
    /**
36
     * @var ConfigurationProviderInterface
37
     */
38
    private $configurationProvider;
39
40
    /**
41
     * @var ThemeFactoryInterface
42
     */
43
    private $themeFactory;
44
45
    /**
46
     * @var ThemeAuthorFactoryInterface
47
     */
48
    private $themeAuthorFactory;
49
50
    /**
51
     * @var ThemeScreenshotFactoryInterface
52
     */
53
    private $themeScreenshotFactory;
54
55
    /**
56
     * @var CircularDependencyCheckerInterface
57
     */
58
    private $circularDependencyChecker;
59
60
    /**
61
     * @param ConfigurationProviderInterface     $configurationProvider
62
     * @param ThemeFactoryInterface              $themeFactory
63
     * @param ThemeAuthorFactoryInterface        $themeAuthorFactory
64
     * @param ThemeScreenshotFactoryInterface    $themeScreenshotFactory
65
     * @param CircularDependencyCheckerInterface $circularDependencyChecker
66
     */
67
    public function __construct(
68
        ConfigurationProviderInterface $configurationProvider,
69
        ThemeFactoryInterface $themeFactory,
70
        ThemeAuthorFactoryInterface $themeAuthorFactory,
71
        ThemeScreenshotFactoryInterface $themeScreenshotFactory,
72
        CircularDependencyCheckerInterface $circularDependencyChecker
73
    ) {
74
        $this->configurationProvider = $configurationProvider;
75
        $this->themeFactory = $themeFactory;
76
        $this->themeAuthorFactory = $themeAuthorFactory;
77
        $this->themeScreenshotFactory = $themeScreenshotFactory;
78
        $this->circularDependencyChecker = $circularDependencyChecker;
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function load(): array
85
    {
86
        $configurations = $this->configurationProvider->getConfigurations();
87
88
        $themes = $this->initializeThemes($configurations);
89
        $themes = $this->hydrateThemes($configurations, $themes);
90
91
        $this->checkForCircularDependencies($themes);
92
93
        return array_values($themes);
94
    }
95
96
    /**
97
     * @param array $configurations
98
     *
99
     * @return ThemeInterface[]
100
     */
101
    private function initializeThemes(array $configurations)
102
    {
103
        $themes = [];
104
        foreach ($configurations as $configuration) {
105
            /* @var ThemeInterface $theme */
106
            $themes[$configuration['name']] = $this->themeFactory->create($configuration['name'], $configuration['path']);
107
        }
108
109
        return $themes;
110
    }
111
112
    /**
113
     * @param array            $configurations
114
     * @param ThemeInterface[] $themes
115
     *
116
     * @return ThemeInterface[]
117
     */
118
    private function hydrateThemes(array $configurations, array $themes)
119
    {
120
        foreach ($configurations as $configuration) {
121
            $themeName = $configuration['name'];
122
            $configuration['parents'] = $this->convertParentsNamesToParentsObjects($themeName, $configuration['parents'], $themes);
123
            $configuration['authors'] = $this->convertAuthorsArraysToAuthorsObjects($configuration['authors']);
124
            $configuration['screenshots'] = $this->convertScreenshotsArraysToScreenshotsObjects($configuration['screenshots']);
125
126
            $theme = $themes[$configuration['name']];
127
128
            $theme->setTitle($configuration['title'] ?? null);
129
            $theme->setDescription($configuration['description'] ?? null);
130
            $theme->setName($configuration['name']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Sylius\Bundle\ThemeBundle\Model\ThemeInterface as the method setName() does only exist in the following implementations of said interface: SWP\Bundle\CoreBundle\Theme\Model\Theme.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
131
132
            foreach ($configuration['parents'] as $parentTheme) {
133
                $theme->addParent($parentTheme);
134
            }
135
136
            foreach ($configuration['authors'] as $themeAuthor) {
137
                $theme->addAuthor($themeAuthor);
138
            }
139
140
            foreach ($configuration['screenshots'] as $themeScreenshot) {
141
                $theme->addScreenshot($themeScreenshot);
142
            }
143
            $themes[$themeName] = $theme;
144
145
            if(isset($configuration['generatedData'])) {
146
                $theme->setGeneratedData($configuration['generatedData']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Sylius\Bundle\ThemeBundle\Model\ThemeInterface as the method setGeneratedData() does only exist in the following implementations of said interface: SWP\Bundle\CoreBundle\Theme\Model\Theme.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
147
            }
148
        }
149
150
        return $themes;
151
    }
152
153
    /**
154
     * @param ThemeInterface[] $themes
155
     */
156
    private function checkForCircularDependencies(array $themes)
157
    {
158
        try {
159
            foreach ($themes as $theme) {
160
                $this->circularDependencyChecker->check($theme);
161
            }
162
        } catch (CircularDependencyFoundException $exception) {
163
            throw new ThemeLoadingFailedException('Circular dependency found.', 0, $exception);
164
        }
165
    }
166
167
    /**
168
     * @param string $themeName
169
     * @param array  $parentsNames
170
     * @param array  $existingThemes
171
     *
172
     * @return ThemeInterface[]
173
     */
174
    private function convertParentsNamesToParentsObjects($themeName, array $parentsNames, array $existingThemes)
175
    {
176
        $tenantCode = substr($themeName, strpos($themeName, '@') + 1);
177
178
        return array_map(function ($parentName) use ($themeName, $existingThemes, $tenantCode) {
179
            $parentName .= '@'.$tenantCode;
180
            if (!isset($existingThemes[$parentName])) {
181
                throw new ThemeLoadingFailedException(sprintf(
182
                    'Unexisting theme "%s" is required by "%s".',
183
                    $parentName,
184
                    $themeName
185
                ));
186
            }
187
188
            return $existingThemes[$parentName];
189
        }, $parentsNames);
190
    }
191
192
    /**
193
     * @param array $authorsArrays
194
     *
195
     * @return ThemeAuthor[]
196
     */
197
    private function convertAuthorsArraysToAuthorsObjects(array $authorsArrays)
198
    {
199
        return array_map(function (array $authorArray) {
200
            return $this->themeAuthorFactory->createFromArray($authorArray);
201
        }, $authorsArrays);
202
    }
203
204
    /**
205
     * @param array $screenshotsArrays
206
     *
207
     * @return ThemeScreenshot[]
208
     */
209
    private function convertScreenshotsArraysToScreenshotsObjects(array $screenshotsArrays)
210
    {
211
        return array_map(function (array $screenshotArray) {
212
            return $this->themeScreenshotFactory->createFromArray($screenshotArray);
213
        }, $screenshotsArrays);
214
    }
215
}
216