Passed
Push — master ( 2d5d29...0740c8 )
by ReliQ
04:50 queued 13s
created

Product::loadMeta()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 32
nop 1
dl 0
loc 29
rs 9.0444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ReliqArts\Docweaver\Model;
6
7
use Carbon\Carbon;
8
use Illuminate\Contracts\Support\Arrayable;
9
use Illuminate\Contracts\Support\Jsonable;
10
use Illuminate\Support\Str;
11
use ReliqArts\Docweaver\Contract\ConfigProvider;
12
use ReliqArts\Docweaver\Contract\Exception;
13
use ReliqArts\Docweaver\Contract\Filesystem;
14
use ReliqArts\Docweaver\Exception\ParsingFailed;
15
use ReliqArts\Docweaver\Exception\Product\AssetPublicationFailed;
16
use ReliqArts\Docweaver\Exception\Product\InvalidAssetDirectory;
17
use Symfony\Component\Yaml\Exception\ParseException;
18
use Symfony\Component\Yaml\Yaml;
19
20
/**
21
 * A documented product.
22
 */
23
class Product implements Arrayable, Jsonable
24
{
25
    public const VERSION_MASTER = 'master';
26
    public const VERSION_UNKNOWN = 'unknown';
27
28
    private const ASSET_URL_PLACEHOLDER_1 = '{{docs}}';
29
    private const ASSET_URL_PLACEHOLDER_2 = '{{doc}}';
30
    private const ASSET_URL_PLACEHOLDERS = [
31
        self::ASSET_URL_PLACEHOLDER_1,
32
        self::ASSET_URL_PLACEHOLDER_2,
33
    ];
34
    private const META_FILE = '.docweaver.yml';
35
36
    /**
37
     * Product key.
38
     *
39
     * @var string
40
     */
41
    private string $key;
42
43
    /**
44
     * Filesystem.
45
     *
46
     * @var Filesystem
47
     */
48
    private Filesystem $filesystem;
49
50
    /**
51
     * @var ConfigProvider
52
     */
53
    private ConfigProvider $configProvider;
54
55
    /**
56
     * Last time product was modified (timestamp).
57
     *
58
     * @var int
59
     */
60
    private int $lastModified = 0;
61
62
    /**
63
     * Product name.
64
     *
65
     * @var string
66
     */
67
    private string $name;
68
69
    /**
70
     * Product description.
71
     *
72
     * @var string
73
     */
74
    private string $description = '';
75
76
    /**
77
     * Product image url.
78
     *
79
     * @var string
80
     */
81
    private string $imageUrl = '';
82
83
    /**
84
     * Product meta (from file).
85
     *
86
     * @var array
87
     */
88
    private array $meta = [];
89
90
    /**
91
     * List of available product versions.
92
     *
93
     * @var array
94
     */
95
    private array $versions = [];
96
97
    /**
98
     * Product resource directory.
99
     *
100
     * @var string
101
     */
102
    private string $directory;
103
104
    /**
105
     * Create product instance.
106
     */
107
    public function __construct(Filesystem $filesystem, ConfigProvider $configProvider, string $directory)
108
    {
109
        $this->filesystem = $filesystem;
110
        $this->configProvider = $configProvider;
111
        $this->name = Str::title(basename($directory));
112
        $this->key = strtolower($this->name);
113
        $this->directory = $directory;
114
    }
115
116
    /**
117
     * Populate product.
118
     *
119
     * @throws Exception if meta file could not be parsed
120
     */
121
    public function populate(): void
122
    {
123
        $this->loadVersions();
124
        $this->loadMeta();
125
    }
126
127
    public function getKey(): string
128
    {
129
        return $this->key;
130
    }
131
132
    /**
133
     * Get default version for product.
134
     *
135
     * @param bool $allowWordedDefault whether a worded version should be accepted as default
136
     */
137
    public function getDefaultVersion(bool $allowWordedDefault = false): string
138
    {
139
        $versions = empty($this->versions) ? $this->getVersions() : $this->versions;
140
        $allowWordedDefault = $allowWordedDefault || $this->configProvider->isWordedDefaultVersionAllowed();
141
        $defaultVersion = self::VERSION_UNKNOWN;
142
143
        foreach ($versions as $tag => $ver) {
144
            if (!$allowWordedDefault) {
145
                if (is_numeric($tag)) {
146
                    $defaultVersion = $tag;
147
148
                    break;
149
                }
150
            } else {
151
                $defaultVersion = $tag;
152
153
                break;
154
            }
155
        }
156
157
        return $defaultVersion;
158
    }
159
160
    /**
161
     * Get product directory.
162
     */
163
    public function getDirectory(): string
164
    {
165
        return $this->directory;
166
    }
167
168
    /**
169
     * Get product name.
170
     */
171
    public function getName(): string
172
    {
173
        return $this->name;
174
    }
175
176
    /**
177
     * Get product description.
178
     */
179
    public function getDescription(): string
180
    {
181
        return $this->description;
182
    }
183
184
    /**
185
     * Get product image url.
186
     */
187
    public function getImageUrl(): string
188
    {
189
        return $this->imageUrl;
190
    }
191
192
    /**
193
     * Get the publicly available versions of the product.
194
     */
195
    public function getVersions(): array
196
    {
197
        return $this->versions;
198
    }
199
200
    /**
201
     * Get last modified time.
202
     */
203
    public function getLastModified(): Carbon
204
    {
205
        return Carbon::createFromTimestamp($this->lastModified);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Carbon\Carbon::cr...mp($this->lastModified) could return the type string which is incompatible with the type-hinted return Carbon\Carbon. Consider adding an additional type-check to rule them out.
Loading history...
206
    }
207
208
    /**
209
     * Determine if the given string is a valid version.
210
     */
211
    public function hasVersion(string $version): bool
212
    {
213
        return array_key_exists($version, $this->getVersions());
214
    }
215
216
    /**
217
     * Publish product public assets.
218
     *
219
     * @throws Exception if products asset directory is invalid or assets could not be published
220
     */
221
    public function publishAssets(string $version): void
222
    {
223
        $version = empty($version) ? $this->getDefaultVersion() : $version;
224
        $storagePath = storage_path(
225
            sprintf('app/public/%s/%s/%s', $this->configProvider->getRoutePrefix(), $this->key, $version)
226
        );
227
        $imageDirectory = sprintf('%s/%s/images', $this->directory, $version);
228
229
        if (!$this->filesystem->isDirectory($imageDirectory)) {
230
            throw InvalidAssetDirectory::forDirectory($imageDirectory);
231
        }
232
233
        if (!$this->filesystem->copyDirectory($imageDirectory, sprintf('%s/images', $storagePath))) {
234
            throw AssetPublicationFailed::forProductAssetsOfType($this, 'image');
235
        }
236
    }
237
238
    /**
239
     * Get the instance as an array.
240
     */
241
    public function toArray(): array
242
    {
243
        return [
244
            'key' => $this->key,
245
            'name' => $this->name,
246
            'description' => $this->description,
247
            'imageUrl' => $this->imageUrl,
248
            'directory' => $this->directory,
249
            'versions' => $this->versions,
250
            'defaultVersion' => $this->getDefaultVersion(),
251
            'lastModified' => $this->getLastModified(),
252
        ];
253
    }
254
255
    /**
256
     * Convert the object to its JSON representation.
257
     *
258
     * @param int $options
259
     */
260
    public function toJson($options = 0): string
261
    {
262
        return json_encode($this->toArray(), JSON_THROW_ON_ERROR, 512);
263
    }
264
265
    public function getMasterDirectory(): string
266
    {
267
        return sprintf('%s/%s', $this->getDirectory(), self::VERSION_MASTER);
268
    }
269
270
    /**
271
     * Convert url string to asset url relative to current product.
272
     */
273
    private function getAssetUrl(string $url, string $version): string
274
    {
275
        $url = empty($url) ? self::ASSET_URL_PLACEHOLDER_1 : $url;
276
277
        if (stripos($url, 'http') === 0) {
278
            return $url;
279
        }
280
281
        return asset(
282
            str_replace(
283
                self::ASSET_URL_PLACEHOLDERS,
284
                sprintf('storage/%s/%s/%s', $this->configProvider->getRoutePrefix(), $this->key, $version),
285
                $url
286
            )
287
        );
288
    }
289
290
    /**
291
     * Load meta onto product.
292
     *
293
     * @param string $version Version to load configuration from. (optional)
294
     *
295
     * @throws Exception if meta file could not be parsed
296
     */
297
    private function loadMeta(string $version = null): void
298
    {
299
        $version = empty($version) ? $this->getDefaultVersion() : $version;
300
        $metaFile = realpath(sprintf('%s/%s/%s', $this->directory, $version, self::META_FILE));
301
302
        if (empty($metaFile)) {
303
            return;
304
        }
305
306
        try {
307
            $meta = Yaml::parse(file_get_contents($metaFile));
308
309
            if (!empty($meta['name'])) {
310
                $this->name = $meta['name'];
311
            }
312
            if (!empty($meta['description'])) {
313
                $this->description = $meta['description'];
314
            }
315
316
            $this->imageUrl = $this->getAssetUrl($meta['image_url'] ?? '', $version);
317
            $this->meta = $meta;
318
        } catch (ParseException $exception) {
319
            $message = sprintf(
320
                'Failed to parse meta file `%s`. %s',
321
                $metaFile,
322
                $exception->getMessage()
323
            );
324
325
            throw ParsingFailed::forFile($metaFile)->withMessage($message);
326
        }
327
    }
328
329
    /**
330
     * Load product versions.
331
     */
332
    private function loadVersions(): void
333
    {
334
        $versions = [];
335
336
        if ($this->key) {
337
            $versionDirs = $this->filesystem->directories($this->directory);
338
339
            // add versions to version array
340
            foreach ($versionDirs as $ver) {
341
                $versionTag = basename($ver);
342
                $versionName = Str::title($versionTag);
343
                $versions[$versionTag] = $versionName;
344
            }
345
346
            // update last modified
347
            $this->lastModified = $this->filesystem->lastModified($this->directory);
348
349
            // sort versions
350
            krsort($versions);
351
        }
352
353
        $this->versions = $versions;
354
    }
355
}
356