| Total Complexity | 100 | 
| Total Lines | 968 | 
| Duplicated Lines | 0 % | 
| Changes | 2 | ||
| Bugs | 0 | Features | 0 | 
Complex classes like ClippingsCartModule 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 ClippingsCartModule, and based on these observations, apply Extract Interface, too.
| 1 | <?php  | 
            ||
| 78 | class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface  | 
            ||
| 79 | { | 
            ||
| 80 | use ModuleMenuTrait;  | 
            ||
| 81 | |||
| 82 | // Routes that have a record which can be added to the clipboard  | 
            ||
| 83 | private const ROUTES_WITH_RECORDS = [  | 
            ||
| 84 | 'Family' => FamilyPage::class,  | 
            ||
| 85 | 'Individual' => IndividualPage::class,  | 
            ||
| 86 | 'Media' => MediaPage::class,  | 
            ||
| 87 | 'Note' => NotePage::class,  | 
            ||
| 88 | 'Repository' => RepositoryPage::class,  | 
            ||
| 89 | 'Source' => SourcePage::class,  | 
            ||
| 90 | ];  | 
            ||
| 91 | |||
| 92 | /** @var int The default access level for this module. It can be changed in the control panel. */  | 
            ||
| 93 | protected $access_level = Auth::PRIV_USER;  | 
            ||
| 94 | |||
| 95 | /**  | 
            ||
| 96 | * @var UserService  | 
            ||
| 97 | */  | 
            ||
| 98 | private $user_service;  | 
            ||
| 99 | |||
| 100 | /**  | 
            ||
| 101 | * ClippingsCartModule constructor.  | 
            ||
| 102 | *  | 
            ||
| 103 | * @param UserService $user_service  | 
            ||
| 104 | */  | 
            ||
| 105 | public function __construct(UserService $user_service)  | 
            ||
| 106 |     { | 
            ||
| 107 | $this->user_service = $user_service;  | 
            ||
| 108 | }  | 
            ||
| 109 | |||
| 110 | /**  | 
            ||
| 111 | * How should this module be identified in the control panel, etc.?  | 
            ||
| 112 | *  | 
            ||
| 113 | * @return string  | 
            ||
| 114 | */  | 
            ||
| 115 | public function title(): string  | 
            ||
| 116 |     { | 
            ||
| 117 | /* I18N: Name of a module */  | 
            ||
| 118 |         return I18N::translate('Clippings cart'); | 
            ||
| 119 | }  | 
            ||
| 120 | |||
| 121 | /**  | 
            ||
| 122 | * A sentence describing what this module does.  | 
            ||
| 123 | *  | 
            ||
| 124 | * @return string  | 
            ||
| 125 | */  | 
            ||
| 126 | public function description(): string  | 
            ||
| 127 |     { | 
            ||
| 128 | /* I18N: Description of the “Clippings cart” module */  | 
            ||
| 129 |         return I18N::translate('Select records from your family tree and save them as a GEDCOM file.'); | 
            ||
| 130 | }  | 
            ||
| 131 | |||
| 132 | /**  | 
            ||
| 133 | * The default position for this menu. It can be changed in the control panel.  | 
            ||
| 134 | *  | 
            ||
| 135 | * @return int  | 
            ||
| 136 | */  | 
            ||
| 137 | public function defaultMenuOrder(): int  | 
            ||
| 138 |     { | 
            ||
| 139 | return 6;  | 
            ||
| 140 | }  | 
            ||
| 141 | |||
| 142 | /**  | 
            ||
| 143 | * A menu, to be added to the main application menu.  | 
            ||
| 144 | *  | 
            ||
| 145 | * @param Tree $tree  | 
            ||
| 146 | *  | 
            ||
| 147 | * @return Menu|null  | 
            ||
| 148 | */  | 
            ||
| 149 | public function getMenu(Tree $tree): ?Menu  | 
            ||
| 150 |     { | 
            ||
| 151 | /** @var ServerRequestInterface $request */  | 
            ||
| 152 | $request = app(ServerRequestInterface::class);  | 
            ||
| 153 | |||
| 154 |         $route = $request->getAttribute('route'); | 
            ||
| 155 | |||
| 156 | $submenus = [  | 
            ||
| 157 |             new Menu($this->title(), route('module', [ | 
            ||
| 158 | 'module' => $this->name(),  | 
            ||
| 159 | 'action' => 'Show',  | 
            ||
| 160 | 'tree' => $tree->name(),  | 
            ||
| 161 | ]), 'menu-clippings-cart', ['rel' => 'nofollow']),  | 
            ||
| 162 | ];  | 
            ||
| 163 | |||
| 164 | $action = array_search($route, self::ROUTES_WITH_RECORDS);  | 
            ||
| 165 |         if ($action !== false) { | 
            ||
| 166 |             $xref = $request->getAttribute('xref'); | 
            ||
| 167 | assert(is_string($xref));  | 
            ||
| 168 | |||
| 169 |             $add_route = route('module', [ | 
            ||
| 170 | 'module' => $this->name(),  | 
            ||
| 171 | 'action' => 'Add' . $action,  | 
            ||
| 172 | 'xref' => $xref,  | 
            ||
| 173 | 'tree' => $tree->name(),  | 
            ||
| 174 | ]);  | 
            ||
| 175 | |||
| 176 |             $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']); | 
            ||
| 177 | }  | 
            ||
| 178 | |||
| 179 |         if (!$this->isCartEmpty($tree)) { | 
            ||
| 180 |             $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [ | 
            ||
| 181 | 'module' => $this->name(),  | 
            ||
| 182 | 'action' => 'Empty',  | 
            ||
| 183 | 'tree' => $tree->name(),  | 
            ||
| 184 | ]), 'menu-clippings-empty', ['rel' => 'nofollow']);  | 
            ||
| 185 | |||
| 186 |             $submenus[] = new Menu(I18N::translate('Download'), route('module', [ | 
            ||
| 187 | 'module' => $this->name(),  | 
            ||
| 188 | 'action' => 'DownloadForm',  | 
            ||
| 189 | 'tree' => $tree->name(),  | 
            ||
| 190 | ]), 'menu-clippings-download', ['rel' => 'nofollow']);  | 
            ||
| 191 | }  | 
            ||
