Issues (2560)

app/Place.php (7 issues)

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;
21
22
use Fisharebest\Webtrees\Module\ModuleInterface;
23
use Fisharebest\Webtrees\Module\ModuleListInterface;
24
use Fisharebest\Webtrees\Module\PlaceHierarchyListModule;
0 ignored issues
show
The type Fisharebest\Webtrees\Mod...laceHierarchyListModule 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...
25
use Fisharebest\Webtrees\Services\ModuleService;
0 ignored issues
show
The type Fisharebest\Webtrees\Services\ModuleService 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 Illuminate\Support\Collection;
27
28
use function e;
29
use function is_object;
30
use function preg_split;
31
use function strip_tags;
32
use function trim;
33
34
use const PREG_SPLIT_NO_EMPTY;
35
36
/**
37
 * A GEDCOM place (PLAC) object.
38
 */
39
class Place
40
{
41
    // "Westminster, London, England"
42
    private string $place_name;
43
44
    /** @var Collection<int,string> The parts of a place name, e.g. ["Westminster", "London", "England"] */
45
    private Collection $parts;
46
47
    private Tree $tree;
48
49
    /**
50
     * Create a place.
51
     *
52
     * @param string $place_name
53
     * @param Tree   $tree
54
     */
55
    public function __construct(string $place_name, Tree $tree)
56
    {
57
        // Ignore any empty parts in place names such as "Village, , , Country".
58
        $place_name  = trim($place_name);
59
        $this->parts = new Collection(preg_split(Gedcom::PLACE_SEPARATOR_REGEX, $place_name, -1, PREG_SPLIT_NO_EMPTY));
0 ignored issues
show
preg_split(Fisharebest\W...1, PREG_SPLIT_NO_EMPTY) of type array<mixed,array>|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

59
        $this->parts = new Collection(/** @scrutinizer ignore-type */ preg_split(Gedcom::PLACE_SEPARATOR_REGEX, $place_name, -1, PREG_SPLIT_NO_EMPTY));
Loading history...
60
61
        // Rebuild the placename in the correct format.
62
        $this->place_name = $this->parts->implode(Gedcom::PLACE_SEPARATOR);
63
64
        $this->tree = $tree;
65
    }
66
67
    /**
68
     * Find a place by its ID.
69
     *
70
     * @param int  $id
71
     * @param Tree $tree
72
     *
73
     * @return Place
74
     */
75
    public static function find(int $id, Tree $tree): Place
76
    {
77
        $parts = new Collection();
78
79
        while ($id !== 0) {
80
            $row = DB::table('places')
0 ignored issues
show
The type Fisharebest\Webtrees\DB was not found. Did you mean DB? If so, make sure to prefix the type with \.
Loading history...
81
                ->where('p_file', '=', $tree->id())
82
                ->where('p_id', '=', $id)
83
                ->first();
84
85
            if (is_object($row)) {
86
                $id = (int) $row->p_parent_id;
87
                $parts->add($row->p_place);
88
            } else {
89
                $id = 0;
90
            }
91
        }
92
93
        $place_name = $parts->implode(Gedcom::PLACE_SEPARATOR);
94
95
        return new Place($place_name, $tree);
96
    }
97
98
    /**
99
     * Get the higher level place.
100
     *
101
     * @return Place
102
     */
103
    public function parent(): Place
104
    {
105
        return new self($this->parts->slice(1)->implode(Gedcom::PLACE_SEPARATOR), $this->tree);
106
    }
107
108
    /**
109
     * The database row that contains this place.
110
     * Note that due to database collation, both "Quebec" and "Québec" will share the same row.
111
     *
112
     * @return int
113
     */
114
    public function id(): int
115
    {
116
        return Registry::cache()->array()->remember('place-' . $this->place_name, function (): int {
117
            // The "top-level" place won't exist in the database.
118
            if ($this->parts->isEmpty()) {
119
                return 0;
120
            }
121
122
            $parent_place_id = $this->parent()->id();
123
124
            $place_id = (int) DB::table('places')
125
                ->where('p_file', '=', $this->tree->id())
126
                ->where('p_place', '=', mb_substr($this->parts->first(), 0, 120))
127
                ->where('p_parent_id', '=', $parent_place_id)
128
                ->value('p_id');
129
130
            if ($place_id === 0) {
131
                $place = $this->parts->first();
132
133
                DB::table('places')->insert([
134
                    'p_file'        => $this->tree->id(),
135
                    'p_place'       => mb_substr($place, 0, 120),
136
                    'p_parent_id'   => $parent_place_id,
137
                    'p_std_soundex' => Soundex::russell($place),
0 ignored issues
show
The type Fisharebest\Webtrees\Soundex 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...
138
                    'p_dm_soundex'  => Soundex::daitchMokotoff($place),
139
                ]);
140
141
                $place_id = DB::lastInsertId();
142
            }
143
144
            return $place_id;
145
        });
146
    }
147
148
    /**
149
     * @return Tree
150
     */
151
    public function tree(): Tree
152
    {
153
        return $this->tree;
154
    }
155
156
    /**
157
     * Extract the locality (first parts) of a place name.
158
     *
159
     * @param int $n
160
     *
161
     * @return Collection<int,string>
162
     */
163
    public function firstParts(int $n): Collection
164
    {
165
        return $this->parts->slice(0, $n);
166
    }
167
168
    /**
169
     * Extract the country (last parts) of a place name.
170
     *
171
     * @param int $n
172
     *
173
     * @return Collection<int,string>
174
     */
175
    public function lastParts(int $n): Collection
176
    {
177
        return $this->parts->slice(-$n);
178
    }
179
180
    /**
181
     * Get the lower level places.
182
     *
183
     * @return array<Place>
184
     */
185
    public function getChildPlaces(): array
186
    {
187
        if ($this->place_name !== '') {
188
            $parent_text = Gedcom::PLACE_SEPARATOR . $this->place_name;
189
        } else {
190
            $parent_text = '';
191
        }
192
193
        return DB::table('places')
194
            ->where('p_file', '=', $this->tree->id())
195
            ->where('p_parent_id', '=', $this->id())
196
            ->pluck('p_place')
197
            ->sort(I18N::comparator())
0 ignored issues
show
The type Fisharebest\Webtrees\I18N 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...
198
            ->map(fn (string $place): Place => new self($place . $parent_text, $this->tree))
199
            ->all();
200
    }
201
202
    /**
203
     * Create a URL to the place-hierarchy page.
204
     *
205
     * @return string
206
     */
207
    public function url(): string
208
    {
209
        //find a module providing the place hierarchy
210
        $module = Registry::container()->get(ModuleService::class)
211
            ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())
0 ignored issues
show
The type Fisharebest\Webtrees\Auth was not found. Did you mean Auth? If so, make sure to prefix the type with \.
Loading history...
212
            ->first(static fn (ModuleInterface $module): bool => $module instanceof PlaceHierarchyListModule);
213
214
        if ($module instanceof PlaceHierarchyListModule) {
215
            return $module->listUrl($this->tree, [
216
                'place_id' => $this->id(),
217
                'tree'     => $this->tree->name(),
218
            ]);
219
        }
220
221
        // The place-list module is disabled...
222
        return '#';
223
    }
