Completed
Push — 3.x ( 3e834f...38b337 )
by Grégoire
03:36
created

src/EventListener/AssetsInstallCommandListener.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\EventListener;
15
16
use Symfony\Component\Console\Application;
17
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
18
use Symfony\Component\Console\Exception\InvalidArgumentException;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Style\SymfonyStyle;
22
use Symfony\Component\DependencyInjection\ContainerInterface;
23
use Symfony\Component\Filesystem\Exception\IOException;
24
use Symfony\Component\Filesystem\Filesystem;
25
use Symfony\Component\Finder\Finder;
26
use Symfony\Component\HttpKernel\KernelInterface;
27
28
/**
29
 * This listener extends `assets:install` command when SonataCoreBundle will be not register. Files from `Resources/private/SonataCoreBundleAssets`
30
 * will be copy with the same result like SonataCoreBundle is register.
31
 *
32
 * This class should be remove when support for Bootstrap 3 will be ended or assets system will be remove in favor for encore webpack.
33
 */
34
final class AssetsInstallCommandListener
35
{
36
    public const METHOD_COPY = 'copy';
37
    public const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink';
38
    public const METHOD_RELATIVE_SYMLINK = 'relative symlink';
39
40
    protected static $defaultName = 'assets:install';
41
42
    private $filesystem;
43
    private $projectDir;
44
45
    public function __construct(Filesystem $filesystem, ?string $projectDir = null)
46
    {
47
        if (null === $projectDir) {
48
            @trigger_error(sprintf('Not passing the project directory to the constructor of %s is deprecated since Symfony 4.3 and will not be supported in 5.0.', __CLASS__), E_USER_DEPRECATED);
49
        }
50
51
        $this->filesystem = $filesystem;
52
        $this->projectDir = $projectDir;
53
    }
54
55
    public function copySonataCoreBundleAssets(ConsoleTerminateEvent $event)
56
    {
57
        $command = $event->getCommand();
58
        $application = $command->getApplication();
59
60
        try {
61
            $coreBundle = $application->getKernel()->getBundle('SonataCoreBundle');
62
        } catch (\Exception $e) {
63
            $coreBundle = null;
64
        }
65
66
        if ('assets:install' !== $command->getName() || null !== $coreBundle) {
67
            return;
68
        }
69
70
        $output = $event->getOutput();
0 ignored issues
show
$output is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
71
72
        $this->execute($event->getInput(), $event->getOutput(), $application);
73
    }
74
75
    protected function execute(InputInterface $input, OutputInterface $output, Application $application): int
76
    {
77
        /**
78
         * @var KernelInterface
79
         */
80
        $kernel = $application->getKernel();
0 ignored issues
show
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...
81
82
        $targetArg = rtrim($input->getArgument('target') ?? '', '/');
83
84
        if (!$targetArg) {
85
            $targetArg = $this->getPublicDirectory($kernel->getContainer());
86
        }
87
88
        if (!is_dir($targetArg)) {
89
            $targetArg = $kernel->getProjectDir().'/'.$targetArg;
90
91
            if (!is_dir($targetArg)) {
92
                throw new InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target')));
93
            }
94
        }
95
96
        $bundlesDir = $targetArg.'/bundles/';
97
98
        $io = new SymfonyStyle($input, $output);
99
        $io->newLine();
100
101
        if ($input->getOption('relative')) {
102
            $expectedMethod = self::METHOD_RELATIVE_SYMLINK;
103
            $io->text('Trying to install deprecated SonataCoreBundle assets from SonataAdminBundle as <info>relative symbolic links</info>.');
104
        } elseif ($input->getOption('symlink')) {
105
            $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
106
            $io->text('Trying to install deprecated SonataCoreBundle assets from SonataAdminBundle as <info>absolute symbolic links</info>.');
107
        } else {
108
            $expectedMethod = self::METHOD_COPY;
109
            $io->text('Installing deprecated SonataCoreBundle assets from SonataAdminBundle as <info>hard copies</info>.');
110
        }
111
112
        $io->newLine();
113
114
        $copyUsed = false;
115
        $exitCode = 0;
116
        $validAssetDirs = [];
117
118
        $bundle = $kernel->getBundle('SonataAdminBundle');
119
        $originDir = $bundle->getPath().'/Resources/private/SonataCoreBundleAssets';
120
121
        $assetDir = preg_replace('/bundle$/', '', 'sonatacore');
122
        $targetDir = $bundlesDir.$assetDir;
123
        $validAssetDirs[] = $assetDir;
124
125
        if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
126
            $message = sprintf("%s\n-> %s", $bundle->getName(), $targetDir);
127
        } else {
128
            $message = $bundle->getName();
129
        }
130
131
        try {
132
            $this->filesystem->remove($targetDir);
133
134
            if (self::METHOD_RELATIVE_SYMLINK === $expectedMethod) {
135
                $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
136
            } elseif (self::METHOD_ABSOLUTE_SYMLINK === $expectedMethod) {
137
                $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
138
            } else {
139
                $method = $this->hardCopy($originDir, $targetDir);
140
            }
141
142
            if (self::METHOD_COPY === $method) {
143
                $copyUsed = true;
144
            }
145
146
            if ($method === $expectedMethod) {
147
                $ioMethod = 'success';
148
            } else {
149
                $ioMethod = 'warning';
150
            }
151
        } catch (\Exception $e) {
152
            $exitCode = 1;
153
            $ioMethod = 'error';
154
        }
155
156
        if (0 !== $exitCode) {
157
            $io->error('Some errors occurred while installing assets.');
158
        } else {
159
            if ($copyUsed) {
160
                $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
161
            }
162
163
            switch ($ioMethod) {
164
                case 'success':
165
                case 'warning':$io->$ioMethod('All deprecated SonataCoreBundle assets from SonataAdminBundle were successfully installed.'); break;
166
                case 'error':
167
                default: $io->$ioMethod('No deprecated SonataCoreBundle assets from SonataAdminBundle were provided by any bundle.'); break;
168
            }
169
        }
170
171
        return $exitCode;
172
    }
