Completed
Push — master ( 429bcf...33dd8e )
by
unknown
37:49 queued 34:59
created

ContentCreateCommand::execute()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 50
ccs 0
cts 32
cp 0
rs 8.4686
c 0
b 0
f 0
cc 6
nc 6
nop 2
crap 42
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);
0 ignored issues
show
Bug introduced by
It seems like $pluralType defined by $input->getArgument('type') on line 90 can also be of type array<integer,string>; however, Doctrine\Common\Inflector\Inflector::singularize() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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));
0 ignored issues
show
Bug introduced by
It seems like $pluralType defined by $input->getArgument('type') on line 90 can also be of type array<integer,string>; however, Sculpin\Bundle\ContentTy...and::getOutputMessage() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $pluralType defined by $input->getArgument('type') on line 90 can also be of type array<integer,string>; however, Sculpin\Bundle\ContentTy...teBoilerplateManifest() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $taxonomies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $taxonomies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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