Passed
Pull Request — main (#5260)
by
unknown
06:45
created

GedcomEditService   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 120
dl 0
loc 319
rs 8.48
c 0
b 0
f 0
wmc 49

10 Methods

Rating   Name   Duplication   Size   Complexity  
A factsToAdd() 0 17 3
A insertMissingRecordSubtags() 0 13 2
A createNewFact() 0 7 1
A insertMissingFactSubtags() 0 6 1
B insertMissingLevels() 0 60 11
A isHiddenTag() 0 16 3
A newFamilyFacts() 0 8 1
A newIndividualFacts() 0 10 1
A addChildToFamily() 0 25 6
D editLinesToGedcom() 0 61 20

How to fix   Complexity   

Complex Class

Complex classes like GedcomEditService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GedcomEditService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Fisharebest\Webtrees\Auth;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Auth was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use Fisharebest\Webtrees\Elements\AbstractXrefElement;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Family;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Family was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Fisharebest\Webtrees\Gedcom;
27
use Fisharebest\Webtrees\GedcomRecord;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\GedcomRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
use Fisharebest\Webtrees\Individual;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Individual was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use Fisharebest\Webtrees\Note;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Note was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use Fisharebest\Webtrees\Registry;
31
use Fisharebest\Webtrees\Site;
32
use Fisharebest\Webtrees\Tree;
33
use Illuminate\Support\Collection;
34
35
use function array_diff;
36
use function array_filter;
37
use function array_keys;
38
use function array_merge;
39
use function array_shift;
40
use function array_slice;
41
use function array_values;
42
use function assert;
43
use function count;
44
use function explode;
45
use function implode;
46
use function max;
47
use function preg_replace;
48
use function preg_split;
49
use function str_ends_with;
50
use function str_repeat;
51
use function str_replace;
52
use function str_starts_with;
53
use function substr_count;
54
use function trim;
55
56
use const ARRAY_FILTER_USE_BOTH;
57
use const ARRAY_FILTER_USE_KEY;
58
use const PHP_INT_MAX;
59
60
/**
61
 * Utilities to edit/save GEDCOM data.
62
 */
63
class GedcomEditService
64
{
65
    /**
66
     * @param Tree $tree
67
     *
68
     * @return Collection<int,Fact>
69
     */
70
    public function newFamilyFacts(Tree $tree): Collection
71
    {
72
        $dummy = Registry::familyFactory()->new('', '0 @@ FAM', null, $tree);
73
        $tags  = (new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FAMFACTS'))))
0 ignored issues
show
Bug introduced by
explode(',', $tree->getP...CK_REQUIRED_FAMFACTS')) of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

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

73
        $tags  = (new Collection(/** @scrutinizer ignore-type */ explode(',', $tree->getPreference('QUICK_REQUIRED_FAMFACTS'))))
Loading history...
74
            ->filter(static fn (string $tag): bool => $tag !== '');
75
        $facts = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
76
77
        return Fact::sortFacts($facts);
78
    }
79
80
    /**
81
     * @param Tree          $tree
82
     * @param string        $sex
83
     * @param array<string> $names
84
     *
85
     * @return Collection<int,Fact>
86
     */
87
    public function newIndividualFacts(Tree $tree, string $sex, array $names): Collection
88
    {
89
        $dummy      = Registry::individualFactory()->new('', '0 @@ INDI', null, $tree);
90
        $tags       = (new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FACTS'))))
0 ignored issues
show
Bug introduced by
explode(',', $tree->getP...QUICK_REQUIRED_FACTS')) of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

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

90
        $tags       = (new Collection(/** @scrutinizer ignore-type */ explode(',', $tree->getPreference('QUICK_REQUIRED_FACTS'))))
Loading history...
91
            ->filter(static fn (string $tag): bool => $tag !== '');
92
        $facts      = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
93
        $sex_fact   = new Collection([new Fact('1 SEX ' . $sex, $dummy, '')]);
0 ignored issues
show
Bug introduced by
array(new Fisharebest\We... ' . $sex, $dummy, '')) of type array<integer,Fisharebest\Webtrees\Fact> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

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

93
        $sex_fact   = new Collection(/** @scrutinizer ignore-type */ [new Fact('1 SEX ' . $sex, $dummy, '')]);
Loading history...
94
        $name_facts = Collection::make($names)->map(static fn (string $gedcom): Fact => new Fact($gedcom, $dummy, ''));
0 ignored issues
show
Bug introduced by
$names of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

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

94
        $name_facts = Collection::make(/** @scrutinizer ignore-type */ $names)->map(static fn (string $gedcom): Fact => new Fact($gedcom, $dummy, ''));
Loading history...
95
96
        return $sex_fact->concat($name_facts)->concat(Fact::sortFacts($facts));
97
    }
98
99
    /**
100
     * @param GedcomRecord $record
101
     * @param string       $tag
102
     *
103
     * @return Fact
104
     */
105
    private function createNewFact(GedcomRecord $record, string $tag): Fact
106
    {
107
        $element = Registry::elementFactory()->make($record->tag() . ':' . $tag);
108
        $default = $element->default($record->tree());
109
        $gedcom  = trim('1 ' . $tag . ' ' . $default);
110
111
        return new Fact($gedcom, $record, '');
112
    }
113
114
    /**
115
     * Reassemble edited GEDCOM fields into a GEDCOM fact/event string.
116
     *
117
     * @param string        $record_type
118
     * @param array<string> $levels
119
     * @param array<string> $tags
120
     * @param array<string> $values
121
     * @param bool          $append Are we appending to a level 0 record, or replacing a level 1 record?
122
     *
123
     * @return string
124
     */
125
    public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values, bool $append = true): string
126
    {
127
        // Assert all arrays are the same size.
128
        $count = count($levels);
129
        assert($count > 0);
130
        assert(count($tags) === $count);
131
        assert(count($values) === $count);
132
133
        $gedcom_lines = [];
134
        $hierarchy    = [$record_type];
135
136
        for ($i = 0; $i < $count; $i++) {
137
            $hierarchy[$levels[$i]] = $tags[$i];
138
139
            $full_tag   = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i]));
140
            $element    = Registry::elementFactory()->make($full_tag);
141
            $values[$i] = $element->canonical($values[$i]);
142
143
            // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y
144
            if ($levels[$i] === '1' && $values[$i] === 'Y') {
145
                for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) {
146
                    if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') {
147
                        $values[$i] = '';
148
                        break;
149
                    }
150
                }
151
            }
152
153
            // Find the next tag at the same level.  Check if any child tags have values.
154
            $children_with_values = false;
155
            for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; $j++) {
156
                if ($values[$j] !== '') {
157
                    $children_with_values = true;
158
                }
159
            }
160
161
            if ($values[$i] !== '' || $children_with_values  && !$element instanceof AbstractXrefElement) {
162
                if ($values[$i] === '') {
163
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i];
164
                } else {
165
                    // We use CONC for editing NOTE records.
166
                    if ($tags[$i] === 'CONC') {
167
                        $next_level = (int) $levels[$i];
168
                    } else {
169
                        $next_level = 1 + (int) $levels[$i];
170
                    }
171
172
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]);
173
                }