224
225
    /**
226
     * Format this place for GEDCOM data.
227
     *
228
     * @return string
229
     */
230
    public function gedcomName(): string
231
    {
232
        return $this->place_name;
233
    }
234
235
    /**
236
     * Format this place for display on screen.
237
     */
238
    public function placeName(): string
239
    {
240
        $place_name = $this->parts->first() ?? I18N::translate('unknown');
241
242
        return '<bdi>' . e($place_name) . '</bdi>';
243
    }
244
245
    /**
246
     * Generate the place name for display, including the full hierarchy.
247
     */
248
    public function fullName(bool $link = false): string
249
    {
250
        if ($this->parts->isEmpty()) {
251
            return '';
252
        }
253
254
        $full_name = $this->parts->implode(I18N::$list_separator);
255
256
        if ($link) {
257
            $url = $this->url();
258
259
            if ($url !== '#') {
260
                return '<a class="ut" href="' . e($url) . '">' . e($full_name) . '</a>';
261
            }
262
        }
263
264
        return '<bdi>' . e($full_name) . '</bdi>';
265
    }
266
267
    /**
268
     * For lists and charts, where the full name won’t fit.
269
     */
270
    public function shortName(bool $link = false): string
271
    {
272
        $SHOW_PEDIGREE_PLACES = (int) $this->tree->getPreference('SHOW_PEDIGREE_PLACES');
273
274
        // Abbreviate the place name, for lists
275
        if ($this->tree->getPreference('SHOW_PEDIGREE_PLACES_SUFFIX') === '1') {
276
            $parts = $this->lastParts($SHOW_PEDIGREE_PLACES);
277
        } else {
278
            $parts = $this->firstParts($SHOW_PEDIGREE_PLACES);
279
        }
280
281
        $short_name = $parts->implode(I18N::$list_separator);
282
283
        if ($link) {
284
            $url = $this->url();
285
286
            if ($url !== '#') {
287
                $title = strip_tags($this->fullName());
288
289
                return '<a class="ut" href="' . e($url) . '" title="' . e($title) . '">' . e($short_name) . '</a>';
290
            }
291
        }
292
293
        return '<span class="ut">' . e($short_name) . '</span>';
294
    }
295
}
296