| 192 | |||
| 193 | return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus);  | 
            ||
| 194 | }  | 
            ||
| 195 | |||
| 196 | /**  | 
            ||
| 197 | * @param ServerRequestInterface $request  | 
            ||
| 198 | *  | 
            ||
| 199 | * @return ResponseInterface  | 
            ||
| 200 | */  | 
            ||
| 201 | public function postDownloadAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 202 |     { | 
            ||
| 203 |         $tree = $request->getAttribute('tree'); | 
            ||
| 204 | assert($tree instanceof Tree);  | 
            ||
| 205 | |||
| 206 | $privatize_export = $request->getParsedBody()['privatize_export'];  | 
            ||
| 207 | $convert = (bool) ($request->getParsedBody()['convert'] ?? false);  | 
            ||
| 208 | |||
| 209 |         $cart = Session::get('cart', []); | 
            ||
| 210 | |||
| 211 | $xrefs = array_keys($cart[$tree->name()] ?? []);  | 
            ||
| 212 | |||
| 213 | // Create a new/empty .ZIP file  | 
            ||
| 214 | $temp_zip_file = tempnam(sys_get_temp_dir(), 'webtrees-zip-');  | 
            ||
| 215 | $zip_adapter = new ZipArchiveAdapter($temp_zip_file);  | 
            ||
| 216 | $zip_filesystem = new Filesystem($zip_adapter);  | 
            ||
| 217 | |||
| 218 | $manager = new MountManager([  | 
            ||
| 219 | 'media' => $tree->mediaFilesystem(),  | 
            ||
| 220 | 'zip' => $zip_filesystem,  | 
            ||
| 221 | ]);  | 
            ||
| 222 | |||
| 223 | // Media file prefix  | 
            ||
| 224 |         $path = $tree->getPreference('MEDIA_DIRECTORY'); | 
            ||
| 225 | |||
| 226 | // GEDCOM file header  | 
            ||
| 227 | $filetext = FunctionsExport::gedcomHeader($tree, $convert ? 'ANSI' : 'UTF-8');  | 
            ||
| 228 | |||
| 229 |         switch ($privatize_export) { | 
            ||
| 230 | case 'gedadmin':  | 
            ||
| 231 | $access_level = Auth::PRIV_NONE;  | 
            ||
| 232 | break;  | 
            ||
| 233 | case 'user':  | 
            ||
| 234 | $access_level = Auth::PRIV_USER;  | 
            ||
| 235 | break;  | 
            ||
| 236 | case 'visitor':  | 
            ||
| 237 | $access_level = Auth::PRIV_PRIVATE;  | 
            ||
| 238 | break;  | 
            ||
| 239 | case 'none':  | 
            ||
| 240 | default:  | 
            ||
| 241 | $access_level = Auth::PRIV_HIDE;  | 
            ||
| 242 | break;  | 
            ||
| 243 | }  | 
            ||
| 244 | |||
| 245 |         foreach ($xrefs as $xref) { | 
            ||
| 246 | $object = GedcomRecord::getInstance($xref, $tree);  | 
            ||
| 247 | // The object may have been deleted since we added it to the cart....  | 
            ||
| 248 |             if ($object instanceof  GedcomRecord) { | 
            ||
| 249 | $record = $object->privatizeGedcom($access_level);  | 
            ||
| 250 | // Remove links to objects that aren't in the cart  | 
            ||
| 251 |                 preg_match_all('/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[2-9].*)*/', $record, $matches, PREG_SET_ORDER); | 
            ||
| 252 |                 foreach ($matches as $match) { | 
            ||
| 253 |                     if (!in_array($match[1], $xrefs, true)) { | 
            ||
| 254 | $record = str_replace($match[0], '', $record);  | 
            ||
| 255 | }  | 
            ||
| 256 | }  | 
            ||
| 257 |                 preg_match_all('/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[3-9].*)*/', $record, $matches, PREG_SET_ORDER); | 
            ||
| 258 |                 foreach ($matches as $match) { | 
            ||
| 259 |                     if (!in_array($match[1], $xrefs, true)) { | 
            ||
| 260 | $record = str_replace($match[0], '', $record);  | 
            ||
| 261 | }  | 
            ||
| 262 | }  | 
            ||
| 263 |                 preg_match_all('/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[4-9].*)*/', $record, $matches, PREG_SET_ORDER); | 
            ||
| 264 |                 foreach ($matches as $match) { | 
            ||
| 265 |                     if (!in_array($match[1], $xrefs, true)) { | 
            ||
| 266 | $record = str_replace($match[0], '', $record);  | 
            ||
| 267 | }  | 
            ||
| 268 | }  | 
            ||
| 269 | |||
| 270 |                 if ($object instanceof Individual || $object instanceof Family) { | 
            ||
| 271 | $filetext .= $record . "\n";  | 
            ||
| 272 | $filetext .= "1 SOUR @WEBTREES@\n";  | 
            ||
| 273 | $filetext .= '2 PAGE ' . $object->url() . "\n";  | 
            ||
| 274 |                 } elseif ($object instanceof Source) { | 
            ||
| 275 | $filetext .= $record . "\n";  | 
            ||
| 276 | $filetext .= '1 NOTE ' . $object->url() . "\n";  | 
            ||
| 277 |                 } elseif ($object instanceof Media) { | 
            ||
| 278 | // Add the media files to the archive  | 
            ||
| 279 |                     foreach ($object->mediaFiles() as $media_file) { | 
            ||
| 280 | $from = 'media://' . $media_file->filename();  | 
            ||
| 281 | $to = 'zip://' . $path . $media_file->filename();  | 
            ||
| 282 |                         if (!$media_file->isExternal() && $manager->has($from)) { | 
            ||
| 283 | $manager->copy($from, $to);  | 
            ||
| 284 | }  | 
            ||
| 285 | }  | 
            ||
| 286 | $filetext .= $record . "\n";  | 
            ||
| 287 |                 } else { | 
            ||
| 288 | $filetext .= $record . "\n";  | 
            ||
| 289 | }  | 
            ||
| 290 | }  | 
            ||
| 291 | }  | 
            ||
| 292 | |||
| 293 |         $base_url = $request->getAttribute('base_url'); | 
            ||
| 294 | |||
| 295 | // Create a source, to indicate the source of the data.  | 
            ||
| 296 | $filetext .= "0 @WEBTREES@ SOUR\n1 TITL " . $base_url . "\n";  | 
            ||
| 297 |         $author   = $this->user_service->find((int) $tree->getPreference('CONTACT_USER_ID')); | 
            ||
| 298 |         if ($author !== null) { | 
            ||
| 299 | $filetext .= '1 AUTH ' . $author->realName() . "\n";  | 
            ||
| 300 | }  | 
            ||
| 301 | $filetext .= "0 TRLR\n";  | 
            ||
| 302 | |||
| 303 | // Make sure the preferred line endings are used  | 
            ||
| 304 |         $filetext = str_replace('\n', Gedcom::EOL, $filetext); | 
            ||
| 305 | |||
| 306 |         if ($convert) { | 
            ||
| 307 | $filetext = utf8_decode($filetext);  | 
            ||
| 308 | }  | 
            ||
| 309 | |||
| 310 | // Finally add the GEDCOM file to the .ZIP file.  | 
            ||
| 311 |         $zip_filesystem->write('clippings.ged', $filetext); | 
            ||
| 312 | |||
| 313 | // Need to force-close ZipArchive filesystems.  | 
            ||
| 314 | $zip_adapter->getArchive()->close();  | 
            ||
| 315 | |||
| 316 | // Use a stream, so that we do not have to load the entire file into memory.  | 
            ||
| 317 | $stream = app(StreamFactoryInterface::class)->createStreamFromFile($temp_zip_file);  | 
            ||
| 318 | |||
| 319 | /** @var ResponseFactoryInterface $response_factory */  | 
            ||
| 320 | $response_factory = app(ResponseFactoryInterface::class);  | 
            ||
| 321 | |||
| 322 | return $response_factory->createResponse()  | 
            ||
| 323 | ->withBody($stream)  | 
            ||
| 324 |             ->withHeader('Content-Type', 'application/zip') | 
            ||
| 325 |             ->withHeader('Content-Disposition', 'attachment; filename="clippings.zip'); | 
            ||
| 326 | }  | 
            ||