174
            } else {
175
                $i = $j - 1;
176
            }
177
        }
178
179
        $gedcom = implode("\n", $gedcom_lines);
180
181
        if ($append && $gedcom !== '') {
182
            $gedcom = "\n" . $gedcom;
183
        }
184
185
        return $gedcom;
186
    }
187
188
    /**
189
     * Add blank lines, to allow a user to add/edit new values.
190
     *
191
     * @param Fact $fact
192
     * @param bool $include_hidden
193
     *
194
     * @return string
195
     */
196
    public function insertMissingFactSubtags(Fact $fact, bool $include_hidden): string
197
    {
198
        // Merge CONT records onto their parent line.
199
        $gedcom = preg_replace('/\n\d CONT ?/', "\r", $fact->gedcom());
200
201
        return $this->insertMissingLevels($fact->record()->tree(), $fact->tag(), $gedcom, $include_hidden);
202
    }
203
204
    /**
205
     * Add blank lines, to allow a user to add/edit new values.
206
     *
207
     * @param GedcomRecord $record
208
     * @param bool         $include_hidden
209
     *
210
     * @return string
211
     */
212
    public function insertMissingRecordSubtags(GedcomRecord $record, bool $include_hidden): string
213
    {
214
        // Merge CONT records onto their parent line.
215
        $gedcom = preg_replace('/\n\d CONT ?/', "\r", $record->gedcom());
216
217
        $gedcom = $this->insertMissingLevels($record->tree(), $record->tag(), $gedcom, $include_hidden);
218
219
        // NOTE records have data at level 0.  Move it to 1 CONC.
220
        if ($record instanceof Note) {
221
            return preg_replace('/^0 @[^@]+@ NOTE/', '1 CONC', $gedcom);
222
        }
223
224
        return preg_replace('/^0.*\n/', '', $gedcom);
225
    }
226
227
    /**
228
     * List of facts/events to add to families and individuals.
229
     *
230
     * @param Family|Individual $record
231
     * @param bool              $include_hidden
232
     *
233
     * @return array<string>
234
     */
235
    public function factsToAdd(Family|Individual $record, bool $include_hidden): array
