Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
MakePublicationCommand
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
12 / 12
27
100.00% covered (success)
100.00%
1 / 1
 safeHandle
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getPublicationTypeSelection
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 collectFieldData
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 captureFieldInput
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 captureTextFieldInput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 captureArrayFieldInput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 captureMediaFieldInput
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 handleEmptyMediaFilesCollection
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 captureTagFieldInput
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 captureOtherFieldInput
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 askForFieldData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseCommaSeparatedValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Publications\Commands;
6
7use Hyde\Hyde;
8use InvalidArgumentException;
9use Illuminate\Support\Collection;
10use Hyde\Publications\Publications;
11use LaravelZero\Framework\Commands\Command;
12use Hyde\Publications\Models\PublicationType;
13use Hyde\Publications\Models\PublicationFieldValue;
14use Hyde\Publications\Concerns\PublicationFieldTypes;
15use Hyde\Publications\Actions\CreatesNewPublicationPage;
16use Hyde\Publications\Models\PublicationFieldDefinition;
17use Hyde\Publications\Commands\Helpers\InputStreamHandler;
18
19use function array_map;
20use function array_merge;
21use function explode;
22use function implode;
23use function sprintf;
24use function in_array;
25use function str_starts_with;
26
27/**
28 * Hyde command to create a new publication for a given publication type.
29 *
30 * @see \Hyde\Publications\Actions\CreatesNewPublicationPage
31 * @see \Hyde\Publications\Testing\Feature\MakePublicationCommandTest
32 */
33class MakePublicationCommand extends ValidatingCommand
34{
35    /** @var string */
36    protected $signature = 'make:publication
37        {publicationType? : The name of the publication type to create a publication for}
38        {--force : Should the generated file overwrite existing publications with the same filename?}';
39
40    /** @var string */
41    protected $description = 'Create a new publication item';
42
43    protected PublicationType $publicationType;
44
45    /** @var \Illuminate\Support\Collection<string, \Hyde\Publications\Models\PublicationType> */
46    protected Collection $fieldData;
47
48    public function safeHandle(): int
49    {
50        $this->title('Creating a new publication!');
51
52        $this->publicationType = $this->getPublicationTypeSelection();
53        $this->fieldData = new Collection();
54
55        $this->collectFieldData();
56
57        $creator = new CreatesNewPublicationPage($this->publicationType, $this->fieldData, (bool) $this->option('force'));
58        if ($creator->hasFileConflict()) {
59            $this->error('Error: A publication already exists with the same canonical field value');
60            if ($this->confirm('Do you wish to overwrite the existing file?')) {
61                $creator->force();
62            } else {
63                $this->info('Exiting without overwriting existing publication file!');
64
65                return ValidatingCommand::USER_EXIT;
66            }
67        }
68        $creator->create();
69
70        $this->infoComment("All done! Created file [{$creator->getOutputPath()}]");
71
72        return Command::SUCCESS;
73    }
74
75    protected function getPublicationTypeSelection(): PublicationType
76    {
77        $publicationTypes = Publications::getPublicationTypes();
78        if ($this->argument('publicationType')) {
79            $publicationTypeSelection = $this->argument('publicationType');
80        } else {
81            if ($publicationTypes->isEmpty()) {
82                throw new InvalidArgumentException('Unable to locate any publication types. Did you create any?');
83            }
84
85            $publicationTypeSelection = $this->choice(
86                'Which publication type would you like to create a publication item for?',
87                $publicationTypes->keys()->toArray()
88            );
89        }
90
91        if ($publicationTypes->has($publicationTypeSelection)) {
92            $this->line("<info>Creating a new publication of type</info> [<comment>$publicationTypeSelection</comment>]");
93
94            return $publicationTypes->get($publicationTypeSelection);
95        }
96
97        throw new InvalidArgumentException("Unable to locate publication type [$publicationTypeSelection]");
98    }
99
100    protected function collectFieldData(): void
101    {
102        $this->newLine();
103        $this->info('Now please enter the field data:');
104
105        /** @var \Hyde\Publications\Models\PublicationFieldDefinition $field */
106        foreach ($this->publicationType->getFields() as $field) {
107            if (str_starts_with($field->name, '__')) {
108                continue;
109            }
110
111            $this->newLine();
112            $fieldInput = $this->captureFieldInput($field);
113            if (empty($fieldInput)) {
114                $this->line("<fg=gray> > Skipping field $field->name</>");
115            } else {
116                $this->fieldData->put($field->name, $fieldInput);
117            }
118        }
119
120        $this->newLine();
121    }
122
123    protected function captureFieldInput(PublicationFieldDefinition $field): ?PublicationFieldValue
124    {
125        return match ($field->type) {
126            PublicationFieldTypes::Text => $this->captureTextFieldInput($field),
127            PublicationFieldTypes::Array => $this->captureArrayFieldInput($field),
128            PublicationFieldTypes::Media => $this->captureMediaFieldInput($field),
129            PublicationFieldTypes::Tag => $this->captureTagFieldInput($field),
130            default => $this->captureOtherFieldInput($field),
131        };
132    }
133
134    protected function captureTextFieldInput(PublicationFieldDefinition $field): PublicationFieldValue
135    {
136        $this->infoComment(sprintf("Enter lines for field [$field->name] (%s)", InputStreamHandler::terminationMessage()));
137
138        return new PublicationFieldValue(PublicationFieldTypes::Text, implode("\n", InputStreamHandler::call()));
139    }
140
141    protected function captureArrayFieldInput(PublicationFieldDefinition $field): PublicationFieldValue
142    {
143        $this->infoComment(sprintf("Enter values for field [$field->name] (%s)", InputStreamHandler::terminationMessage()));
144
145        return new PublicationFieldValue(PublicationFieldTypes::Array, InputStreamHandler::call());
146    }
147
148    protected function captureMediaFieldInput(PublicationFieldDefinition $field): ?PublicationFieldValue
149    {
150        $this->infoComment("Select file for image field [$field->name]");
151
152        $mediaFiles = Publications::getMediaForType($this->publicationType);
153        if ($mediaFiles->isEmpty()) {
154            $this->handleEmptyMediaFilesCollection($field);
155
156            return null;
157        }
158
159        return new PublicationFieldValue(PublicationFieldTypes::Media, $this->choice('Which file would you like to use?', $mediaFiles->toArray()));
160    }
161
162    protected function handleEmptyMediaFilesCollection(PublicationFieldDefinition $field): void
163    {
164        $directory = sprintf('directory %s/%s/', Hyde::getMediaDirectory(),
165            $this->publicationType->getIdentifier()
166        );
167
168        if (in_array('required', $field->rules)) {
169            throw new InvalidArgumentException("Unable to create publication as no media files were found in $directory");
170        }
171
172        $this->newLine();
173        $this->warn("<fg=red>Warning:</> No media files found in $directory");
174        if ($this->confirm('Would you like to skip this field?', true)) {
175            // We could have a choice here, where 0 skips, and 1 reloads the media files
176            return;
177        } else {
178            throw new InvalidArgumentException('Unable to locate any media files for this publication type');
179        }
180    }
181
182    protected function captureTagFieldInput(PublicationFieldDefinition $field): ?PublicationFieldValue
183    {
184        $this->infoComment("Select one or more tags for field [$field->name]");
185
186        $existingTags = Publications::getPublicationTags();
187
188        if ($existingTags) {
189            $choice = $this->choice(/** @lang Text */ 'Select from existing or', array_merge([
190                '<comment>Add new tag</comment>',
191            ], $existingTags), 0, multiple: true);
192
193            $addNew = (is_array($choice) ? $choice[0] : $choice) === '<comment>Add new tag</comment>';
194        } else {
195            $addNew = true;
196        }
197
198        if ($addNew) {
199            $choice = $this->askWithCompletion('Enter tag(s) <fg=gray>(multiple tags separated by commas)</>',
200                $existingTags);
201
202            $choice = $this->parseCommaSeparatedValues($choice);
203        }
204
205        return new PublicationFieldValue(PublicationFieldTypes::Tag, $choice);
206    }
207
208    protected function captureOtherFieldInput(PublicationFieldDefinition $field): ?PublicationFieldValue
209    {
210        $selection = $this->askForFieldData($field->name, $field->getRules());
211        if (empty($selection)) {
212            return null;
213        }
214
215        return new PublicationFieldValue($field->type, $selection);
216    }
217
218    protected function askForFieldData(string $name, array $rules): string
219    {
220        return $this->askWithValidation($name, "Enter data for field </>[<comment>$name</comment>]", $rules);
221    }
222
223    protected function parseCommaSeparatedValues(string $choice): array
224    {
225        return array_map('trim', explode(',', $choice));
226    }
227}