| 327 | |||
| 328 | /**  | 
            ||
| 329 | * @param ServerRequestInterface $request  | 
            ||
| 330 | *  | 
            ||
| 331 | * @return ResponseInterface  | 
            ||
| 332 | */  | 
            ||
| 333 | public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 334 |     { | 
            ||
| 335 |         $tree = $request->getAttribute('tree'); | 
            ||
| 336 | assert($tree instanceof Tree);  | 
            ||
| 337 | |||
| 338 |         $user  = $request->getAttribute('user'); | 
            ||
| 339 |         $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download'); | 
            ||
| 340 | |||
| 341 |         return $this->viewResponse('modules/clippings/download', [ | 
            ||
| 342 | 'is_manager' => Auth::isManager($tree, $user),  | 
            ||
| 343 | 'is_member' => Auth::isMember($tree, $user),  | 
            ||
| 344 | 'module' => $this->name(),  | 
            ||
| 345 | 'title' => $title,  | 
            ||
| 346 | 'tree' => $tree,  | 
            ||
| 347 | ]);  | 
            ||
| 348 | }  | 
            ||
| 349 | |||
| 350 | /**  | 
            ||
| 351 | * @param ServerRequestInterface $request  | 
            ||
| 352 | *  | 
            ||
| 353 | * @return ResponseInterface  | 
            ||
| 354 | */  | 
            ||
| 355 | public function getEmptyAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 356 |     { | 
            ||
| 357 |         $tree = $request->getAttribute('tree'); | 
            ||
| 358 | assert($tree instanceof Tree);  | 
            ||
| 359 | |||
| 360 |         $cart                = Session::get('cart', []); | 
            ||
| 361 | $cart[$tree->name()] = [];  | 
            ||
| 362 |         Session::put('cart', $cart); | 
            ||
| 363 | |||
| 364 |         $url = route('module', [ | 
            ||
| 365 | 'module' => $this->name(),  | 
            ||
| 366 | 'action' => 'Show',  | 
            ||
| 367 | 'tree' => $tree->name(),  | 
            ||
| 368 | ]);  | 
            ||
| 369 | |||
| 370 | return redirect($url);  | 
            ||
| 371 | }  | 
            ||
| 372 | |||
| 373 | /**  | 
            ||
| 374 | * @param ServerRequestInterface $request  | 
            ||
| 375 | *  | 
            ||
| 376 | * @return ResponseInterface  | 
            ||
| 377 | */  | 
            ||