236
    {
237
        $subtags = Registry::elementFactory()->make($record->tag())->subtags();
238
239
        $subtags = array_filter($subtags, static fn (string $v, string $k) => !str_ends_with($v, ':1') || $record->facts([$k])->isEmpty(), ARRAY_FILTER_USE_BOTH);
240
241
        $subtags = array_keys($subtags);
242
243
        // Don't include facts/events that we have hidden in the control panel.
244
        $subtags = array_filter($subtags, fn (string $subtag): bool => !$this->isHiddenTag($record->tag() . ':' . $subtag));
245
246
        if (!$include_hidden) {
247
            $fn_hidden = fn (string $t): bool => !$this->isHiddenTag($record->tag() . ':' . $t);
248
            $subtags   = array_filter($subtags, $fn_hidden);
249
        }
250
251
        return array_diff($subtags, ['HUSB', 'WIFE', 'CHIL', 'FAMC', 'FAMS', 'CHAN']);
252
    }
253
254
    /**
255
     * @param Tree   $tree
256
     * @param string $tag
257
     * @param string $gedcom
258
     * @param bool   $include_hidden
259
     *
260
     * @return string
261
     */
262
    protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string
263
    {
264
        $next_level = substr_count($tag, ':') + 1;
265
        $factory    = Registry::elementFactory();
266
        $subtags    = $factory->make($tag)->subtags();
267
268
        // The first part is level N.  The remainder are level N+1.
269
        $parts  = preg_split('/\n(?=' . $next_level . ')/', $gedcom);
270
        $return = array_shift($parts) ?? '';
271
272
        foreach ($subtags as $subtag => $occurrences) {
273
            $hidden = str_ends_with($occurrences, ':?') || $this->isHiddenTag($tag . ':' . $subtag);
274
275
            if (!$include_hidden && $hidden) {
276
                continue;
277
            }
278
279
            [$min, $max] = explode(':', $occurrences);
280
281
            $min = (int) $min;
282
283
            if ($max === 'M') {
284
                $max = PHP_INT_MAX;
285
            } else {
286
                $max = (int) $max;
287
            }
288
289
            $count = 0;
290
291
            // Add expected subtags in our preferred order.
292
            foreach ($parts as $n => $part) {
293
                if (str_starts_with($part, $next_level . ' ' . $subtag)) {
294
                    $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden);
295
                    $count++;
296
                    unset($parts[$n]);
297
                }
298
            }
299
300
            // Allowed to have more of this subtag?
301
            if ($count < $max) {
302
                // Create a new one.
303
                $gedcom  = $next_level . ' ' . $subtag;
304
                $default = $factory->make($tag . ':' . $subtag)->default($tree);
305
                if ($default !== '') {
306
                    $gedcom .= ' ' . $default;
307
                }
308
309
                $number_to_add = max(1, $min - $count);
310
                $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden);
311
312
                $return .= str_repeat($gedcom_to_add, $number_to_add);
313
            }
314
        }
315
316
        // Now add any unexpected/existing data.
317
        if ($parts !== []) {
318
            $return .= "\n" . implode("\n", $parts);
319
        }
320
321
        return $return;
322
    }
323
324
    /**
325
     * List of tags to exclude when creating new data.
326
     *
327
     * @param string $tag
328
     *
329
     * @return bool
330
     */
331
    private function isHiddenTag(string $tag): bool
332
    {
333
        // Function to filter hidden tags.
334
        $fn_hide = static fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x);
335
336
        $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY);
337
        $preferences = array_values($preferences);
338
        $hidden_tags = array_merge(...$preferences);
339
340
        foreach ($hidden_tags as $hidden_tag) {
341
            if (str_contains($tag, $hidden_tag)) {
342
                return true;
343
            }
344
        }
345
346
        return false;
347
    }
348
349
    /**
350
     * Add a child to a family.
351
     *
352
     * @param Individual $child
353
     * @param Family $family
354
     *
355
     * @return void
356
     */
357
    public function addChildToFamily(Individual $child, Family $family): void
358
    {
359
        $child_birth_day = $child->getBirthDate()->julianDay();
360
        $child_gedcom = '1 CHIL @' . $child->xref() . '@';
361
        $family_facts = ['0 @' . $family->xref() . '@ FAM'];
362
363
        // Insert new child at the right place
364
        $done = false;
365
        foreach ($family->facts([], false, Auth::PRIV_HIDE, true) as $fact) {
366
            if ($fact->tag() === 'FAM:CHIL' && !$done) {
367
                // insert new child when born before this child
368
                if ($child_birth_day < $fact->target()->getBirthDate()->julianDay()) {
369
                    $family_facts[] = $child_gedcom;
370
                    $done = true;
371
                }
372
            }
373
            $family_facts[] = $fact->gedcom();
374
        }
375
        if (!$done) {
376
            // Append child at end
377
            $family_facts[] = $child_gedcom;
378
        }
379
380
        $gedcom = implode("\n", $family_facts);
381
        $family->updateRecord($gedcom, false);
382
    }
383
384
}
385