1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Services; 19 20use Closure; 21use Fisharebest\Webtrees\Auth; 22use Fisharebest\Webtrees\Contracts\UserInterface; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Module\AhnentafelReportModule; 25use Fisharebest\Webtrees\Module\AlbumModule; 26use Fisharebest\Webtrees\Module\AncestorsChartModule; 27use Fisharebest\Webtrees\Module\BatchUpdateModule; 28use Fisharebest\Webtrees\Module\BingWebmasterToolsModule; 29use Fisharebest\Webtrees\Module\BirthDeathMarriageReportModule; 30use Fisharebest\Webtrees\Module\BirthReportModule; 31use Fisharebest\Webtrees\Module\BranchesListModule; 32use Fisharebest\Webtrees\Module\BritishMonarchs; 33use Fisharebest\Webtrees\Module\BritishPrimeMinisters; 34use Fisharebest\Webtrees\Module\BritishSocialHistory; 35use Fisharebest\Webtrees\Module\CalendarMenuModule; 36use Fisharebest\Webtrees\Module\CemeteryReportModule; 37use Fisharebest\Webtrees\Module\CensusAssistantModule; 38use Fisharebest\Webtrees\Module\ChangeReportModule; 39use Fisharebest\Webtrees\Module\ChartsBlockModule; 40use Fisharebest\Webtrees\Module\ChartsMenuModule; 41use Fisharebest\Webtrees\Module\CkeditorModule; 42use Fisharebest\Webtrees\Module\ClippingsCartModule; 43use Fisharebest\Webtrees\Module\CloudsTheme; 44use Fisharebest\Webtrees\Module\ColorsTheme; 45use Fisharebest\Webtrees\Module\CompactTreeChartModule; 46use Fisharebest\Webtrees\Module\ContactsFooterModule; 47use Fisharebest\Webtrees\Module\CookieWarningModule; 48use Fisharebest\Webtrees\Module\CustomCssJsModule; 49use Fisharebest\Webtrees\Module\DeathReportModule; 50use Fisharebest\Webtrees\Module\DescendancyChartModule; 51use Fisharebest\Webtrees\Module\DescendancyModule; 52use Fisharebest\Webtrees\Module\DescendancyReportModule; 53use Fisharebest\Webtrees\Module\FabTheme; 54use Fisharebest\Webtrees\Module\FactSourcesReportModule; 55use Fisharebest\Webtrees\Module\FamilyBookChartModule; 56use Fisharebest\Webtrees\Module\FamilyGroupReportModule; 57use Fisharebest\Webtrees\Module\FamilyListModule; 58use Fisharebest\Webtrees\Module\FamilyNavigatorModule; 59use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; 60use Fisharebest\Webtrees\Module\FamilyTreeNewsModule; 61use Fisharebest\Webtrees\Module\FamilyTreeStatisticsModule; 62use Fisharebest\Webtrees\Module\FanChartModule; 63use Fisharebest\Webtrees\Module\FrequentlyAskedQuestionsModule; 64use Fisharebest\Webtrees\Module\GoogleAnalyticsModule; 65use Fisharebest\Webtrees\Module\GoogleWebmasterToolsModule; 66use Fisharebest\Webtrees\Module\HitCountFooterModule; 67use Fisharebest\Webtrees\Module\HourglassChartModule; 68use Fisharebest\Webtrees\Module\HtmlBlockModule; 69use Fisharebest\Webtrees\Module\IndividualFactsTabModule; 70use Fisharebest\Webtrees\Module\IndividualFamiliesReportModule; 71use Fisharebest\Webtrees\Module\IndividualListModule; 72use Fisharebest\Webtrees\Module\IndividualMetadataModule; 73use Fisharebest\Webtrees\Module\IndividualReportModule; 74use Fisharebest\Webtrees\Module\InteractiveTreeModule; 75use Fisharebest\Webtrees\Module\LifespansChartModule; 76use Fisharebest\Webtrees\Module\ListsMenuModule; 77use Fisharebest\Webtrees\Module\LoggedInUsersModule; 78use Fisharebest\Webtrees\Module\LoginBlockModule; 79use Fisharebest\Webtrees\Module\MarriageReportModule; 80use Fisharebest\Webtrees\Module\MatomoAnalyticsModule; 81use Fisharebest\Webtrees\Module\MediaListModule; 82use Fisharebest\Webtrees\Module\MediaTabModule; 83use Fisharebest\Webtrees\Module\MinimalTheme; 84use Fisharebest\Webtrees\Module\MissingFactsReportModule; 85use Fisharebest\Webtrees\Module\ModuleAnalyticsInterface; 86use Fisharebest\Webtrees\Module\ModuleBlockInterface; 87use Fisharebest\Webtrees\Module\ModuleChartInterface; 88use Fisharebest\Webtrees\Module\ModuleConfigInterface; 89use Fisharebest\Webtrees\Module\ModuleCustomInterface; 90use Fisharebest\Webtrees\Module\ModuleFooterInterface; 91use Fisharebest\Webtrees\Module\ModuleHistoricEventsInterface; 92use Fisharebest\Webtrees\Module\ModuleInterface; 93use Fisharebest\Webtrees\Module\ModuleLanguageInterface; 94use Fisharebest\Webtrees\Module\ModuleListInterface; 95use Fisharebest\Webtrees\Module\ModuleMenuInterface; 96use Fisharebest\Webtrees\Module\ModuleReportInterface; 97use Fisharebest\Webtrees\Module\ModuleSidebarInterface; 98use Fisharebest\Webtrees\Module\ModuleTabInterface; 99use Fisharebest\Webtrees\Module\ModuleThemeInterface; 100use Fisharebest\Webtrees\Module\NoteListModule; 101use Fisharebest\Webtrees\Module\NotesTabModule; 102use Fisharebest\Webtrees\Module\OccupationReportModule; 103use Fisharebest\Webtrees\Module\OnThisDayModule; 104use Fisharebest\Webtrees\Module\PedigreeChartModule; 105use Fisharebest\Webtrees\Module\PedigreeMapModule; 106use Fisharebest\Webtrees\Module\PedigreeReportModule; 107use Fisharebest\Webtrees\Module\PlaceHierarchyListModule; 108use Fisharebest\Webtrees\Module\PlacesModule; 109use Fisharebest\Webtrees\Module\PoweredByWebtreesModule; 110use Fisharebest\Webtrees\Module\RecentChangesModule; 111use Fisharebest\Webtrees\Module\RelatedIndividualsReportModule; 112use Fisharebest\Webtrees\Module\RelationshipsChartModule; 113use Fisharebest\Webtrees\Module\RelativesTabModule; 114use Fisharebest\Webtrees\Module\ReportsMenuModule; 115use Fisharebest\Webtrees\Module\RepositoryListModule; 116use Fisharebest\Webtrees\Module\ResearchTaskModule; 117use Fisharebest\Webtrees\Module\ReviewChangesModule; 118use Fisharebest\Webtrees\Module\SearchMenuModule; 119use Fisharebest\Webtrees\Module\SiteMapModule; 120use Fisharebest\Webtrees\Module\SlideShowModule; 121use Fisharebest\Webtrees\Module\SourceListModule; 122use Fisharebest\Webtrees\Module\SourcesTabModule; 123use Fisharebest\Webtrees\Module\StatcounterModule; 124use Fisharebest\Webtrees\Module\StatisticsChartModule; 125use Fisharebest\Webtrees\Module\StoriesModule; 126use Fisharebest\Webtrees\Module\ThemeSelectModule; 127use Fisharebest\Webtrees\Module\TimelineChartModule; 128use Fisharebest\Webtrees\Module\TopGivenNamesModule; 129use Fisharebest\Webtrees\Module\TopPageViewsModule; 130use Fisharebest\Webtrees\Module\TopSurnamesModule; 131use Fisharebest\Webtrees\Module\TreesMenuModule; 132use Fisharebest\Webtrees\Module\UpcomingAnniversariesModule; 133use Fisharebest\Webtrees\Module\UserFavoritesModule; 134use Fisharebest\Webtrees\Module\UserJournalModule; 135use Fisharebest\Webtrees\Module\UserMessagesModule; 136use Fisharebest\Webtrees\Module\UserWelcomeModule; 137use Fisharebest\Webtrees\Module\USPresidents; 138use Fisharebest\Webtrees\Module\WebtreesTheme; 139use Fisharebest\Webtrees\Module\WelcomeBlockModule; 140use Fisharebest\Webtrees\Module\XeneaTheme; 141use Fisharebest\Webtrees\Module\YahrzeitModule; 142use Fisharebest\Webtrees\Tree; 143use Fisharebest\Webtrees\Webtrees; 144use Illuminate\Database\Capsule\Manager as DB; 145use Illuminate\Support\Collection; 146use Illuminate\Support\Str; 147use stdClass; 148use Throwable; 149 150/** 151 * Functions for managing and maintaining modules. 152 */ 153class ModuleService 154{ 155 // Components are pieces of user-facing functionality, are managed together in the control panel. 156 private const COMPONENTS = [ 157 ModuleAnalyticsInterface::class, 158 ModuleBlockInterface::class, 159 ModuleChartInterface::class, 160 ModuleFooterInterface::class, 161 ModuleHistoricEventsInterface::class, 162 ModuleLanguageInterface::class, 163 ModuleListInterface::class, 164 ModuleMenuInterface::class, 165 ModuleReportInterface::class, 166 ModuleSidebarInterface::class, 167 ModuleTabInterface::class, 168 ModuleThemeInterface::class, 169 ]; 170 171 // Array keys are module names, and should match module names from earlier versions of webtrees. 172 private const CORE_MODULES = [ 173 'GEDFact_assistant' => CensusAssistantModule::class, 174 'ahnentafel_report' => AhnentafelReportModule::class, 175 'ancestors_chart' => AncestorsChartModule::class, 176 'batch_update' => BatchUpdateModule::class, 177 'bdm_report' => BirthDeathMarriageReportModule::class, 178 'bing-webmaster-tools' => BingWebmasterToolsModule::class, 179 'birth_report' => BirthReportModule::class, 180 'branches_list' => BranchesListModule::class, 181 'british-monarchs' => BritishMonarchs::class, 182 'british-prime-ministers' => BritishPrimeMinisters::class, 183 'british-social-history' => BritishSocialHistory::class, 184 'calendar-menu' => CalendarMenuModule::class, 185 'cemetery_report' => CemeteryReportModule::class, 186 'change_report' => ChangeReportModule::class, 187 'charts' => ChartsBlockModule::class, 188 'charts-menu' => ChartsMenuModule::class, 189 'ckeditor' => CkeditorModule::class, 190 'clippings' => ClippingsCartModule::class, 191 'clouds' => CloudsTheme::class, 192 'colors' => ColorsTheme::class, 193 'compact-chart' => CompactTreeChartModule::class, 194 'contact-links' => ContactsFooterModule::class, 195 'cookie-warning' => CookieWarningModule::class, 196 'custom-css-js' => CustomCssJsModule::class, 197 'death_report' => DeathReportModule::class, 198 'descendancy' => DescendancyModule::class, 199 'descendancy_chart' => DescendancyChartModule::class, 200 'descendancy_report' => DescendancyReportModule::class, 201 'extra_info' => IndividualMetadataModule::class, 202 'fab' => FabTheme::class, 203 'fact_sources' => FactSourcesReportModule::class, 204 'family_book_chart' => FamilyBookChartModule::class, 205 'family_group_report' => FamilyGroupReportModule::class, 206 'family_list' => FamilyListModule::class, 207 'family_nav' => FamilyNavigatorModule::class, 208 'fan_chart' => FanChartModule::class, 209 'faq' => FrequentlyAskedQuestionsModule::class, 210 'gedcom_block' => WelcomeBlockModule::class, 211 'gedcom_favorites' => FamilyTreeFavoritesModule::class, 212 'gedcom_news' => FamilyTreeNewsModule::class, 213 'gedcom_stats' => FamilyTreeStatisticsModule::class, 214 'google-analytics' => GoogleAnalyticsModule::class, 215 'google-webmaster-tools' => GoogleWebmasterToolsModule::class, 216 'hit-counter' => HitCountFooterModule::class, 217 'hourglass_chart' => HourglassChartModule::class, 218 'html' => HtmlBlockModule::class, 219 'individual_ext_report' => IndividualFamiliesReportModule::class, 220 'individual_list' => IndividualListModule::class, 221 'individual_report' => IndividualReportModule::class, 222 'lifespans_chart' => LifespansChartModule::class, 223 'lightbox' => AlbumModule::class, 224 'lists-menu' => ListsMenuModule::class, 225 'logged_in' => LoggedInUsersModule::class, 226 'login_block' => LoginBlockModule::class, 227 'marriage_report' => MarriageReportModule::class, 228 'matomo-analytics' => MatomoAnalyticsModule::class, 229 'media' => MediaTabModule::class, 230 'media_list' => MediaListModule::class, 231 'minimal' => MinimalTheme::class, 232 'missing_facts_report' => MissingFactsReportModule::class, 233 'notes' => NotesTabModule::class, 234 'note_list' => NoteListModule::class, 235 'occupation_report' => OccupationReportModule::class, 236 'pedigree-map' => PedigreeMapModule::class, 237 'pedigree_chart' => PedigreeChartModule::class, 238 'pedigree_report' => PedigreeReportModule::class, 239 'personal_facts' => IndividualFactsTabModule::class, 240 'places' => PlacesModule::class, 241 'places_list' => PlaceHierarchyListModule::class, 242 'powered-by-webtrees' => PoweredByWebtreesModule::class, 243 'random_media' => SlideShowModule::class, 244 'recent_changes' => RecentChangesModule::class, 245 'relationships_chart' => RelationshipsChartModule::class, 246 'relative_ext_report' => RelatedIndividualsReportModule::class, 247 'relatives' => RelativesTabModule::class, 248 'reports-menu' => ReportsMenuModule::class, 249 'repository_list' => RepositoryListModule::class, 250 'review_changes' => ReviewChangesModule::class, 251 'search-menu' => SearchMenuModule::class, 252 'sitemap' => SiteMapModule::class, 253 'source_list' => SourceListModule::class, 254 'sources_tab' => SourcesTabModule::class, 255 'statcounter' => StatcounterModule::class, 256 'statistics_chart' => StatisticsChartModule::class, 257 'stories' => StoriesModule::class, 258 'theme_select' => ThemeSelectModule::class, 259 'timeline_chart' => TimelineChartModule::class, 260 'todays_events' => OnThisDayModule::class, 261 'todo' => ResearchTaskModule::class, 262 'top10_givnnames' => TopGivenNamesModule::class, 263 'top10_pageviews' => TopPageViewsModule::class, 264 'top10_surnames' => TopSurnamesModule::class, 265 'tree' => InteractiveTreeModule::class, 266 'trees-menu' => TreesMenuModule::class, 267 'upcoming_events' => UpcomingAnniversariesModule::class, 268 'us-presidents' => USPresidents::class, 269 'user_blog' => UserJournalModule::class, 270 'user_favorites' => UserFavoritesModule::class, 271 'user_messages' => UserMessagesModule::class, 272 'user_welcome' => UserWelcomeModule::class, 273 'webtrees' => WebtreesTheme::class, 274 'xenea' => XeneaTheme::class, 275 'yahrzeit' => YahrzeitModule::class, 276 ]; 277 278 /** 279 * A function to convert modules into their titles - to create option lists, etc. 280 * 281 * @return Closure 282 */ 283 public function titleMapper(): Closure 284 { 285 return function (ModuleInterface $module): string { 286 return $module->title(); 287 }; 288 } 289 290 /** 291 * Modules which (a) provide a specific function and (b) we have permission to see. 292 * 293 * @param string $interface 294 * @param Tree $tree 295 * @param UserInterface $user 296 * 297 * @return Collection 298 * @return ModuleInterface[] 299 */ 300 public function findByComponent(string $interface, Tree $tree, UserInterface $user): Collection 301 { 302 return $this->findByInterface($interface, false, true) 303 ->filter(function (ModuleInterface $module) use ($interface, $tree, $user): bool { 304 return $module->accessLevel($tree, $interface) >= Auth::accessLevel($tree, $user); 305 }); 306 } 307 308 /** 309 * All modules which provide a specific function. 310 * 311 * @param string $interface 312 * @param bool $include_disabled 313 * @param bool $sort 314 * 315 * @return Collection 316 * @return ModuleInterface[] 317 */ 318 public function findByInterface(string $interface, $include_disabled = false, $sort = false): Collection 319 { 320 $modules = $this->all($include_disabled) 321 ->filter($this->interfaceFilter($interface)); 322 323 switch ($interface) { 324 case ModuleFooterInterface::class: 325 return $modules->sort($this->footerSorter()); 326 327 case ModuleMenuInterface::class: 328 return $modules->sort($this->menuSorter()); 329 330 case ModuleSidebarInterface::class: 331 return $modules->sort($this->sidebarSorter()); 332 333 case ModuleTabInterface::class: 334 return $modules->sort($this->tabSorter()); 335 336 default: 337 if ($sort) { 338 return $modules->sort($this->moduleSorter()); 339 } 340 341 return $modules; 342 } 343 } 344 345 /** 346 * All modules. 347 * 348 * @param bool $include_disabled 349 * 350 * @return Collection 351 * @return ModuleInterface[] 352 */ 353 public function all(bool $include_disabled = false): Collection 354 { 355 return app('cache.array')->rememberForever('all_modules', function (): Collection { 356 // Modules have a default status, order etc. 357 // We can override these from database settings. 358 $module_info = DB::table('module') 359 ->get() 360 ->mapWithKeys(function (stdClass $row): array { 361 return [$row->module_name => $row]; 362 }); 363 364 return $this->coreModules() 365 ->merge($this->customModules()) 366 ->map(function (ModuleInterface $module) use ($module_info): ModuleInterface { 367 $info = $module_info->get($module->name()); 368 369 if ($info instanceof stdClass) { 370 $module->setEnabled($info->status === 'enabled'); 371 372 if ($module instanceof ModuleFooterInterface && $info->footer_order !== null) { 373 $module->setFooterOrder((int) $info->footer_order); 374 } 375 376 if ($module instanceof ModuleMenuInterface && $info->menu_order !== null) { 377 $module->setMenuOrder((int) $info->menu_order); 378 } 379 380 if ($module instanceof ModuleSidebarInterface && $info->sidebar_order !== null) { 381 $module->setSidebarOrder((int) $info->sidebar_order); 382 } 383 384 if ($module instanceof ModuleTabInterface && $info->tab_order !== null) { 385 $module->setTabOrder((int) $info->tab_order); 386 } 387 } else { 388 $module->setEnabled($module->isEnabledByDefault()); 389 390 DB::table('module')->insert([ 391 'module_name' => $module->name(), 392 'status' => $module->isEnabled() ? 'enabled' : 'disabled', 393 ]); 394 } 395 396 return $module; 397 }); 398 })->filter($this->enabledFilter($include_disabled)); 399 } 400 401 /** 402 * All core modules in the system. 403 * 404 * @return Collection 405 */ 406 private function coreModules(): Collection 407 { 408 return Collection::make(self::CORE_MODULES) 409 ->map(function (string $class, string $name): ModuleInterface { 410 $module = app($class); 411 412 $module->setName($name); 413 414 return $module; 415 }); 416 } 417 418 /** 419 * All custom modules in the system. Custom modules are defined in modules_v4/ 420 * 421 * @return Collection 422 */ 423 private function customModules(): Collection 424 { 425 $pattern = WT_ROOT . Webtrees::MODULES_PATH . '*/module.php'; 426 $filenames = glob($pattern, GLOB_NOSORT); 427 428 return Collection::make($filenames) 429 ->filter(function (string $filename): bool { 430 // Special characters will break PHP variable names. 431 // This also allows us to ignore modules called "foo.example" and "foo.disable" 432 $module_name = basename(dirname($filename)); 433 434 return !Str::contains($module_name, ['.', ' ', '[', ']']) && Str::length($module_name) <= 30; 435 }) 436 ->map(function (string $filename): ?ModuleCustomInterface { 437 try { 438 $module = self::load($filename); 439 440 if ($module instanceof ModuleCustomInterface) { 441 $module_name = '_' . basename(dirname($filename)) . '_'; 442 443 $module->setName($module_name); 444 } else { 445 return null; 446 } 447 448 return $module; 449 } catch (Throwable $ex) { 450 // It would be nice to show this error in a flash-message or similar, but the framework 451 // has not yet been initialised so we have no themes, languages, sessions, etc. 452 throw $ex; 453 } 454 }) 455 ->filter() 456 ->mapWithKeys(function (ModuleCustomInterface $module): array { 457 return [$module->name() => $module]; 458 }); 459 } 460 461 /** 462 * Load a module in a static scope, to prevent it from modifying local or object variables. 463 * 464 * @param string $filename 465 * 466 * @return mixed 467 */ 468 private static function load(string $filename) 469 { 470 return include $filename; 471 } 472 473 /** 474 * A function filter modules by enabled/disabled 475 * 476 * @param bool $include_disabled 477 * 478 * @return Closure 479 */ 480 private function enabledFilter(bool $include_disabled): Closure 481 { 482 return function (ModuleInterface $module) use ($include_disabled): bool { 483 return $include_disabled || $module->isEnabled(); 484 }; 485 } 486 487 /** 488 * A function filter modules by type 489 * 490 * @param string $interface 491 * 492 * @return Closure 493 */ 494 private function interfaceFilter(string $interface): Closure 495 { 496 return function (ModuleInterface $module) use ($interface): bool { 497 return $module instanceof $interface; 498 }; 499 } 500 501 /** 502 * A function to sort footers 503 * 504 * @return Closure 505 */ 506 private function footerSorter(): Closure 507 { 508 return function (ModuleFooterInterface $x, ModuleFooterInterface $y): int { 509 return $x->getFooterOrder() <=> $y->getFooterOrder(); 510 }; 511 } 512 513 /** 514 * A function to sort menus 515 * 516 * @return Closure 517 */ 518 private function menuSorter(): Closure 519 { 520 return function (ModuleMenuInterface $x, ModuleMenuInterface $y): int { 521 return $x->getMenuOrder() <=> $y->getMenuOrder(); 522 }; 523 } 524 525 /** 526 * A function to sort menus 527 * 528 * @return Closure 529 */ 530 private function sidebarSorter(): Closure 531 { 532 return function (ModuleSidebarInterface $x, ModuleSidebarInterface $y): int { 533 return $x->getSidebarOrder() <=> $y->getSidebarOrder(); 534 }; 535 } 536 537 /** 538 * A function to sort menus 539 * 540 * @return Closure 541 */ 542 private function tabSorter(): Closure 543 { 544 return function (ModuleTabInterface $x, ModuleTabInterface $y): int { 545 return $x->getTabOrder() <=> $y->getTabOrder(); 546 }; 547 } 548 549 /** 550 * A function to sort modules by name 551 * 552 * @return Closure 553 */ 554 private function moduleSorter(): Closure 555 { 556 return function (ModuleInterface $x, ModuleInterface $y): int { 557 return I18N::strcasecmp($x->title(), $y->title()); 558 }; 559 } 560 561 /** 562 * Find a specified module, if it is currently active. 563 * 564 * @param string $module_name 565 * @param bool $include_disabled 566 * 567 * @return ModuleInterface|null 568 */ 569 public function findByName(string $module_name, bool $include_disabled = false): ?ModuleInterface 570 { 571 return $this->all($include_disabled) 572 ->filter(function (ModuleInterface $module) use ($module_name): bool { 573 return $module->name() === $module_name; 574 }) 575 ->first(); 576 } 577 578 /** 579 * Configuration settings are available through the various "module component" pages. 580 * For modules that do not provide a component, we need to list them separately. 581 * 582 * @param bool $include_disabled 583 * 584 * @return Collection 585 * @return ModuleConfigInterface[] 586 */ 587 public function otherModules(bool $include_disabled = false): Collection 588 { 589 return $this->findByInterface(ModuleInterface::class, $include_disabled, true) 590 ->filter(function (ModuleInterface $module): bool { 591 foreach (self::COMPONENTS as $interface) { 592 if ($module instanceof $interface) { 593 return false; 594 } 595 } 596 597 return true; 598 }); 599 } 600 601 /** 602 * Generate a list of module names which exist in the database but not on disk. 603 * 604 * @return Collection 605 * @return string[] 606 */ 607 public function deletedModules(): Collection 608 { 609 $database_modules = DB::table('module')->pluck('module_name'); 610 611 $disk_modules = $this->all(true) 612 ->map(function (ModuleInterface $module): string { 613 return $module->name(); 614 }); 615 616 return $database_modules->diff($disk_modules); 617 } 618} 619