| 378 | public function postRemoveAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 379 |     { | 
            ||
| 380 |         $tree = $request->getAttribute('tree'); | 
            ||
| 381 | assert($tree instanceof Tree);  | 
            ||
| 382 | |||
| 383 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 384 | |||
| 385 |         $cart = Session::get('cart', []); | 
            ||
| 386 | unset($cart[$tree->name()][$xref]);  | 
            ||
| 387 |         Session::put('cart', $cart); | 
            ||
| 388 | |||
| 389 |         $url = route('module', [ | 
            ||
| 390 | 'module' => $this->name(),  | 
            ||
| 391 | 'action' => 'Show',  | 
            ||
| 392 | 'tree' => $tree->name(),  | 
            ||
| 393 | ]);  | 
            ||
| 394 | |||
| 395 | return redirect($url);  | 
            ||
| 396 | }  | 
            ||
| 397 | |||
| 398 | /**  | 
            ||
| 399 | * @param ServerRequestInterface $request  | 
            ||
| 400 | *  | 
            ||
| 401 | * @return ResponseInterface  | 
            ||
| 402 | */  | 
            ||
| 403 | public function getShowAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 404 |     { | 
            ||
| 405 |         $tree = $request->getAttribute('tree'); | 
            ||
| 406 | assert($tree instanceof Tree);  | 
            ||
| 407 | |||
| 408 |         return $this->viewResponse('modules/clippings/show', [ | 
            ||
| 409 | 'records' => $this->allRecordsInCart($tree),  | 
            ||
| 410 |             'title'   => I18N::translate('Family tree clippings cart'), | 
            ||
| 411 | 'tree' => $tree,  | 
            ||
| 412 | ]);  | 
            ||
| 413 | }  | 
            ||
| 414 | |||
| 415 | /**  | 
            ||
| 416 | * @param ServerRequestInterface $request  | 
            ||
| 417 | *  | 
            ||
| 418 | * @return ResponseInterface  | 
            ||
| 419 | */  | 
            ||
| 420 | public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 421 |     { | 
            ||
| 422 |         $tree = $request->getAttribute('tree'); | 
            ||
| 423 | assert($tree instanceof Tree);  | 
            ||
| 424 | |||
| 425 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 426 | |||
| 427 | $family = Family::getInstance($xref, $tree);  | 
            ||
| 428 | |||
| 429 |         if ($family === null) { | 
            ||
| 430 | throw new FamilyNotFoundException();  | 
            ||
| 431 | }  | 
            ||
| 432 | |||
| 433 | $options = $this->familyOptions($family);  | 
            ||
| 434 | |||
| 435 |         $title = I18N::translate('Add %s to the clippings cart', $family->fullName()); | 
            ||
| 436 | |||
| 437 |         return $this->viewResponse('modules/clippings/add-options', [ | 
            ||
| 438 | 'options' => $options,  | 
            ||
| 439 | 'default' => key($options),  | 
            ||
| 440 | 'record' => $family,  | 
            ||
| 441 | 'title' => $title,  | 
            ||
| 442 | 'tree' => $tree,  | 
            ||
| 443 | ]);  | 
            ||
| 444 | }  | 
            ||
| 445 | |||
| 446 | /**  | 
            ||
| 447 | * @param Family $family  | 
            ||
| 448 | *  | 
            ||
| 449 | * @return string[]  | 
            ||
| 450 | */  | 
            ||
| 451 | private function familyOptions(Family $family): array  | 
            ||
| 452 |     { | 
            ||
| 453 | $name = strip_tags($family->fullName());  | 
            ||
| 454 | |||
| 455 | return [  | 
            ||
| 456 | 'parents' => $name,  | 
            ||
| 457 | /* I18N: %s is a family (husband + wife) */  | 
            ||
| 458 |             'members'     => I18N::translate('%s and their children', $name), | 
            ||
| 459 | /* I18N: %s is a family (husband + wife) */  | 
            ||
| 460 |             'descendants' => I18N::translate('%s and their descendants', $name), | 
            ||
| 461 | ];  | 
            ||
| 462 | }  | 
            ||
| 463 | |||
| 464 | /**  | 
            ||
| 465 | * @param ServerRequestInterface $request  | 
            ||
| 466 | *  | 
            ||
| 467 | * @return ResponseInterface  | 
            ||
| 468 | */  | 
            ||
| 469 | public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 470 |     { | 
            ||
| 471 |         $tree = $request->getAttribute('tree'); | 
            ||
| 472 | assert($tree instanceof Tree);  | 
            ||
| 473 | |||
| 474 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 475 | $option = $request->getParsedBody()['option'];  | 
            ||
| 476 | |||
| 477 | $family = Family::getInstance($xref, $tree);  | 
            ||
| 478 | |||
| 479 |         if ($family === null) { | 
            ||
| 480 | throw new FamilyNotFoundException();  | 
            ||
| 481 | }  | 
            ||
| 482 | |||
| 483 |         switch ($option) { | 
            ||
| 484 | case 'parents':  | 
            ||
| 485 | $this->addFamilyToCart($family);  | 
            ||
| 486 | break;  | 
            ||
| 487 | |||
| 488 | case 'members':  | 
            ||
| 489 | $this->addFamilyAndChildrenToCart($family);  | 
            ||
| 490 | break;  | 
            ||
| 491 | |||
| 492 | case 'descendants':  | 
            ||
| 493 | $this->addFamilyAndDescendantsToCart($family);  | 
            ||
| 494 | break;  | 
            ||
| 495 | }  | 
            ||
| 496 | |||
| 497 | return redirect($family->url());  | 
            ||
| 498 | }  | 
            ||
| 499 | |||
| 500 | /**  | 
            ||
| 501 | * @param Family $family  | 
            ||
| 502 | *  | 
            ||
| 503 | * @return void  | 
            ||
| 504 | */  | 
            ||
