Issues (20)

src/Console/BlockSyncCommand.php (4 issues)

1
<?php
2
3
namespace Riclep\Storyblok\Console;
4
5
use Barryvdh\Reflection\DocBlock;
6
use Barryvdh\Reflection\DocBlock\Context;
7
use Barryvdh\Reflection\DocBlock\Serializer;
8
use Barryvdh\Reflection\DocBlock\Tag;
9
use Illuminate\Console\Command;
10
use Illuminate\Filesystem\Filesystem;
11
use Illuminate\Support\Str;
12
use Storyblok\ApiException;
13
14
class BlockSyncCommand extends Command
15
{
16
17
	/**
18
	 * The name and signature of the console command.
19
	 *
20
	 * @var string
21
	 */
22
	protected $signature = 'ls:sync {component?} {--path=app/Storyblok/Blocks/}';
23
24
	/**
25
	 * The console command description.
26
	 *
27
	 * @var string
28
	 */
29
	protected $description = 'Sync Storyblok fields to Laravel Block class properties.';
30
	/**
31
	 * @var Filesystem
32
	 */
33
	private $files;
34
35
	/**
36
	 * Create a new command instance.
37
	 * @param  Filesystem  $files
38
	 */
39
	public function __construct(Filesystem $files)
40
	{
41
		parent::__construct();
42
43
		$this->files = $files;
44
	}
45
46
	/**
47
	 * Execute the console command.
48
	 *
49
	 * @return void
50
	 */
51
	public function handle(): void
52
	{
53
		$components = [];
54
		if ($this->argument('component')) {
55
			$components = [
56
				[
57
					'class' => $this->argument('component'),
58
					'component' => Str::of($this->argument('component'))->kebab(),
0 ignored issues
show
It seems like $this->argument('component') can also be of type array; however, parameter $string of Illuminate\Support\Str::of() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
					'component' => Str::of(/** @scrutinizer ignore-type */ $this->argument('component'))->kebab(),
Loading history...
59
				]
60
			];
61
		} else {
62
			// get all components
63
			if ($this->confirm("Do you wish to update all components in {$this->option('path')}?")) {
64
				$components = $this->getAllComponents();
65
			}
66
		}
67
68
		foreach ($components as $component) {
69
			$this->info("Updating {$component['component']}");
70
			$this->updateComponent($component);
71
		}
72
	}
73
74
	/**
75
	 * @return \Illuminate\Support\Collection
76
	 */
77
	protected function getAllComponents(): \Illuminate\Support\Collection
78
	{
79
		$path = $this->option('path');
80
81
		$files = collect($this->files->allFiles($path));
0 ignored issues
show
$this->files->allFiles($path) of type Symfony\Component\Finder\SplFileInfo[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

81
		$files = collect(/** @scrutinizer ignore-type */ $this->files->allFiles($path));
Loading history...
82
83
		return $files->map(fn($file) => [
84
			'class' => Str::of($file->getFilename())->replace('.php', ''),
85
			'component' => Str::of($file->getFilename())->replace('.php', '')->kebab(),
86
		]);
87
	}
88
89
	private function updateComponent($component): void
90
	{
91
		$rootNamespace = "App\Storyblok\Blocks";
92
		$class = "{$rootNamespace}\\{$component['class']}";
93
94
		$reflection = new \ReflectionClass($class);
95
		$namespace = $reflection->getNamespaceName();
96
		$path = $this->option('path');
97
		$originalDoc = $reflection->getDocComment();
98
99
		$filepath = $path.$component['class'].'.php';
100
101
		$phpdoc = new DocBlock($reflection, new Context($namespace));
102
103
		$tags = $phpdoc->getTagsByName('property-read');
104
105
		// Clear old attributes
106
		foreach ($tags as $tag) {
107
			$phpdoc->deleteTag($tag);
108
		}
109
110
		// Add new attributes
111
		$fields = $this->getComponentFields($component['component']);
112
		foreach ($fields as $field => $type) {
113
			$tagLine = trim("@property-read {$type} {$field}");
114
			$tag = Tag::createInstance($tagLine, $phpdoc);
115
116
			$phpdoc->appendTag($tag);
117
		}
118
119
		// Add default description if none exists
120
		if ( ! $phpdoc->getText()) {
121
			$phpdoc->setText("Class representation for Storyblok {$component['component']} component.");
122
		}
123
124
		// Write to file
125
		if ($this->files->exists($filepath)) {
126
			$serializer = new Serializer();
127
			$updatedBlock = $serializer->getDocComment($phpdoc);
128
129
			$content = $this->files->get($filepath);
130
131
			$content = str_replace($originalDoc, $updatedBlock, $content);
132
133
			$this->files->replace($filepath, $content);
134
			$this->files->chmod($filepath, 0644); // replace() changes permissions
135
136
			$this->info('Component updated successfully.');
137
		} else {
138
			$this->error('Component not yet created...');
139
		}
140
	}
141
142
	protected function getComponentFields($name): array
143
	{
144
		if (config('storyblok.oauth_token')) {
145
			$managementClient = new \Storyblok\ManagementClient(config('storyblok.oauth_token'));
146
147
			$components = collect($managementClient->get('spaces/'.config('storyblok.space_id').'/components')->getBody()['components']);
0 ignored issues
show
The method getBody() does not exist on stdClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

147
			$components = collect($managementClient->get('spaces/'.config('storyblok.space_id').'/components')->/** @scrutinizer ignore-call */ getBody()['components']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
148
149
			$component = $components->firstWhere('name', $name);
150
151
			if( ! $component ){
152
				$this->error("Storyblok component [{$name}] does not exist.");
153
154
				if ($this->confirm('Do you want to create it now?')) {
155
					$this->createStoryblokCompontent($name);
156
				}
157
			}
158
159
			$fields = [];
160
			foreach ($component['schema'] as $name => $data) {
0 ignored issues
show
$name is overwriting one of the parameters of this function.
Loading history...
161
				if ( ! $this->isIgnoredType($data['type'])) {
162
					$fields[$name] = $this->convertToPhpType($data['type']);
163
				}
164
			}
165
166
			return $fields;
167
		}
168
169
		$this->error("Please set your management token in the Storyblok config file");
170
		return [];
171
	}
172
173
	/**
174
	 * Create a new Storyblok component with given name
175
	 *
176
	 * @param  $component_name
177
	 * @throws ApiException
178
	 */
179
	protected function createStoryblokCompontent($component_name){
180
        $managementClient = new \Storyblok\ManagementClient(config('storyblok.oauth_token'));
181
182
        $payload = [
183
			"component" =>  [
184
				"name" =>  $component_name,
185
				"display_name" =>  str::of( str_replace('-', ' ' ,$component_name) )->ucfirst(),
186
					// "schema" =>  [],
187
    				// "is_root" =>  false,
188
					// "is_nestable" =>  true
189
			]
190
		];
191
192
		$component = $managementClient->post('spaces/'.config('storyblok.space_id').'/components/', $payload)->getBody();
193
194
		$this->info("Storyblok component created");
195
196
        return $component['component'];
197
	}
198
199
	/**
200
	 * Convert Storyblok types to PHP native types for proper type-hinting
201
	 *
202
	 * @param $type
203
	 * @return string
204
	 */
205
	protected function convertToPhpType($type): string
206
	{
207
		return match ($type) {
208
			"bloks" => "array",
209
			default => "string",
210
		};
211
	}
212
213
	/**
214
	 * There are certain Storyblok types that are not useful to model in our component classes. We can use this to
215
	 * filter those types out.
216
	 *
217
	 * @param $type
218
	 * @return bool
219
	 */
220
	protected function isIgnoredType($type): bool
221
	{
222
		$ignored = ['section'];
223
224
		return in_array($type, $ignored);
225
	}
226
227
}
228