Completed
Push — master ( bee941...29f099 )
by Łukasz
29:47
created

ResizeOriginalImagesCommand::resize()   A

Complexity

Conditions 4
Paths 19

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 19
nop 4
dl 0
loc 46
rs 9.1781
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Bundle\EzPublishCoreBundle\Command;
10
11
use eZ\Publish\API\Repository\ContentService;
12
use eZ\Publish\API\Repository\ContentTypeService;
13
use eZ\Publish\API\Repository\PermissionResolver;
14
use eZ\Publish\API\Repository\SearchService;
15
use eZ\Publish\API\Repository\UserService;
16
use eZ\Publish\API\Repository\Values\Content\Query;
17
use eZ\Publish\API\Repository\Values\Content\Search\SearchHit;
18
use eZ\Publish\Core\FieldType\Image\Value;
19
use eZ\Publish\Core\IO\IOServiceInterface;
20
use eZ\Publish\Core\IO\Values\BinaryFile;
21
use Imagine\Image\ImagineInterface;
22
use Liip\ImagineBundle\Binary\BinaryInterface;
23
use Liip\ImagineBundle\Exception\Imagine\Filter\NonExistingFilterException;
24
use Liip\ImagineBundle\Imagine\Filter\FilterManager;
25
use Liip\ImagineBundle\Model\Binary;
26
use Symfony\Component\Console\Command\Command;
27
use Symfony\Component\Console\Helper\ProgressBar;
28
use Symfony\Component\Console\Input\InputArgument;
29
use Symfony\Component\Console\Input\InputInterface;
30
use Symfony\Component\Console\Input\InputOption;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Question\ConfirmationQuestion;
33
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesserInterface;
34
use Exception;
35
36
/**
37
 * This command resizes original images stored in ezimage FieldType in given ContentType using the selected filter.
38
 */