| 505 | private function addFamilyToCart(Family $family): void  | 
            ||
| 506 |     { | 
            ||
| 507 | $this->addRecordToCart($family);  | 
            ||
| 508 | |||
| 509 |         foreach ($family->spouses() as $spouse) { | 
            ||
| 510 | $this->addRecordToCart($spouse);  | 
            ||
| 511 | }  | 
            ||
| 512 | }  | 
            ||
| 513 | |||
| 514 | /**  | 
            ||
| 515 | * @param Family $family  | 
            ||
| 516 | *  | 
            ||
| 517 | * @return void  | 
            ||
| 518 | */  | 
            ||
| 519 | private function addFamilyAndChildrenToCart(Family $family): void  | 
            ||
| 520 |     { | 
            ||
| 521 | $this->addRecordToCart($family);  | 
            ||
| 522 | |||
| 523 |         foreach ($family->spouses() as $spouse) { | 
            ||
| 524 | $this->addRecordToCart($spouse);  | 
            ||
| 525 | }  | 
            ||
| 526 |         foreach ($family->children() as $child) { | 
            ||
| 527 | $this->addRecordToCart($child);  | 
            ||
| 528 | }  | 
            ||
| 529 | }  | 
            ||
| 530 | |||
| 531 | /**  | 
            ||
| 532 | * @param Family $family  | 
            ||
| 533 | *  | 
            ||
| 534 | * @return void  | 
            ||
| 535 | */  | 
            ||
| 536 | private function addFamilyAndDescendantsToCart(Family $family): void  | 
            ||
| 537 |     { | 
            ||
| 538 | $this->addRecordToCart($family);  | 
            ||
| 539 | |||
| 540 |         foreach ($family->spouses() as $spouse) { | 
            ||
| 541 | $this->addRecordToCart($spouse);  | 
            ||
| 542 | }  | 
            ||
| 543 |         foreach ($family->children() as $child) { | 
            ||
| 544 | $this->addRecordToCart($child);  | 
            ||
| 545 |             foreach ($child->spouseFamilies() as $child_family) { | 
            ||
| 546 | $this->addFamilyAndDescendantsToCart($child_family);  | 
            ||
| 547 | }  | 
            ||
| 548 | }  | 
            ||
| 549 | }  | 
            ||
| 550 | |||
| 551 | /**  | 
            ||
| 552 | * @param ServerRequestInterface $request  | 
            ||
| 553 | *  | 
            ||
| 554 | * @return ResponseInterface  | 
            ||
| 555 | */  | 
            ||
| 556 | public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 557 |     { | 
            ||
| 558 |         $tree = $request->getAttribute('tree'); | 
            ||
| 559 | assert($tree instanceof Tree);  | 
            ||
| 560 | |||
| 561 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 562 | |||
| 563 | $individual = Individual::getInstance($xref, $tree);  | 
            ||
| 564 | |||
| 565 |         if ($individual === null) { | 
            ||
| 566 | throw new IndividualNotFoundException();  | 
            ||
| 567 | }  | 
            ||
| 568 | |||
| 569 | $options = $this->individualOptions($individual);  | 
            ||
| 570 | |||
| 571 |         $title = I18N::translate('Add %s to the clippings cart', $individual->fullName()); | 
            ||
| 572 | |||
| 573 |         return $this->viewResponse('modules/clippings/add-options', [ | 
            ||
| 574 | 'options' => $options,  | 
            ||
| 575 | 'default' => key($options),  | 
            ||
| 576 | 'record' => $individual,  | 
            ||
| 577 | 'title' => $title,  | 
            ||
| 578 | 'tree' => $tree,  | 
            ||
| 579 | ]);  | 
            ||
| 580 | }  | 
            ||
| 581 | |||
| 582 | /**  | 
            ||
| 583 | * @param Individual $individual  | 
            ||
| 584 | *  | 
            ||
| 585 | * @return string[]  | 
            ||
| 586 | */  | 
            ||
| 587 | private function individualOptions(Individual $individual): array  | 
            ||
| 588 |     { | 
            ||
| 589 | $name = strip_tags($individual->fullName());  | 
            ||
| 590 | |||
| 591 |         if ($individual->sex() === 'F') { | 
            ||
| 592 | return [  | 
            ||
| 593 | 'self' => $name,  | 
            ||
| 594 |                 'parents'           => I18N::translate('%s, her parents and siblings', $name), | 
            ||
| 595 |                 'spouses'           => I18N::translate('%s, her spouses and children', $name), | 
            ||
| 596 |                 'ancestors'         => I18N::translate('%s and her ancestors', $name), | 
            ||
| 597 |                 'ancestor_families' => I18N::translate('%s, her ancestors and their families', $name), | 
            ||
| 598 |                 'descendants'       => I18N::translate('%s, her spouses and descendants', $name), | 
            ||
| 599 | ];  | 
            ||
| 600 | }  | 
            ||
| 601 | |||
| 602 | return [  | 
            ||
| 603 | 'self' => $name,  | 
            ||
| 604 |             'parents'           => I18N::translate('%s, his parents and siblings', $name), | 
            ||
| 605 |             'spouses'           => I18N::translate('%s, his spouses and children', $name), | 
            ||
| 606 |             'ancestors'         => I18N::translate('%s and his ancestors', $name), | 
            ||
| 607 |             'ancestor_families' => I18N::translate('%s, his ancestors and their families', $name), | 
            ||
| 608 |             'descendants'       => I18N::translate('%s, his spouses and descendants', $name), | 
            ||
| 609 | ];  | 
            ||
| 610 | }  | 
            ||
| 611 | |||
| 612 | /**  | 
            ||
| 613 | * @param ServerRequestInterface $request  | 
            ||
| 614 | *  | 
            ||
| 615 | * @return ResponseInterface  | 
            ||
| 616 | */  | 
            ||