173
174
    /**
175
     * Try to create relative symlink.
176
     *
177
     * Falling back to absolute symlink and finally hard copy.
178
     */
179
    private function relativeSymlinkWithFallback(string $originDir, string $targetDir): string
180
    {
181
        try {
182
            $this->symlink($originDir, $targetDir, true);
183
            $method = self::METHOD_RELATIVE_SYMLINK;
184
        } catch (IOException $e) {
185
            $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
186
        }
187
188
        return $method;
189
    }
190
191
    /**
192
     * Try to create absolute symlink.
193
     *
194
     * Falling back to hard copy.
195
     */
196
    private function absoluteSymlinkWithFallback(string $originDir, string $targetDir): string
197
    {
198
        try {
199
            $this->symlink($originDir, $targetDir);
200
            $method = self::METHOD_ABSOLUTE_SYMLINK;
201
        } catch (IOException $e) {
202
            // fall back to copy
203
            $method = $this->hardCopy($originDir, $targetDir);
204
        }
205
206
        return $method;
207
    }
208
209
    /**
210
     * Creates symbolic link.
211
     *
212
     * @throws IOException if link can not be created
213
     */
214
    private function symlink(string $originDir, string $targetDir, bool $relative = false)
215
    {
216
        if ($relative) {
217
            $this->filesystem->mkdir(\dirname($targetDir));
218
            $originDir = $this->filesystem->makePathRelative($originDir, realpath(\dirname($targetDir)));
219
        }
220
        $this->filesystem->symlink($originDir, $targetDir);
221
        if (!file_exists($targetDir)) {
222
            throw new IOException(sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir), 0, null, $targetDir);
223
        }
224
    }
225
226
    /**
227
     * Copies origin to target.
228
     */
229
    private function hardCopy(string $originDir, string $targetDir): string
230
    {
231
        $this->filesystem->mkdir($targetDir, 0777);
232
        // We use a custom iterator to ignore VCS files
233
        $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
234
235
        return self::METHOD_COPY;
236
    }
237
238
    private function getPublicDirectory(ContainerInterface $container): string
239
    {
240
        $defaultPublicDir = 'public';
241
242
        if (null === $this->projectDir && !$container->hasParameter('kernel.project_dir')) {
243
            return $defaultPublicDir;
244
        }
245
246
        $composerFilePath = ($this->projectDir ?? $container->getParameter('kernel.project_dir')).'/composer.json';
247
248
        if (!file_exists($composerFilePath)) {
249
            return $defaultPublicDir;
250
        }
251
252
        $composerConfig = json_decode(file_get_contents($composerFilePath), true);
253
254
        if (isset($composerConfig['extra']['public-dir'])) {
255
            return $composerConfig['extra']['public-dir'];
256
        }
257
258
        return $defaultPublicDir;
259
    }
260
}
261