39
class ResizeOriginalImagesCommand extends Command
40
{
41
    const DEFAULT_ITERATION_COUNT = 25;
42
    const DEFAULT_REPOSITORY_USER = 'admin';
43
44
    /**
45
     * @var \eZ\Publish\API\Repository\PermissionResolver
46
     */
47
    private $permissionResolver;
48
49
    /**
50
     * @var \eZ\Publish\API\Repository\UserService
51
     */
52
    private $userService;
53
54
    /**
55
     * @var \eZ\Publish\API\Repository\ContentTypeService
56
     */
57
    private $contentTypeService;
58
59
    /**
60
     * @var \eZ\Publish\API\Repository\ContentService
61
     */
62
    private $contentService;
63
64
    /**
65
     * @var \eZ\Publish\API\Repository\SearchService
66
     */
67
    private $searchService;
68
69
    /**
70
     * @var \Liip\ImagineBundle\Imagine\Filter\FilterManager
71
     */
72
    private $filterManager;
73
74
    /**
75
     * @var \eZ\Publish\Core\IO\IOServiceInterface
76
     */
77
    private $ioService;
78
79
    /**
80
     * @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesserInterface
81
     */
82
    private $extensionGuesser;
83
84
    /**
85
     * @var \Imagine\Image\ImagineInterface
86
     */
87
    private $imagine;
88
89
    public function __construct(
90
        PermissionResolver $permissionResolver,
91
        UserService $userService,
92
        ContentTypeService $contentTypeService,
93
        ContentService $contentService,
94
        SearchService $searchService,
95
        FilterManager $filterManager,
96
        IOServiceInterface $ioService,
97
        ExtensionGuesserInterface $extensionGuesser,
98
        ImagineInterface $imagine
99
    ) {
100
        $this->permissionResolver = $permissionResolver;
101
        $this->userService = $userService;
102
        $this->contentTypeService = $contentTypeService;
103
        $this->contentService = $contentService;
104
        $this->searchService = $searchService;
105
        $this->filterManager = $filterManager;
106
        $this->ioService = $ioService;
107
        $this->extensionGuesser = $extensionGuesser;
108
        $this->imagine = $imagine;
109
110
        parent::__construct();
111
    }
112
113
    protected function initialize(InputInterface $input, OutputInterface $output)
114
    {
115
        parent::initialize($input, $output);
116
117
        $this->permissionResolver->setCurrentUserReference(
118
            $this->userService->loadUserByLogin($input->getOption('user'))
119
        );
120
    }
121
122
    protected function configure()
123
    {
124
        $this->setName('ezplatform:images:resize-original')->setDefinition(
125
            [
126
                new InputArgument('contentTypeIdentifier', InputArgument::REQUIRED,
127
                    'Indentifier of ContentType which has ezimage FieldType.'),
128
                new InputArgument('imageFieldIdentifier', InputArgument::REQUIRED,
129
                    'Identifier of field of ezimage type.'),
130
            ]
131
        )
132
        ->addOption(
133
                'filter',
134
                'f',
135
                InputOption::VALUE_REQUIRED,
136
                'Filter which will be used for original images.'
137
        )
138
        ->addOption(
139
            'iteration-count',
140
            'i',
141
            InputOption::VALUE_OPTIONAL,
142
            'Iteration count. Number of images to be recreated in a single iteration, for avoiding using too much memory.',
143
            self::DEFAULT_ITERATION_COUNT
144
        )
145
        ->addOption(
146
            'user',
147
            'u',
148
            InputOption::VALUE_OPTIONAL,
149
            'eZ Platform username (with Role containing at least Content policies: read, versionread, edit, publish)',
150
            self::DEFAULT_REPOSITORY_USER
151
        );
152
    }
153
154
    protected function execute(InputInterface $input, OutputInterface $output)
155
    {
156
        $contentTypeIdentifier = $input->getArgument('contentTypeIdentifier');
157
        $imageFieldIdentifier = $input->getArgument('imageFieldIdentifier');
158
        $filter = $input->getOption('filter');
159
        $iterationCount = (int)$input->getOption('iteration-count');
160
161
        $contentType = $this->contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier);
162
        $fieldType = $contentType->getFieldDefinition($imageFieldIdentifier);
163
        if (!$fieldType || $fieldType->fieldTypeIdentifier !== 'ezimage') {
164
            $output->writeln(
165
                sprintf(
166
                    "<error>FieldType of identifier '%s' of ContentType '%s' has to be 'ezimage', '%s' given.</error>",
167
                    $imageFieldIdentifier,
168
                    $contentType->identifier,
169
                    $fieldType ? $fieldType->fieldTypeIdentifier : ''
170
                )
171
            );
172
173
            return;
174
        }
175
176
        try {
177
            $this->filterManager->getFilterConfiguration()->get($filter);
178
        } catch (NonExistingFilterException $e) {
179
            $output->writeln(
180
                sprintf(
181
                    '<error>%s</error>',
182
                    $e->getMessage()
183
                )
184
            );
185
186
            return;
187
        }
188
189
        $query = new Query();
190
        $query->filter = new Query\Criterion\ContentTypeIdentifier($contentType->identifier);
191
192
        $totalCount = $this->searchService->findContent($query)->totalCount;
193
        $query->limit = $iterationCount;
194
195
        if ($totalCount > 0) {
196
            $output->writeln(
197
                sprintf(
198
                    '<info>Found %d images matching given criteria.</info>',
199
                    $totalCount
200
                )
201
            );
202
        } else {
203
            $output->writeln(
204
                sprintf(
205
                    '<info>No images matching given criteria (ContentType: %s, FieldType %s) found. Exiting.</info>',
206
                    $contentTypeIdentifier,
207
                    $imageFieldIdentifier
208
                )
209
            );
210
211
            return;
212
        }
213
214
        $helper = $this->getHelper('question');
215
        $question = new ConfirmationQuestion('<question>The changes you are going to perform cannot be undone. Please remember to do a proper backup before. Would you like to continue?</question> ', false);
216
        if (!$helper->ask($input, $output, $question)) {
217
            return;
218
        }
219
220
        $progressBar = new ProgressBar($output, $totalCount);
221
        $progressBar->start();
222
223
        while ($query->offset <= $totalCount) {
224
            $results = $this->searchService->findContent($query);
225
226
            /** @var \eZ\Publish\API\Repository\Values\Content\Search\SearchHit $hit */
227
            foreach ($results->searchHits as $hit) {
228
                $this->resize($output, $hit, $imageFieldIdentifier, $filter);
229
                $progressBar->advance();
230
            }
231
232
            $query->offset += $iterationCount;
233
        }
234
235
        $progressBar->finish();
236
        $output->writeln('');
237
        $output->writeln(
238
            sprintf(
239
                "<info>All images have been successfully resized using '%s' filter.</info>",
240
                $filter
241
            )
242
        );
243
    }
244
245
    /**
246
     * @param \Symfony\Component\Console\Output\OutputInterface $output
247
     * @param \eZ\Publish\API\Repository\Values\Content\Search\SearchHit $hit
248
     * @param string $imageFieldIdentifier
249
     * @param string $filter
250
     */