| 617 | public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 618 |     { | 
            ||
| 619 |         $tree = $request->getAttribute('tree'); | 
            ||
| 620 | assert($tree instanceof Tree);  | 
            ||
| 621 | |||
| 622 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 623 | $option = $request->getParsedBody()['option'];  | 
            ||
| 624 | |||
| 625 | $individual = Individual::getInstance($xref, $tree);  | 
            ||
| 626 | |||
| 627 |         if ($individual === null) { | 
            ||
| 628 | throw new IndividualNotFoundException();  | 
            ||
| 629 | }  | 
            ||
| 630 | |||
| 631 |         switch ($option) { | 
            ||
| 632 | case 'self':  | 
            ||
| 633 | $this->addRecordToCart($individual);  | 
            ||
| 634 | break;  | 
            ||
| 635 | |||
| 636 | case 'parents':  | 
            ||
| 637 |                 foreach ($individual->childFamilies() as $family) { | 
            ||
| 638 | $this->addFamilyAndChildrenToCart($family);  | 
            ||
| 639 | }  | 
            ||
| 640 | break;  | 
            ||
| 641 | |||
| 642 | case 'spouses':  | 
            ||
| 643 |                 foreach ($individual->spouseFamilies() as $family) { | 
            ||
| 644 | $this->addFamilyAndChildrenToCart($family);  | 
            ||
| 645 | }  | 
            ||
| 646 | break;  | 
            ||
| 647 | |||
| 648 | case 'ancestors':  | 
            ||
| 649 | $this->addAncestorsToCart($individual);  | 
            ||
| 650 | break;  | 
            ||
| 651 | |||
| 652 | case 'ancestor_families':  | 
            ||
| 653 | $this->addAncestorFamiliesToCart($individual);  | 
            ||
| 654 | break;  | 
            ||
| 655 | |||
| 656 | case 'descendants':  | 
            ||
| 657 |                 foreach ($individual->spouseFamilies() as $family) { | 
            ||
| 658 | $this->addFamilyAndDescendantsToCart($family);  | 
            ||
| 659 | }  | 
            ||
| 660 | break;  | 
            ||
| 661 | }  | 
            ||
| 662 | |||
| 663 | return redirect($individual->url());  | 
            ||
| 664 | }  | 
            ||
| 665 | |||
| 666 | /**  | 
            ||
| 667 | * @param Individual $individual  | 
            ||
| 668 | *  | 
            ||
| 669 | * @return void  | 
            ||
| 670 | */  | 
            ||
| 671 | private function addAncestorsToCart(Individual $individual): void  | 
            ||
| 672 |     { | 
            ||
| 673 | $this->addRecordToCart($individual);  | 
            ||
| 674 | |||
| 675 |         foreach ($individual->childFamilies() as $family) { | 
            ||
| 676 |             foreach ($family->spouses() as $parent) { | 
            ||
| 677 | $this->addAncestorsToCart($parent);  | 
            ||
| 678 | }  | 
            ||
| 679 | }  | 
            ||
| 680 | }  | 
            ||
| 681 | |||
| 682 | /**  | 
            ||
| 683 | * @param Individual $individual  | 
            ||
| 684 | *  | 
            ||
| 685 | * @return void  | 
            ||
| 686 | */  | 
            ||
| 687 | private function addAncestorFamiliesToCart(Individual $individual): void  | 
            ||
| 688 |     { | 
            ||
| 689 |         foreach ($individual->childFamilies() as $family) { | 
            ||
| 690 | $this->addFamilyAndChildrenToCart($family);  | 
            ||
| 691 |             foreach ($family->spouses() as $parent) { | 
            ||
| 692 | $this->addAncestorsToCart($parent);  | 
            ||
| 693 | }  | 
            ||
| 694 | }  | 
            ||
| 695 | }  | 
            ||
| 696 | |||
| 697 | /**  | 
            ||
| 698 | * @param ServerRequestInterface $request  | 
            ||
| 699 | *  | 
            ||
| 700 | * @return ResponseInterface  | 
            ||
| 701 | */  | 
            ||
| 702 | public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 703 |     { | 
            ||
| 704 |         $tree = $request->getAttribute('tree'); | 
            ||
| 705 | assert($tree instanceof Tree);  | 
            ||
| 706 | |||
| 707 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 708 | |||
| 709 | $media = Media::getInstance($xref, $tree);  | 
            ||
| 710 | |||
| 711 |         if ($media === null) { | 
            ||
| 712 | throw new MediaNotFoundException();  | 
            ||
| 713 | }  | 
            ||
| 714 | |||
| 715 | $options = $this->mediaOptions($media);  | 
            ||
| 716 | |||
| 717 |         $title = I18N::translate('Add %s to the clippings cart', $media->fullName()); | 
            ||
| 718 | |||
| 719 |         return $this->viewResponse('modules/clippings/add-options', [ | 
            ||
| 720 | 'options' => $options,  | 
            ||
| 721 | 'default' => key($options),  | 
            ||
| 722 | 'record' => $media,  | 
            ||
| 723 | 'title' => $title,  | 
            ||
| 724 | 'tree' => $tree,  | 
            ||
| 725 | ]);  | 
            ||
| 726 | }  | 
            ||
| 727 | |||
| 728 | /**  | 
            ||
| 729 | * @param Media $media  | 
            ||
| 730 | *  | 
            ||
| 731 | * @return string[]  | 
            ||
| 732 | */  | 
            ||
| 733 | private function mediaOptions(Media $media): array  | 
            ||
