Completed
Push — master ( d167e0...3fdc21 )
by ReliQ
08:34
created

Publisher::getProductSource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ReliQArts\Docweaver\Services\Product;
6
7
use ReliQArts\Docweaver\Contracts\Exception;
8
use ReliQArts\Docweaver\Contracts\Filesystem;
9
use ReliQArts\Docweaver\Contracts\Logger;
10
use ReliQArts\Docweaver\Contracts\Product\Publisher as PublisherContract;
11
use ReliQArts\Docweaver\Contracts\VCSCommandRunner;
12
use ReliQArts\Docweaver\Exceptions\Product\InvalidAssetDirectory;
13
use ReliQArts\Docweaver\Exceptions\Product\PublicationFailed;
14
use ReliQArts\Docweaver\Models\Product;
15
use ReliQArts\Docweaver\Services\Publisher as BasePublisher;
16
use ReliQArts\Docweaver\VO\Result;
17
use Symfony\Component\Process\Exception\ProcessFailedException;
18
19
/**
20
 * Publishes and updates product versions.
21
 */
22
final class Publisher extends BasePublisher implements PublisherContract
23
{
24
    /**
25
     * @var VCSCommandRunner
26
     */
27
    private $vcsCommandRunner;
28
29
    /**
30
     * Publisher constructor.
31
     *
32
     * @param Filesystem       $filesystem
33
     * @param Logger           $logger
34
     * @param VCSCommandRunner $vcsCommandRunner
35
     */
36
    public function __construct(Filesystem $filesystem, Logger $logger, VCSCommandRunner $vcsCommandRunner)
37
    {
38
        parent::__construct($filesystem, $logger);
39
40
        $this->vcsCommandRunner = $vcsCommandRunner;
41
    }
42
43
    /**
44
     * @param Product $product
45
     * @param string  $source
46
     *
47
     * @throws Exception
48
     *
49
     * @return Result
50
     */
51
    public function publish(Product $product, string $source): Result
52
    {
53
        $result = new Result();
54
        $versions = [Product::VERSION_MASTER];
55
        $versionsPublished = [];
56
        $versionsUpdated = [];
57
        $productDirectory = $product->getDirectory();
58
        $masterDirectory = $product->getMasterDirectory();
59
60
        $this->setExecutionStartTime();
61
62
        if (!$this->readyResourceDirectory($productDirectory)) {
63
            return $result->setError(sprintf('Product directory %s is not writable.', $productDirectory))
64
                ->setData((object)['executionTime' => $this->getExecutionTime()]);
65
        }
66
67
        if (!$this->filesystem->isDirectory($masterDirectory)) {
68
            $this->publishVersion($product, $source, Product::VERSION_MASTER);
69
            $versionsPublished[] = Product::VERSION_MASTER;
70
        } else {
71
            $this->updateVersion($product, Product::VERSION_MASTER);
72
            $versionsUpdated[] = Product::VERSION_MASTER;
73
        }
74
75
        $tagResult = $this->publishTags($product, $source);
76
        $tagData = $tagResult->getData();
77
        $versions = array_merge($versions, $tagData->tags ?? []);
78
        $versionsPublished = array_merge($versionsPublished, $tagData->tagsPublished ?? []);
79
80
        if ($result->isSuccess()) {
81
            $result = $result->setMessage(sprintf('%s was successfully published.', $product->getName()))
82
                ->setData((object)[
83
                    'versions' => $versions,
84
                    'versionsPublished' => $versionsPublished,
85
                    'versionsUpdated' => $versionsUpdated,
86
                    'executionTime' => $this->getExecutionTime(),
87
                ]);
88
        }
89
90
        return $result;
91
    }
92
93
    /**
94
     * @param Product $product
95
     *
96
     * @return Result
97
     */
98
    public function update(Product $product): Result
99
    {
100
        $result = new Result();
101
        $publishedVersions = array_keys($product->getVersions());
102
        $availableTags = $this->listAvailableProductTags($product);
103
        $source = $this->getProductSource($product);
104
        $branches = array_diff($publishedVersions, $availableTags);
105
        $unpublishedTags = array_diff($availableTags, $publishedVersions);
106
        $versions = array_unique(array_merge($publishedVersions, $availableTags));
107
        $versionsPublished = [];
108
        $versionsUpdated = [];
109
110
        foreach ($branches as $version) {
111
            try {
112
                $this->updateVersion($product, $version);
113
                $versionsUpdated[] = $version;
114
            } catch (Exception $exception) {
115
                $this->logger->info($exception->getMessage(), [$exception]);
116
117
                $result = $result->addMessage($exception->getMessage());
118
            }
119
        }
120
121
        $tagResult = $this->publishTags($product, $source, $unpublishedTags);
122
        $tagData = $tagResult->getData();
123
        $versionsPublished = array_merge($versionsPublished, $tagData->tagsPublished ?? []);
124
125
        if ($result->isSuccess()) {
126
            $result = $result->setMessage(sprintf('%s was successfully updated.', $product->getName()));
127
        }
128
129
        return $result->setData((object)[
130
            'versions' => $versions,
131
            'versionsPublished' => $versionsPublished,
132
            'versionsUpdated' => $versionsUpdated,
133
        ]);
134
    }
135
136
    /**
137
     * @param Product $product
138
     * @param string  $source
139
     * @param string  $version
140
     *
141
     * @throws Exception
142
     *
143
     * @return bool
144
     */
145
    private function publishVersion(Product $product, string $source, string $version): bool
146
    {
147
        try {
148
            $this->vcsCommandRunner->clone($source, $version, $product->getDirectory());
149
        } catch (ProcessFailedException $e) {
150
            throw PublicationFailed::forProductVersion($product, $version);
151
        }
152
153
        $this->publishProductAssets($product, $version);
154
155
        return true;
156
    }
157
158
    /**
159
     * @param Product $product
160
     * @param string  $version
161
     *
162
     * @throws Exception
163
     *
164
     * @return bool
165
     */
166
    private function updateVersion(Product $product, string $version): bool
167
    {
168
        try {
169
            $this->vcsCommandRunner->pull(sprintf('%s/%s', $product->getDirectory(), $version));
170
        } catch (ProcessFailedException $e) {
171
            throw new PublicationFailed(
172
                sprintf(
173
                    'Failed to update version `%s` of product `%s`.',
174
                    $version,
175
                    $product->getName()
176
                )
177
            );
178
        }
179
180
        $this->publishProductAssets($product, $version);
181
182
        return true;
183
    }
184
185
    /**
186
     * @param Product $product
187
     * @param string  $source
188
     * @param array   $tags
189
     *
190
     * @return Result
191
     */
192
    private function publishTags(Product $product, string $source, array $tags = []): Result
193
    {
194
        $result = new Result();
195
        $tagsPublished = [];
196
197
        try {
198
            $tags = empty($tags) ? $this->listAvailableProductTags($product) : $tags;
199
200
            foreach ($tags as $tag) {
201
                $tagDirectory = sprintf('%s/%s', $product->getDirectory(), $tag);
202
203
                if (!$this->filesystem->isDirectory($tagDirectory)) {
204
                    $this->publishVersion($product, $source, $tag);
205
                    $result = $result->addMessage(sprintf('Successfully published tag `%s`.', $tag));
206
                    $tagsPublished[] = $tag;
207
                } else {
208
                    $message = sprintf('Version `%s` already exists.', $tag);
209
                    $result = $result->addMessage($message);
210
                }
211
            }
212
        } catch (Exception $e) {
213
            $errorMessage = $e->getMessage();
214
215
            return $result->setMessage($errorMessage)->setError($errorMessage);
216
        }
217
218
        return $result->setData((object)[
219
            'tags' => $tags,
220
            'tagsPublished' => $tagsPublished,
221
        ]);
222
    }
223
224
    /**
225
     * @param Product $product
226
     * @param string  $version
227
     */
228
    private function publishProductAssets(Product $product, string $version): void
229
    {
230
        try {
231
            $product->publishAssets($version);
232
        } catch (Exception $exception) {
233
            if ($exception instanceof InvalidAssetDirectory) {
234
                $this->logger->info($exception->getMessage());
235
            } else {
236
                $this->logger->error($exception->getMessage());
237
            }
238
        }
239
    }
240
241
    /**
242
     * @param Product $product
243
     *
244
     * @throws ProcessFailedException
245
     *
246
     * @return array
247
     */
248
    private function listAvailableProductTags(Product $product): array
249
    {
250
        return $this->vcsCommandRunner->listTags($product->getMasterDirectory());
251
    }
252
253
    /**
254
     * @param Product $product
255
     *
256
     * @throws ProcessFailedException
257
     *
258
     * @return string
259
     */
260
    private function getProductSource(Product $product): string
261
    {
262
        return $this->vcsCommandRunner->getRemoteUrl($product->getMasterDirectory());
263
    }
264
}
265