251
    private function resize(OutputInterface $output, SearchHit $hit, string $imageFieldIdentifier, string $filter): void
252
    {
253
        try {
254
            /** @var \eZ\Publish\Core\FieldType\Image\Value $field */
255
            foreach ($hit->valueObject->fields[$imageFieldIdentifier] as $language => $field) {
0 ignored issues
show
Documentation introduced by
The property fields does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
256
                if (null === $field->id) {
257
                    continue;
258
                }
259
                $binaryFile = $this->ioService->loadBinaryFile($field->id);
260
                $mimeType = $this->ioService->getMimeType($field->id);
261
                $binary = new Binary(
262
                    $this->ioService->getFileContents($binaryFile),
263
                    $mimeType,
264
                    $this->extensionGuesser->guess($mimeType)
265
                );
266
267
                $resizedImageBinary = $this->filterManager->applyFilter($binary, $filter);
268
                $newBinaryFile = $this->store($resizedImageBinary, $field);
269
                $image = $this->imagine->load($this->ioService->getFileContents($newBinaryFile));
270
                $dimensions = $image->getSize();
271
272
                $contentDraft = $this->contentService->createContentDraft($hit->valueObject->getVersionInfo()->getContentInfo(), $hit->valueObject->getVersionInfo());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class eZ\Publish\API\Repository\Values\ValueObject as the method getVersionInfo() does only exist in the following sub-classes of eZ\Publish\API\Repository\Values\ValueObject: eZ\Publish\API\Repository\Values\Content\Content, eZ\Publish\API\Repository\Values\User\User, eZ\Publish\API\Repository\Values\User\UserGroup, eZ\Publish\Core\REST\Client\Values\Content\Content, eZ\Publish\Core\REST\Client\Values\User\User, eZ\Publish\Core\REST\Client\Values\User\UserGroup, eZ\Publish\Core\Repository\Values\Content\Content, eZ\Publish\Core\Reposito...es\Content\ContentProxy, eZ\Publish\Core\Repository\Values\User\User, eZ\Publish\Core\Repository\Values\User\UserGroup. 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...
273
                $contentUpdateStruct = $this->contentService->newContentUpdateStruct();
274
                $contentUpdateStruct->setField($imageFieldIdentifier, [
275
                    'id' => $field->id,
276
                    'alternativeText' => $field->alternativeText,
277
                    'fileName' => $field->fileName,
278
                    'fileSize' => $newBinaryFile->size,
279
                    'imageId' => $field->imageId,
280
                    'width' => $dimensions->getWidth(),
281
                    'height' => $dimensions->getHeight(),
282
                ]);
283
                $contentDraft = $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct);
284
                $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct);
285
                $this->contentService->publishVersion($contentDraft->versionInfo);
286
            }
287
        } catch (Exception $e) {
288
            $output->writeln(
289
                sprintf(
290
                    '<error>Can not resize image ID: %s, error message: %s.</error>',
291
                    $field->imageId,
0 ignored issues
show
Bug introduced by
The variable $field seems to be defined by a foreach iteration on line 255. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
292
                    $e->getMessage()
293
                )
294
            );
295
        }
296
    }
297
298
    /**
299
     * Copy of eZ\Bundle\EzPublishCoreBundle\Imagine\IORepositoryResolver::store()
300
     * Original one cannot be used since original method uses eZ\Bundle\EzPublishCoreBundle\Imagine\IORepositoryResolver::getFilePath()
301
     * so ends-up with image stored in _aliases instead of overwritten original image.
302
     *
303
     * @param \Liip\ImagineBundle\Binary\BinaryInterface $binary
304
     * @param \eZ\Publish\Core\FieldType\Image\Value $image
305
     *
306
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
307
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
308
     *
309
     * @return \eZ\Publish\Core\IO\Values\BinaryFile
310
     */
311
    private function store(BinaryInterface $binary, Value $image): BinaryFile
312
    {
313
        $tmpFile = tmpfile();
314
        fwrite($tmpFile, $binary->getContent());
315
        $tmpMetadata = stream_get_meta_data($tmpFile);
316
        $binaryCreateStruct = $this->ioService->newBinaryCreateStructFromLocalFile($tmpMetadata['uri']);
317
        $binaryCreateStruct->id = $image->id;
318
        $newBinaryFile = $this->ioService->createBinaryFile($binaryCreateStruct);
319
        fclose($tmpFile);
320
321
        return $newBinaryFile;
322
    }
323
}
324