| 734 |     { | 
            ||
| 735 | $name = strip_tags($media->fullName());  | 
            ||
| 736 | |||
| 737 | return [  | 
            ||
| 738 | 'self' => $name,  | 
            ||
| 739 | ];  | 
            ||
| 740 | }  | 
            ||
| 741 | |||
| 742 | /**  | 
            ||
| 743 | * @param ServerRequestInterface $request  | 
            ||
| 744 | *  | 
            ||
| 745 | * @return ResponseInterface  | 
            ||
| 746 | */  | 
            ||
| 747 | public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 748 |     { | 
            ||
| 749 |         $tree = $request->getAttribute('tree'); | 
            ||
| 750 | assert($tree instanceof Tree);  | 
            ||
| 751 | |||
| 752 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 753 | |||
| 754 | $media = Media::getInstance($xref, $tree);  | 
            ||
| 755 | |||
| 756 |         if ($media === null) { | 
            ||
| 757 | throw new MediaNotFoundException();  | 
            ||
| 758 | }  | 
            ||
| 759 | |||
| 760 | $this->addRecordToCart($media);  | 
            ||
| 761 | |||
| 762 | return redirect($media->url());  | 
            ||
| 763 | }  | 
            ||
| 764 | |||
| 765 | /**  | 
            ||
| 766 | * @param ServerRequestInterface $request  | 
            ||
| 767 | *  | 
            ||
| 768 | * @return ResponseInterface  | 
            ||
| 769 | */  | 
            ||
| 770 | public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 793 | ]);  | 
            ||
| 794 | }  | 
            ||
| 795 | |||
| 796 | /**  | 
            ||
| 797 | * @param Note $note  | 
            ||
| 798 | *  | 
            ||
| 799 | * @return string[]  | 
            ||
| 800 | */  | 
            ||
| 801 | private function noteOptions(Note $note): array  | 
            ||
| 802 |     { | 
            ||
| 803 | $name = strip_tags($note->fullName());  | 
            ||
| 804 | |||
| 805 | return [  | 
            ||
| 806 | 'self' => $name,  | 
            ||
| 807 | ];  | 
            ||
| 808 | }  | 
            ||
| 809 | |||
| 810 | /**  | 
            ||
| 811 | * @param ServerRequestInterface $request  | 
            ||
| 812 | *  | 
            ||
| 813 | * @return ResponseInterface  | 
            ||
| 814 | */  | 
            ||
| 815 | public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 816 |     { | 
            ||
| 817 |         $tree = $request->getAttribute('tree'); | 
            ||
| 818 | assert($tree instanceof Tree);  | 
            ||
| 819 | |||
| 820 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 821 | |||
| 822 | $note = Note::getInstance($xref, $tree);  | 
            ||
| 823 | |||
| 824 |         if ($note === null) { | 
            ||
| 825 | throw new NoteNotFoundException();  | 
            ||
| 826 | }  | 
            ||
| 827 | |||
| 828 | $this->addRecordToCart($note);  | 
            ||
| 829 | |||
| 830 | return redirect($note->url());  | 
            ||
| 831 | }  | 
            ||
| 832 | |||
| 833 | /**  | 
            ||
| 834 | * @param ServerRequestInterface $request  | 
            ||
| 835 | *  | 
            ||
| 836 | * @return ResponseInterface  | 
            ||
| 837 | */  | 
            ||
| 838 | public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 861 | ]);  | 
            ||
| 862 | }  | 
            ||
| 863 | |||
| 864 | /**  | 
            ||
| 865 | * @param Repository $repository  | 
            ||
| 866 | *  | 
            ||
| 867 | * @return string[]  | 
            ||
| 868 | */  | 
            ||
| 869 | private function repositoryOptions(Repository $repository): array  | 
            ||
| 870 |     { | 
            ||
| 871 | $name = strip_tags($repository->fullName());  | 
            ||
| 872 | |||
| 873 | return [  | 
            ||
| 874 | 'self' => $name,  | 
            ||
| 875 | ];  | 
            ||
| 876 | }  | 
            ||
| 877 | |||
| 878 | /**  | 
            ||
| 879 | * @param ServerRequestInterface $request  | 
            ||
| 880 | *  | 
            ||
| 881 | * @return ResponseInterface  | 
            ||
| 882 | */  | 
            ||
| 883 | public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 884 |     { | 
            ||
| 885 |         $tree = $request->getAttribute('tree'); | 
            ||
| 886 | assert($tree instanceof Tree);  | 
            ||
| 887 | |||
| 888 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 889 | |||
| 890 | $repository = Repository::getInstance($xref, $tree);  | 
            ||
| 891 | |||
| 892 |         if ($repository === null) { | 
            ||
| 893 | throw new RepositoryNotFoundException();  | 
            ||
| 894 | }  | 
            ||
| 895 | |||
| 896 | $this->addRecordToCart($repository);  | 
            ||
| 897 | |||
| 898 | return redirect($repository->url());  | 
            ||
| 899 | }  | 
            ||
| 900 | |||
| 901 | /**  | 
            ||
| 902 | * @param ServerRequestInterface $request  | 
            ||
| 903 | *  | 
            ||
| 904 | * @return ResponseInterface  | 
            ||
| 905 | */  | 
            ||
| 906 | public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 907 |     { | 
            ||
| 908 |         $tree = $request->getAttribute('tree'); | 
            ||
| 909 | assert($tree instanceof Tree);  | 
            ||
| 910 | |||
| 911 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 912 | |||
| 913 | $source = Source::getInstance($xref, $tree);  | 
            ||
| 914 | |||
| 915 |         if ($source === null) { | 
            ||
| 916 | throw new SourceNotFoundException();  | 
            ||
| 917 | }  | 
            ||
| 918 | |||
| 919 | $options = $this->sourceOptions($source);  | 
            ||
| 920 | |||
| 921 |         $title = I18N::translate('Add %s to the clippings cart', $source->fullName()); | 
            ||
| 922 | |||
| 923 |         return $this->viewResponse('modules/clippings/add-options', [ | 
            ||
| 924 | 'options' => $options,  | 
            ||
| 925 | 'default' => key($options),  | 
            ||
| 926 | 'record' => $source,  | 
            ||
| 927 | 'title' => $title,  | 
            ||
| 928 | 'tree' => $tree,  | 
            ||
| 929 | ]);  | 
            ||
| 930 | }  | 
            ||
