1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is a part of Sculpin. |
5
|
|
|
* |
6
|
|
|
* (c) Dragonfly Development Inc. |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Sculpin\Bundle\ContentTypesBundle\Command; |
13
|
|
|
|
14
|
|
|
use Doctrine\Common\Inflector\Inflector; |
15
|
|
|
use Sculpin\Bundle\SculpinBundle\Command\AbstractCommand; |
16
|
|
|
use Sculpin\Bundle\SculpinBundle\Console\Application; |
17
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
18
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
19
|
|
|
use Symfony\Component\Console\Input\InputOption; |
20
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
21
|
|
|
use Symfony\Component\Filesystem\Filesystem; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Helper command to create a new content type. |
25
|
|
|
* |
26
|
|
|
* Outputs the YAML required to add a new content type, and optionally |
27
|
|
|
* generates the associated boilerplate for the type. |
28
|
|
|
*/ |
29
|
|
|
final class ContentCreateCommand extends AbstractCommand |
30
|
|
|
{ |
31
|
|
|
private const DIRECTORY_FLAG = '_directory_'; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* {@inheritdoc} |
35
|
|
|
*/ |
36
|
|
|
protected function configure(): void |
37
|
|
|
{ |
38
|
|
|
$prefix = $this->isStandaloneSculpin() ? '' : 'sculpin:'; |
39
|
|
|
|
40
|
|
|
$this->setName($prefix . 'content:create'); |
41
|
|
|
$this->setDescription('Create a new content type, including boilerplate template files.'); |
42
|
|
|
$this->setDefinition( |
43
|
|
|
[ |
44
|
|
|
new InputArgument( |
45
|
|
|
'type', |
46
|
|
|
InputArgument::REQUIRED, |
47
|
|
|
'Name for this type (e.g., "posts")' |
48
|
|
|
), |
49
|
|
|
new InputOption( |
50
|
|
|
'boilerplate', |
51
|
|
|
'b', |
52
|
|
|
InputOption::VALUE_NONE, |
53
|
|
|
'Enabled by default. Use --dry-run if you do not want to generate the files.' |
54
|
|
|
), |
55
|
|
|
new InputOption( |
56
|
|
|
'dry-run', |
57
|
|
|
'd', |
58
|
|
|
InputOption::VALUE_NONE, |
59
|
|
|
'Don\'t generate boilerplate/placeholder/template files.' |
60
|
|
|
), |
61
|
|
|
new InputOption( |
62
|
|
|
'taxonomy', |
63
|
|
|
't', |
64
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
65
|
|
|
"Organize content by taxonomy categories (\"tags\", \"categories\", \"types\", etc)\n" |
66
|
|
|
. "Add multiple taxonomies by repeating the option." |
67
|
|
|
) |
68
|
|
|
] |
69
|
|
|
); |
70
|
|
|
|
71
|
|
|
$this->setHelp(<<<EOT |
72
|
|
|
The <info>content:create</info> command helps you create a custom content type and the associated boilerplate/templates. |
73
|
|
|
|
74
|
|
|
Example: |
75
|
|
|
|
76
|
|
|
vendor/bin/sculpin content:create docs -t product -t year |
77
|
|
|
|
78
|
|
|
NOTE: This command does not automatically modify the <info>app/config/sculpin_kernel.yml</info> file. You will have to |
79
|
|
|
add the suggested changes yourself. |
80
|
|
|
|
81
|
|
|
EOT |
82
|
|
|
); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritdoc} |
87
|
|
|
*/ |
88
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): void |
89
|
|
|
{ |
90
|
|
|
$pluralType = $input->getArgument('type'); |
91
|
|
|
$singularType = Inflector::singularize($pluralType); |
|
|
|
|
92
|
|
|
$dryRun = $input->getOption('dry-run'); |
93
|
|
|
$taxonomies = $input->getOption('taxonomy'); |
94
|
|
|
|
95
|
|
|
$output->writeln('Generating new content type: <info>' . $pluralType . '</info>'); |
96
|
|
|
|
97
|
|
|
// TODO: Prompt the user with a preview before generating content |
98
|
|
|
$output->writeln($this->getOutputMessage($pluralType, $singularType, $taxonomies)); |
|
|
|
|
99
|
|
|
|
100
|
|
|
// TODO: Write a yaml file to configure the content type (and recommend a wildcard include for types?) |
101
|
|
|
|
102
|
|
|
// grab the boilerplate manifest |
103
|
|
|
$boilerplateManifest = $this->generateBoilerplateManifest($pluralType, $singularType, $taxonomies); |
|
|
|
|
104
|
|
|
|
105
|
|
|
// skip creating boilerplate files if this is a dry run |
106
|
|
|
if ($dryRun) { |
107
|
|
|
$output->writeln("Dry run. Skipping creation of these boilerplate files:"); |
108
|
|
|
|
109
|
|
|
foreach ($boilerplateManifest as $filename => $value) { |
110
|
|
|
$output->writeln("\t<info>" . $filename . '</info>'); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$output->writeln("\nRemember to add the content type definition (displayed above) to sculpin_kernel.yml!"); |
114
|
|
|
|
115
|
|
|
return; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$output->writeln('Generating boilerplate for ' . $pluralType); |
119
|
|
|
$fs = new Filesystem(); |
120
|
|
|
foreach ($boilerplateManifest as $filename => $value) { |
121
|
|
|
// create directory and skip the rest of the loop |
122
|
|
|
if ($value === static::DIRECTORY_FLAG) { |
123
|
|
|
$fs->mkdir($filename); |
124
|
|
|
continue; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
if ($fs->exists($filename)) { |
128
|
|
|
$output->writeln('Warning: ' . $filename . ' already exists at the target location. Skipping.'); |
129
|
|
|
continue; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
// create file $filename with contents $value |
133
|
|
|
$fs->dumpFile($filename, $value); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$output->writeln("\nRemember to add the content type definition (displayed above) to sculpin_kernel.yml!"); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
private function generateBoilerplateManifest(string $plural, string $singular, array $taxonomies = []): array |
140
|
|
|
{ |
141
|
|
|
$app = $this->getApplication(); |
142
|
|
|
if (!$app instanceof Application) { |
143
|
|
|
throw new \RuntimeException('Sculpin Application not found!'); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
$rootDir = \dirname($app->getKernel()->getRootDir()); |
147
|
|
|
$manifest = []; |
148
|
|
|
|
149
|
|
|
// ensure the content type storage folder exists |
150
|
|
|
$storageFolder = $rootDir . '/source/_' . $plural; |
151
|
|
|
$manifest[$storageFolder] = static::DIRECTORY_FLAG; |
152
|
|
|
|
153
|
|
|
// content type index template |
154
|
|
|
$index = $rootDir . '/source/' . $plural . '.html'; |
155
|
|
|
$manifest[$index] = $this->getIndexTemplate($plural, $singular); |
156
|
|
|
|
157
|
|
|
// ensure the views folder exists |
158
|
|
|
$storageFolder = $rootDir . '/source/_views'; |
159
|
|
|
$manifest[$storageFolder] = static::DIRECTORY_FLAG; |
160
|
|
|
|
161
|
|
|
// content type view template |
162
|
|
|
$index = $rootDir . '/source/_views/' . $singular . '.html'; |
163
|
|
|
$manifest[$index] = $this->getViewTemplate($plural, $taxonomies); |
164
|
|
|
|
165
|
|
|
foreach ($taxonomies as $taxonomy) { |
166
|
|
|
$singularTaxonomy = Inflector::singularize($taxonomy); |
167
|
|
|
// content taxonomy index template |
168
|
|
|
$index = $rootDir . '/source/' . $plural . '/' . $taxonomy . '.html'; |
169
|
|
|
$manifest[$index] = $this->getTaxonomyIndexTemplate($plural, $taxonomy, $singularTaxonomy); |
170
|
|
|
|
171
|
|
|
// content taxonomy directory |
172
|
|
|
$storageFolder = $rootDir . '/source/' . $plural . '/' . $taxonomy; |
173
|
|
|
$manifest[$storageFolder] = static::DIRECTORY_FLAG; |
174
|
|
|
|
175
|
|
|
// content taxonomy view template(s) |
176
|
|
|
$index = $rootDir . '/source/' . $plural . '/' . $taxonomy . '/' . $singularTaxonomy . '.html'; |
177
|
|
|
$manifest[$index] = $this->getTaxonomyViewTemplate($plural, $singular, $singularTaxonomy); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
return $manifest; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
private function getOutputMessage(string $type, string $singularType, array $taxonomies = []): string |
184
|
|
|
{ |
185
|
|
|
$outputMessage = <<<EOT |
186
|
|
|
|
187
|
|
|
YAML content type definition you will have to |
188
|
|
|
add to <info>app/config/sculpin_kernel.yml</info>: |
189
|
|
|
================START OF YAML================ |
190
|
|
|
|
191
|
|
|
sculpin_content_types: |
192
|
|
|
${type}: |
193
|
|
|
type: path |
194
|
|
|
path: _${type} |
195
|
|
|
singular_name: ${singularType} |
196
|
|
|
layout: ${singularType} |
197
|
|
|
enabled: true |
198
|
|
|
permalink: ${type}/:title |
199
|
|
|
EOT; |
200
|
|
|
if ($taxonomies) { |
|
|
|
|
201
|
|
|
$outputMessage .= "\n taxonomies:\n"; |
202
|
|
|
foreach ($taxonomies as $taxonomy) { |
203
|
|
|
$outputMessage .= " - ${taxonomy}\n"; |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$outputMessage .= "\n=================END OF YAML=================\n\n"; |
208
|
|
|
|
209
|
|
|
return $outputMessage; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
private function getIndexTemplate(string $plural, string $singular) |
213
|
|
|
{ |
214
|
|
|
$title = ucfirst($plural); |
215
|
|
|
|
216
|
|
|
return <<<EOT |
217
|
|
|
--- |
218
|
|
|
layout: default |
219
|
|
|
title: $title |
220
|
|
|
generator: pagination |
221
|
|
|
pagination: |
222
|
|
|
provider: data.$plural |
223
|
|
|
max_per_page: 10 |
224
|
|
|
use: [$plural] |
225
|
|
|
--- |
226
|
|
|
<ul> |
227
|
|
|
{% for $singular in page.pagination.items %} |
228
|
|
|
<li><a href="{{ $singular.url }}">{{ $singular.title }}</a></li> |
229
|
|
|
{% endfor %} |
230
|
|
|
</ul> |
231
|
|
|
|
232
|
|
|
<nav> |
233
|
|
|
{% if page.pagination.previous_page or page.pagination.next_page %} |
234
|
|
|
{% if page.pagination.previous_page %} |
235
|
|
|
<a href="{{ site.url }}{{ page.pagination.previous_page.url }}">Newer ${title}</a> |
236
|
|
|
{% endif %} |
237
|
|
|
{% if page.pagination.next_page %} |
238
|
|
|
<a href="{{ site.url }}{{ page.pagination.next_page.url }}">Older ${title}</a> |
239
|
|
|
{% endif %} |
240
|
|
|
{% endif %} |
241
|
|
|
</nav> |
242
|
|
|
EOT; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
private function getViewTemplate(string $plural, array $taxonomies = []): string |
246
|
|
|
{ |
247
|
|
|
$output = <<<EOT |
248
|
|
|
{% extends 'default' %} |
249
|
|
|
|
250
|
|
|
{% block content_wrapper %} |
251
|
|
|
<article> |
252
|
|
|
<header> |
253
|
|
|
<h2>{{ page.title }}</h2> |
254
|
|
|
{% if page.subtitle %} |
255
|
|
|
<h3 class="subtitle">{{ page.subtitle }}</h3> |
256
|
|
|
{% endif %} |
257
|
|
|
</header> |
258
|
|
|
<section class="main_body"> |
259
|
|
|
{{ page.blocks.content|raw }} |
260
|
|
|
</section> |
261
|
|
|
EOT; |
262
|
|
|
|
263
|
|
|
if ($taxonomies) { |
|
|
|
|
264
|
|
|
$output .= "\n" . ' <section class="taxonomies">' . "\n"; |
265
|
|
|
|
266
|
|
|
foreach ($taxonomies as $taxonomy) { |
267
|
|
|
$capitalTaxonomy = ucwords($taxonomy); |
268
|
|
|
$singularTaxonomy = Inflector::singularize($taxonomy); |
269
|
|
|
$output .= <<<EOT |
270
|
|
|
<div class="taxonomy"> |
271
|
|
|
<a href="{{site.url }}/${plural}/{$taxonomy}">${capitalTaxonomy}</a>: |
272
|
|
|
{% for ${singularTaxonomy} in page.${taxonomy} %} |
273
|
|
|
<a href="{{ site.url }}/${plural}/${taxonomy}/{{ ${singularTaxonomy} }}"> |
274
|
|
|
{{ ${singularTaxonomy} }} |
275
|
|
|
</a>{% if not loop.last %}, {% endif %} |
276
|
|
|
{% endfor %} |
277
|
|
|
</div> |
278
|
|
|
EOT; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$output .= "\n" . ' </section>' . "\n"; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
$output .= <<<EOT |
285
|
|
|
<footer> |
286
|
|
|
<p class="published_date">Published: {{page.date|date('F j, Y')}}</p> |
287
|
|
|
</footer> |
288
|
|
|
</article> |
289
|
|
|
{% endblock content_wrapper %} |
290
|
|
|
EOT; |
291
|
|
|
|
292
|
|
|
return $output; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
private function getTaxonomyIndexTemplate( |
296
|
|
|
string $plural, |
297
|
|
|
string $taxonomy, |
298
|
|
|
string $singularTaxonomy |
299
|
|
|
): string { |
300
|
|
|
$title = ucfirst($taxonomy); |
301
|
|
|
|
302
|
|
|
return <<<EOT |
303
|
|
|
--- |
304
|
|
|
use: [${plural}_${taxonomy}] |
305
|
|
|
--- |
306
|
|
|
<h1>${title}</h1> |
307
|
|
|
<ul> |
308
|
|
|
{% for ${singularTaxonomy},${plural} in data.${plural}_${taxonomy} %} |
309
|
|
|
<li> |
310
|
|
|
<a href="/${plural}/${taxonomy}/{{ ${singularTaxonomy}|url_encode(true) }}">{{ ${singularTaxonomy} }}</a> |
311
|
|
|
</li> |
312
|
|
|
{% endfor %} |
313
|
|
|
</ul> |
314
|
|
|
EOT; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
private function getTaxonomyViewTemplate( |
318
|
|
|
string $plural, |
319
|
|
|
string $singular, |
320
|
|
|
string $singularTaxonomy |
321
|
|
|
): string { |
322
|
|
|
$title = ucfirst($plural); |
323
|
|
|
|
324
|
|
|
return <<<EOT |
325
|
|
|
--- |
326
|
|
|
generator: [${plural}_${singularTaxonomy}_index, pagination] |
327
|
|
|
pagination: |
328
|
|
|
provider: page.${singularTaxonomy}_${plural} |
329
|
|
|
max_per_page: 10 |
330
|
|
|
--- |
331
|
|
|
<h1>{{ page.${singularTaxonomy}|capitalize }}</h1> |
332
|
|
|
<ul> |
333
|
|
|
{% for ${singular} in page.pagination.items %} |
334
|
|
|
<li><a href="{{ ${singular}.url }}">{{ ${singular}.title }}</a></li> |
335
|
|
|
{% endfor %} |
336
|
|
|
</ul> |
337
|
|
|
|
338
|
|
|
<nav> |
339
|
|
|
{% if page.pagination.previous_page or page.pagination.next_page %} |
340
|
|
|
{% if page.pagination.previous_page %} |
341
|
|
|
<a href="{{ site.url }}{{ page.pagination.previous_page.url }}">Newer ${title}</a> |
342
|
|
|
{% endif %} |
343
|
|
|
{% if page.pagination.next_page %} |
344
|
|
|
<a href="{{ site.url }}{{ page.pagination.next_page.url }}">Older ${title}</a> |
345
|
|
|
{% endif %} |
346
|
|
|
{% endif %} |
347
|
|
|
</nav> |
348
|
|
|
EOT; |
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
|
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.