PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / trunk
MailPoet – Newsletters, Email Marketing, and Automation vtrunk
5.37.0 5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Subscribers / ImportExport / Import / Cli.php
mailpoet / lib / Subscribers / ImportExport / Import Last commit date
Cli.php 1 day ago Import.php 1 day ago MailChimp.php 3 months ago MailChimpDataMapper.php 3 months ago index.php 3 years ago
Cli.php
464 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Subscribers\ImportExport\Import;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\CustomFields\CustomFieldsRepository;
9 use MailPoet\Entities\CustomFieldEntity;
10 use MailPoet\Entities\SegmentEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
13 use MailPoet\Segments\SegmentSaveController;
14 use MailPoet\Segments\SegmentsRepository;
15 use MailPoet\Segments\WP as SegmentsWP;
16 use MailPoet\Services\Validator;
17 use MailPoet\Subscribers\ImportExport\ImportExportRepository;
18 use MailPoet\Subscribers\SubscribersRepository;
19 use MailPoet\Tags\TagRepository;
20 use WP_CLI;
21
22 class Cli {
23 /** Subscriber fields that can appear as CSV columns, matched by their canonical name. */
24 private const BASE_FIELDS = [
25 'email',
26 'first_name',
27 'last_name',
28 'subscribed_ip',
29 'created_at',
30 'confirmed_at',
31 'confirmed_ip',
32 'tracking_consent',
33 'tracking_consent_method',
34 'tracking_consent_copy',
35 ];
36
37 private const NEW_SUBSCRIBER_STATUSES = [
38 SubscriberEntity::STATUS_SUBSCRIBED,
39 SubscriberEntity::STATUS_UNCONFIRMED,
40 SubscriberEntity::STATUS_UNSUBSCRIBED,
41 SubscriberEntity::STATUS_INACTIVE,
42 ];
43
44 private const EXISTING_SUBSCRIBER_STATUSES = [
45 Import::STATUS_DONT_UPDATE,
46 SubscriberEntity::STATUS_SUBSCRIBED,
47 SubscriberEntity::STATUS_UNSUBSCRIBED,
48 SubscriberEntity::STATUS_INACTIVE,
49 ];
50
51 private const DEFAULT_BATCH_SIZE = 2000;
52
53 /** @var SegmentsWP */
54 private $wpSegment;
55
56 /** @var CustomFieldsRepository */
57 private $customFieldsRepository;
58
59 /** @var ImportExportRepository */
60 private $importExportRepository;
61
62 /** @var NewsletterOptionsRepository */
63 private $newsletterOptionsRepository;
64
65 /** @var SubscribersRepository */
66 private $subscribersRepository;
67
68 /** @var TagRepository */
69 private $tagRepository;
70
71 /** @var Validator */
72 private $validator;
73
74 /** @var SegmentsRepository */
75 private $segmentsRepository;
76
77 /** @var SegmentSaveController */
78 private $segmentSaveController;
79
80 public function __construct(
81 SegmentsWP $wpSegment,
82 CustomFieldsRepository $customFieldsRepository,
83 ImportExportRepository $importExportRepository,
84 NewsletterOptionsRepository $newsletterOptionsRepository,
85 SubscribersRepository $subscribersRepository,
86 TagRepository $tagRepository,
87 Validator $validator,
88 SegmentsRepository $segmentsRepository,
89 SegmentSaveController $segmentSaveController
90 ) {
91 $this->wpSegment = $wpSegment;
92 $this->customFieldsRepository = $customFieldsRepository;
93 $this->importExportRepository = $importExportRepository;
94 $this->newsletterOptionsRepository = $newsletterOptionsRepository;
95 $this->subscribersRepository = $subscribersRepository;
96 $this->tagRepository = $tagRepository;
97 $this->validator = $validator;
98 $this->segmentsRepository = $segmentsRepository;
99 $this->segmentSaveController = $segmentSaveController;
100 }
101
102 public function initialize(): void {
103 if (!class_exists(WP_CLI::class)) {
104 return;
105 }
106
107 WP_CLI::add_command('mailpoet import', [$this, 'import'], [
108 'shortdesc' => 'Imports subscribers into MailPoet from a CSV file',
109 'synopsis' => [
110 [
111 'type' => 'positional',
112 'name' => 'file',
113 'description' => 'Path to the CSV file. The header row must use MailPoet field names (email, first_name, last_name, subscribed_ip, created_at, confirmed_at, confirmed_ip, tracking_consent, tracking_consent_method, tracking_consent_copy) or existing custom field names. An "email" column is required. tracking_consent accepts granted, denied or unknown; a blank cell leaves the stored value alone.',
114 'optional' => false,
115 ],
116 [
117 'type' => 'assoc',
118 'name' => 'segments',
119 'description' => 'Comma-separated segment IDs or names to add subscribers to. Names that do not exist are created.',
120 'optional' => true,
121 ],
122 [
123 'type' => 'assoc',
124 'name' => 'status',
125 'description' => 'Status for newly created subscribers.',
126 'optional' => true,
127 'default' => SubscriberEntity::STATUS_SUBSCRIBED,
128 'options' => self::NEW_SUBSCRIBER_STATUSES,
129 ],
130 [
131 'type' => 'flag',
132 'name' => 'update-existing',
133 'description' => 'Update the details of subscribers that already exist.',
134 'optional' => true,
135 ],
136 [
137 'type' => 'assoc',
138 'name' => 'existing-status',
139 'description' => 'Status to set on existing subscribers.',
140 'optional' => true,
141 'default' => Import::STATUS_DONT_UPDATE,
142 'options' => self::EXISTING_SUBSCRIBER_STATUSES,
143 ],
144 [
145 'type' => 'assoc',
146 'name' => 'tags',
147 'description' => 'Comma-separated tag names to assign to imported subscribers. Tags are created if they do not exist.',
148 'optional' => true,
149 ],
150 [
151 'type' => 'assoc',
152 'name' => 'batch-size',
153 'description' => 'Number of subscribers to process per batch.',
154 'optional' => true,
155 'default' => self::DEFAULT_BATCH_SIZE,
156 ],
157 [
158 'type' => 'flag',
159 'name' => 'dry-run',
160 'description' => 'Parse and validate the file and report what would be imported without writing anything.',
161 'optional' => true,
162 ],
163 ],
164 ]);
165 }
166
167 /**
168 * WP-CLI entry point. Translates CLI input/output; the work happens in run().
169 *
170 * @param string[] $args
171 * @param array<string, string> $assocArgs
172 */
173 public function import(array $args, array $assocArgs): void {
174 $options = [
175 'segments' => $this->parseList((string)($assocArgs['segments'] ?? '')),
176 'status' => (string)($assocArgs['status'] ?? SubscriberEntity::STATUS_SUBSCRIBED),
177 'existing_status' => (string)($assocArgs['existing-status'] ?? Import::STATUS_DONT_UPDATE),
178 'update_existing' => !empty($assocArgs['update-existing']),
179 'tags' => $this->parseList((string)($assocArgs['tags'] ?? '')),
180 'batch_size' => (int)($assocArgs['batch-size'] ?? self::DEFAULT_BATCH_SIZE),
181 'dry_run' => !empty($assocArgs['dry-run']),
182 ];
183
184 try {
185 $totals = $this->run((string)($args[0] ?? ''), $options, function (string $message): void {
186 WP_CLI::log($message);
187 });
188 } catch (\Exception $e) {
189 WP_CLI::error($e->getMessage());
190 return;
191 }
192
193 $rowsRead = $totals['rows'] + $totals['skipped'];
194 $skippedNotice = '';
195 if ($totals['skipped'] > 0) {
196 $skippedTemplate = $totals['skipped'] === 1
197 ? ' %d row was skipped because its column count did not match the header.'
198 : ' %d rows were skipped because their column count did not match the header.';
199 $skippedNotice = sprintf($skippedTemplate, $totals['skipped']);
200 }
201
202 if ($options['dry_run']) {
203 WP_CLI::success(sprintf(
204 'Dry run: %d rows read, %d subscribers with a valid email. Nothing was written.%s',
205 $rowsRead,
206 $totals['valid'],
207 $skippedNotice
208 ));
209 return;
210 }
211
212 WP_CLI::success(sprintf(
213 'Import finished: %d created, %d updated (out of %d rows).%s',
214 $totals['created'],
215 $totals['updated'],
216 $rowsRead,
217 $skippedNotice
218 ));
219 }
220
221 /**
222 * Parses the CSV file and imports the subscribers. Throws on invalid input.
223 * Free of any WP-CLI dependency so it can be unit/integration tested.
224 *
225 * @param array{segments: string[], status: string, existing_status: string, update_existing: bool, tags: string[], batch_size: int, dry_run: bool} $options
226 * @param callable(string): void|null $logger
227 * @return array{created: int, updated: int, valid: int, rows: int, skipped: int}
228 * @throws \RuntimeException
229 */
230 public function run(string $file, array $options, ?callable $logger = null): array {
231 $log = $logger ?? function (string $message): void {
232 };
233
234 if (!is_readable($file)) {
235 throw new \RuntimeException(sprintf('File "%s" does not exist or is not readable.', $file));
236 }
237 if (!in_array($options['status'], self::NEW_SUBSCRIBER_STATUSES, true)) {
238 throw new \RuntimeException(sprintf('Invalid status "%s". Allowed: %s.', $options['status'], implode(', ', self::NEW_SUBSCRIBER_STATUSES)));
239 }
240 if (!in_array($options['existing_status'], self::EXISTING_SUBSCRIBER_STATUSES, true)) {
241 throw new \RuntimeException(sprintf('Invalid existing status "%s". Allowed: %s.', $options['existing_status'], implode(', ', self::EXISTING_SUBSCRIBER_STATUSES)));
242 }
243 if ($options['batch_size'] < 1) {
244 throw new \RuntimeException('Batch size must be a positive integer.');
245 }
246
247 $segmentIds = $this->resolveSegments($options['segments'], $options['dry_run'], $log);
248
249 $handle = fopen($file, 'r');
250 if ($handle === false) {
251 throw new \RuntimeException(sprintf('Unable to open file "%s".', $file));
252 }
253
254 try {
255 $header = fgetcsv($handle, 0, ',', '"', '\\');
256 if (!is_array($header)) {
257 throw new \RuntimeException('The CSV file is empty or has no header row.');
258 }
259 $columns = $this->buildColumns($header);
260
261 $headerColumnCount = count($header);
262 $totals = ['created' => 0, 'updated' => 0, 'valid' => 0, 'rows' => 0, 'skipped' => 0];
263 $batch = [];
264 $lineNumber = 1; // header is line 1
265 while (is_array($row = fgetcsv($handle, 0, ',', '"', '\\'))) {
266 $lineNumber++;
267 if ($row === [null]) {
268 continue; // skip blank lines
269 }
270 // fgetcsv does not pad short rows or trim long ones. A row whose column
271 // count differs from the header would misalign the per-column arrays built
272 // in Import (a value landing on the wrong subscriber), so skip it and warn
273 // rather than guessing which columns are missing.
274 if (count($row) !== $headerColumnCount) {
275 $totals['skipped']++;
276 $log(sprintf(' Skipped line %d: expected %d column(s) but found %d.', $lineNumber, $headerColumnCount, count($row)));
277 continue;
278 }
279 $totals['rows']++;
280 $batch[] = $row;
281 if (count($batch) >= $options['batch_size']) {
282 $this->processBatch($batch, $columns, $segmentIds, $options, $totals, $log);
283 $batch = [];
284 }
285 }
286 if ($batch) {
287 $this->processBatch($batch, $columns, $segmentIds, $options, $totals, $log);
288 }
289 } finally {
290 fclose($handle);
291 }
292
293 return $totals;
294 }
295
296 /**
297 * @param array<int, array<int, string|null>> $batch
298 * @param array<string|int, array{index: int}> $columns
299 * @param int[] $segmentIds
300 * @param array{segments: string[], status: string, existing_status: string, update_existing: bool, tags: string[], batch_size: int, dry_run: bool} $options
301 * @param array{created: int, updated: int, valid: int, rows: int, skipped: int} $totals
302 * @param callable(string): void $log
303 */
304 private function processBatch(
305 array $batch,
306 array $columns,
307 array $segmentIds,
308 array $options,
309 array &$totals,
310 callable $log
311 ): void {
312 $data = [
313 'subscribers' => $batch,
314 'columns' => $columns,
315 'segments' => $segmentIds,
316 'tags' => $options['tags'],
317 'timestamp' => time(),
318 'newSubscribersStatus' => $options['status'],
319 'existingSubscribersStatus' => $options['existing_status'],
320 'updateSubscribers' => $options['update_existing'],
321 ];
322
323 $import = new Import(
324 $this->wpSegment,
325 $this->customFieldsRepository,
326 $this->importExportRepository,
327 $this->newsletterOptionsRepository,
328 $this->subscribersRepository,
329 $this->tagRepository,
330 $this->validator,
331 $data
332 );
333
334 if ($options['dry_run']) {
335 $valid = $import->validateSubscribersData($import->subscribersData);
336 $emails = is_array($valid) && isset($valid['email']) ? $valid['email'] : [];
337 $totals['valid'] += count($emails);
338 return;
339 }
340
341 $result = $import->process();
342 $totals['created'] += (int)$result['created'];
343 $totals['updated'] += (int)$result['updated'];
344 $log(sprintf(' Batch of %d rows: %d created, %d updated.', count($batch), $result['created'], $result['updated']));
345 }
346
347 /**
348 * Maps each CSV header to a subscriber field or custom field id.
349 *
350 * @param array<int, string|null> $header
351 * @return array<string|int, array{index: int}>
352 * @throws \RuntimeException
353 */
354 private function buildColumns(array $header): array {
355 $columns = [];
356 $unknown = [];
357 $duplicates = [];
358 $namesByField = [];
359 foreach ($header as $index => $name) {
360 $name = trim((string)$name);
361 if ($name === '') {
362 continue;
363 }
364 $field = $this->resolveField($name);
365 if ($field === null) {
366 $unknown[] = $name;
367 continue;
368 }
369 if (isset($columns[$field])) {
370 $duplicates[$field] = array_merge($namesByField[$field], [$name]);
371 continue;
372 }
373 $namesByField[$field] = [$name];
374 $columns[$field] = ['index' => $index];
375 }
376
377 if ($unknown) {
378 throw new \RuntimeException(sprintf(
379 'Unrecognized CSV column(s): %s. Use MailPoet field names (%s) or an existing custom field name.',
380 implode(', ', $unknown),
381 implode(', ', self::BASE_FIELDS)
382 ));
383 }
384
385 if ($duplicates) {
386 $details = array_map(function (array $names): string {
387 return implode(', ', $names);
388 }, $duplicates);
389 throw new \RuntimeException(sprintf(
390 'Duplicate CSV column(s) mapping to the same field: %s. Each field may only appear once in the header.',
391 implode('; ', $details)
392 ));
393 }
394
395 if (!isset($columns['email'])) {
396 throw new \RuntimeException('The CSV file must contain an "email" column.');
397 }
398
399 return $columns;
400 }
401
402 /**
403 * @return string|int|null Field name, custom field id, or null when unrecognized.
404 */
405 private function resolveField(string $header) {
406 if (in_array(strtolower($header), self::BASE_FIELDS, true)) {
407 return strtolower($header);
408 }
409 $customField = $this->customFieldsRepository->findOneBy(['name' => $header]);
410 if ($customField instanceof CustomFieldEntity) {
411 return $customField->getId();
412 }
413 return null;
414 }
415
416 /**
417 * Resolves segment IDs/names to IDs. Unknown names are created unless this is a dry run.
418 *
419 * @param string[] $segments
420 * @param bool $dryRun
421 * @param callable(string): void $log
422 * @return int[]
423 * @throws \RuntimeException
424 */
425 private function resolveSegments(array $segments, bool $dryRun, callable $log): array {
426 $ids = [];
427 foreach ($segments as $segment) {
428 if (ctype_digit($segment)) {
429 $entity = $this->segmentsRepository->findOneById((int)$segment);
430 if (!$entity instanceof SegmentEntity) {
431 throw new \RuntimeException(sprintf('Segment with ID "%s" does not exist.', $segment));
432 }
433 $ids[] = (int)$segment;
434 continue;
435 }
436 $entity = $this->segmentsRepository->findOneBy(['name' => $segment, 'type' => SegmentEntity::TYPE_DEFAULT]);
437 if ($entity instanceof SegmentEntity) {
438 $ids[] = (int)$entity->getId();
439 continue;
440 }
441 if ($dryRun) {
442 $log(sprintf('Segment "%s" would be created.', $segment));
443 continue;
444 }
445 $entity = $this->segmentSaveController->save(['name' => $segment]);
446 $log(sprintf('Created segment "%s" (ID %d).', $segment, (int)$entity->getId()));
447 $ids[] = (int)$entity->getId();
448 }
449 return array_values(array_unique($ids));
450 }
451
452 /**
453 * @return string[]
454 */
455 private function parseList(string $value): array {
456 if (trim($value) === '') {
457 return [];
458 }
459 return array_values(array_filter(array_map('trim', explode(',', $value)), function (string $item): bool {
460 return $item !== '';
461 }));
462 }
463 }
464