| 931 | |||
| 932 | /**  | 
            ||
| 933 | * @param Source $source  | 
            ||
| 934 | *  | 
            ||
| 935 | * @return string[]  | 
            ||
| 936 | */  | 
            ||
| 937 | private function sourceOptions(Source $source): array  | 
            ||
| 938 |     { | 
            ||
| 939 | $name = strip_tags($source->fullName());  | 
            ||
| 940 | |||
| 941 | return [  | 
            ||
| 942 | 'only' => strip_tags($source->fullName()),  | 
            ||
| 943 |             'linked' => I18N::translate('%s and the individuals that reference it.', $name), | 
            ||
| 944 | ];  | 
            ||
| 945 | }  | 
            ||
| 946 | |||
| 947 | /**  | 
            ||
| 948 | * @param ServerRequestInterface $request  | 
            ||
| 949 | *  | 
            ||
| 950 | * @return ResponseInterface  | 
            ||
| 951 | */  | 
            ||
| 952 | public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface  | 
            ||
| 953 |     { | 
            ||
| 954 |         $tree = $request->getAttribute('tree'); | 
            ||
| 955 | assert($tree instanceof Tree);  | 
            ||
| 956 | |||
| 957 | $xref = $request->getQueryParams()['xref'];  | 
            ||
| 958 | $option = $request->getParsedBody()['option'];  | 
            ||
| 959 | |||
| 960 | $source = Source::getInstance($xref, $tree);  | 
            ||
| 961 | |||
| 962 |         if ($source === null) { | 
            ||
| 963 | throw new SourceNotFoundException();  | 
            ||
| 964 | }  | 
            ||
| 965 | |||
| 966 | $this->addRecordToCart($source);  | 
            ||
| 967 | |||
| 968 |         if ($option === 'linked') { | 
            ||
| 969 |             foreach ($source->linkedIndividuals('SOUR') as $individual) { | 
            ||
| 970 | $this->addRecordToCart($individual);  | 
            ||
| 971 | }  | 
            ||
| 972 |             foreach ($source->linkedFamilies('SOUR') as $family) { | 
            ||
| 973 | $this->addRecordToCart($family);  | 
            ||
| 974 | }  | 
            ||
| 975 | }  | 
            ||
| 976 | |||
| 977 | return redirect($source->url());  | 
            ||
| 978 | }  | 
            ||
| 979 | |||
| 980 | /**  | 
            ||
| 981 | * Get all the records in the cart.  | 
            ||
| 982 | *  | 
            ||
| 983 | * @param Tree $tree  | 
            ||
| 984 | *  | 
            ||
| 985 | * @return GedcomRecord[]  | 
            ||
| 986 | */  | 
            ||
| 987 | private function allRecordsInCart(Tree $tree): array  | 
            ||
| 988 |     { | 
            ||
| 989 |         $cart = Session::get('cart', []); | 
            ||
| 990 | |||
| 991 | $xrefs = array_keys($cart[$tree->name()] ?? []);  | 
            ||
| 992 | |||
| 993 | // Fetch all the records in the cart.  | 
            ||
| 994 |         $records = array_map(static function (string $xref) use ($tree): ?GedcomRecord { | 
            ||
| 995 | return GedcomRecord::getInstance($xref, $tree);  | 
            ||
| 996 | }, $xrefs);  | 
            ||
| 997 | |||
| 998 | // Some records may have been deleted after they were added to the cart.  | 
            ||
| 999 | $records = array_filter($records);  | 
            ||
| 1000 | |||
| 1001 | // Group and sort.  | 
            ||
| 1002 |         uasort($records, static function (GedcomRecord $x, GedcomRecord $y): int { | 
            ||
| 1003 | return $x::RECORD_TYPE <=> $y::RECORD_TYPE ?: GedcomRecord::nameComparator()($x, $y);  | 
            ||
| 1004 | });  | 
            ||
| 1005 | |||
| 1006 | return $records;  | 
            ||
| 1007 | }  | 
            ||
| 1008 | |||
| 1009 | /**  | 
            ||
| 1010 | * Add a record (and direclty linked sources, notes, etc. to the cart.  | 
            ||
| 1011 | *  | 
            ||
| 1012 | * @param GedcomRecord $record  | 
            ||
| 1013 | *  | 
            ||
| 1014 | * @return void  | 
            ||
| 1015 | */  | 
            ||
| 1016 | private function addRecordToCart(GedcomRecord $record): void  | 
            ||
| 1033 | }  | 
            ||
| 1034 | |||
| 1035 | /**  | 
            ||
| 1036 | * @param Tree $tree  | 
            ||
| 1037 | *  | 
            ||
| 1038 | * @return bool  | 
            ||
| 1039 | */  | 
            ||
| 1040 | private function isCartEmpty(Tree $tree): bool  | 
            ||
| 1041 |     { | 
            ||
| 1042 |         $cart     = Session::get('cart', []); | 
            ||
| 1043 | $contents = $cart[$tree->name()] ?? [];  | 
            ||
| 1044 | |||
| 1045 | return $contents === [];  | 
            ||
| 1046 | }  | 
            ||
| 1047 | }  | 
            ||
| 1048 |