Passed
Push — v4 ( b636d0...2d2527 )
by Andrew
39:55 queued 27:10
created

ImageTransform::assetFromAssetOrIdOrQuery()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
dl 0
loc 32
ccs 0
cts 18
cp 0
rs 8.8333
c 2
b 0
f 0
cc 7
nc 7
nop 2
crap 56
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use Craft;
15
use craft\elements\Asset;
16
use craft\elements\db\ElementQuery;
17
use craft\elements\ElementCollection;
18
use craft\fs\Local;
19
use craft\helpers\StringHelper;
20
use craft\models\ImageTransform as ImageTransformModel;
21
use Exception;
22
use nystudio107\seomatic\helpers\Environment as EnvironmentHelper;
23
use nystudio107\seomatic\Seomatic;
24
use yii\base\InvalidConfigException;
25
use function in_array;
26
use function is_array;
27
28
/**
29
 * @author    nystudio107
30
 * @package   Seomatic
31
 * @since     3.0.0
32
 */
33
class ImageTransform
34
{
35
    // Constants
36
    // =========================================================================
37
38
    public const SOCIAL_TRANSFORM_QUALITY = 82;
39
40
    public const ALLOWED_SOCIAL_MIME_TYPES = [
41
        'image/jpeg',
42
        'image/png',
43
    ];
44
45
    public const DEFAULT_SOCIAL_FORMAT = 'jpg';
46
47
    // Static Public Properties
48
    // =========================================================================
49
50
    /**
51
     * @var bool
52
     */
53
    static public $pendingImageTransforms = false;
54
55
    // Static Private Properties
56
    // =========================================================================
57
58
    static private $transforms = [
59
        'base' => [
60
            'format' => null,
61
            'quality' => self::SOCIAL_TRANSFORM_QUALITY,
62
            'width' => 1200,
63
            'height' => 630,
64
            'mode' => 'crop',
65
        ],
66
        'facebook' => [
67
            'width' => 1200,
68
            'height' => 630,
69
        ],
70
        'twitter-summary' => [
71
            'width' => 800,
72
            'height' => 800,
73
        ],
74
        'twitter-large' => [
75
            'width' => 800,
76
            'height' => 418,
77
        ],
78
        'schema-logo' => [
79
            'format' => 'png',
80
            'width' => 600,
81
            'height' => 60,
82
            'mode' => 'fit',
83
        ],
84
    ];
85
86
    static private $cachedAssetsElements = [];
87
88
    // Static Methods
89
    // =========================================================================
90
91
    /**
92
     * Transform the $asset for social media sites in $transformName and
93
     * optional $siteId
94
     *
95
     * @param int|Asset $asset the Asset or Asset ID
96
     * @param string $transformName the name of the transform to apply
97
     * @param int|null $siteId
98
     * @param string $transformMode
99
     *
100
     * @return string URL to the transformed image
101
     */
102
    public static function socialTransform(
103
        $asset,
104
        $transformName = '',
105
        $siteId = null,
106
        $transformMode = null
107
    ): string
108
    {
109
        $url = '';
110
        $transform = self::createSocialTransform($transformName);
111
        // Let them override the mode
112
        if (empty($transformMode)) {
113
            $transformMode = $transform->mode ?? 'crop';
114
        }
115
        if ($transform !== null) {
116
            $transform->mode = $transformMode ?? $transform->mode;
117
        }
118
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
119
        if (($asset !== null) && ($asset instanceof Asset)) {
120
            // Make sure the format is an allowed format, otherwise explicitly change it
121
            $mimeType = $asset->getMimeType();
122
            if (!in_array($mimeType, self::ALLOWED_SOCIAL_MIME_TYPES, false)) {
123
                $transform->format = self::DEFAULT_SOCIAL_FORMAT;
124
            }
125
            // Generate a transformed image
126
            $assets = Craft::$app->getAssets();
127
            try {
128
                $volume = $asset->getVolume();
129
            } catch (InvalidConfigException $e) {
130
                $volume = null;
131
            }
132
            // If we're not in local dev, tell it to generate the transform immediately so that
133
            // urls like `actions/assets/generate-transform` don't get cached
134
            $generateNow = Seomatic::$environment === EnvironmentHelper::SEOMATIC_DEV_ENV ? null : true;
135
            if ($volume instanceof Local) {
136
                // Preflight to ensure that the source asset actually exists to avoid Craft hanging
137
                if (!$volume->fileExists($asset->getPath())) {
138
                    $generateNow = false;
139
                }
140
            } else {
141
                // If this is not a local volume, avoid a potentially long round-trip by
142
                // being paranoid, and defaulting to not generating the image now
143
                // if we're in local dev
144
                if (Seomatic::$environment === EnvironmentHelper::SEOMATIC_DEV_ENV) {
145
                    $generateNow = false;
146
                }
147
            }
148
            try {
149
                $url = $assets->getAssetUrl($asset, $transform, $generateNow);
150
            } catch (Exception $e) {
151
                $url = $asset->getUrl();
152
            }
153
            if ($url === null) {
154
                $url = '';
155
            }
156
            // If we have a url, add an `mtime` param to cache bust
157
            if (!empty($url) && empty(parse_url($url, PHP_URL_QUERY))) {
158
                $now = new \DateTime();
159
                $url = UrlHelper::url($url, [
160
                    'mtime' => $asset->dateModified->getTimestamp() ?? $now->getTimestamp(),
0 ignored issues
show
Bug introduced by
The method getTimestamp() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

160
                    'mtime' => $asset->dateModified->/** @scrutinizer ignore-call */ getTimestamp() ?? $now->getTimestamp(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
161
                ]);
162
            }
163
        }
164
        // Check to see if the $url contains a pending image transform
165
        if (!empty($url) && StringHelper::contains($url, 'assets/generate-transform')) {
166
            self::$pendingImageTransforms = true;
167
        }
168
169
        return $url;
170
    }
171
172
    /**
173
     * @param int|Asset $asset the Asset or Asset ID
174
     * @param string $transformName the name of the transform to apply
175
     * @param int|null $siteId
176
     * @param string $transformMode
177
     *
178
     * @return string width of the transformed image
179
     */
180
    public static function socialTransformWidth(
181
        $asset,
182
        $transformName = '',
183
        $siteId = null,
184
        $transformMode = null
185
    ): string
186
    {
187
        $width = '';
188
        $transform = self::createSocialTransform($transformName);
189
        // Let them override the mode
190
        if ($transform !== null) {
191
            $transform->mode = $transformMode ?? $transform->mode;
192
        }
193
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
194
        if (($asset !== null) && ($asset instanceof Asset)) {
195
            $width = (string)$asset->getWidth($transform);
196
            if ($width === null) {
0 ignored issues
show
introduced by
The condition $width === null is always false.
Loading history...
197
                $width = '';
198
            }
199
        }
200
201
        return $width;
202
    }
203
204
    /**
205
     * @param int|Asset $asset the Asset or Asset ID
206
     * @param string $transformName the name of the transform to apply
207
     * @param int|null $siteId
208
     * @param string $transformMode
209
     *
210
     * @return string width of the transformed image
211
     */
212
    public static function socialTransformHeight(
213
        $asset,
214
        $transformName = '',
215
        $siteId = null,
216
        $transformMode = null
217
    ): string
218
    {
219
        $height = '';
220
        $transform = self::createSocialTransform($transformName);
221
        // Let them override the mode
222
        if ($transform !== null) {
223
            $transform->mode = $transformMode ?? $transform->mode;
224
        }
225
        $asset = self::assetFromAssetOrIdOrQuery($asset, $siteId);
226
        if (($asset !== null) && ($asset instanceof Asset)) {
227
            $height = (string)$asset->getHeight($transform);
228
            if ($height === null) {
0 ignored issues
show
introduced by
The condition $height === null is always false.
Loading history...
229
                $height = '';
230
            }
231
        }
232
233
        return $height;
234
    }
235
236
    /**
237
     * Return an array of Asset elements from an array of element IDs
238
     *
239
     * @param array|string $assetIds
240
     * @param int|null $siteId
241
     *
242
     * @return array
243
     */
244
    public static function assetElementsFromIds($assetIds, $siteId = null): array
245
    {
246
        $elements = Craft::$app->getElements();
247
        $assets = [];
248
        if (!empty($assetIds)) {
249
            if (is_array($assetIds)) {
250
                foreach ($assetIds as $assetId) {
251
                    if (!empty($assetId)) {
252
                        $assets[] = $elements->getElementById((int)$assetId, Asset::class, $siteId);
253
                    }
254
                }
255
            } else {
256
                $assetId = $assetIds;
257
                if (!empty($assetId)) {
258
                    $assets[] = $elements->getElementById((int)$assetId, Asset::class, $siteId);
259
                }
260
            }
261
        }
262
263
        return $assets;
264
    }
265
266
    // Protected Static Methods
267
    // =========================================================================
268
269
    /**
270
     * Return an asset from either an id or an asset
271
     *
272
     * @param int|array|ElementCollection|Asset|ElementQuery $asset the Asset or Asset ID or ElementQuery
273
     * @param int|null $siteId
274
     *
275
     * @return Asset|null
276
     */
277
    protected static function assetFromAssetOrIdOrQuery($asset, $siteId = null)
278
    {
279
        if (empty($asset)) {
280
            return null;
281
        }
282
        // If it's an array (eager loaded Element query), return the first element
283
        if (is_array($asset)) {
284
            return reset($asset);
285
        }
286
        // If it's an asset already, just return it
287
        if ($asset instanceof Asset) {
288
            return $asset;
289
        }
290
        // If it is a Collection, resolve that to an asset
291
        if ($asset instanceof ElementCollection) {
292
            return $asset->first();
293
        }
294
        // If it is an ElementQuery, resolve that to an asset
295
        if ($asset instanceof ElementQuery) {
0 ignored issues
show
introduced by
$asset is never a sub-type of craft\elements\db\ElementQuery.
Loading history...
296
            return $asset->one();
297
        }
298
299
        $resolvedAssetId = (int)$asset;
300
        $resolvedSiteId = $siteId ?? 0;
301
        if (isset(self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId])) {
302
            return self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId];
303
        }
304
305
        $asset = Craft::$app->getAssets()->getAssetById($asset, $siteId);
306
        self::$cachedAssetsElements[$resolvedAssetId][$resolvedSiteId] = $asset;
307
308
        return $asset;
309
    }
310
311
    /**
312
     * Create a transform from the passed in $transformName
313
     *
314
     * @param string $transformName the name of the transform to apply
315
     *
316
     * @return ImageTransformModel|null
317
     */
318
    protected static function createSocialTransform($transformName = 'base')
319
    {
320
        $transform = null;
321
        if (!empty($transformName)) {
322
            $config = array_merge(
323
                self::$transforms['base'],
324
                self::$transforms[$transformName] ?? self::$transforms['base']
325
            );
326
            $transform = new ImageTransformModel($config);
327
        }
328
329
        return $transform;
330
    }
331
}
332